# Obsługa notyfikacji Płatności Online (IPN v2) Informacja Notyfikacje wysyłane są metodą POST za pomocą Content-Type application/json. Każda notyfikacja musi zwrócić w response jako plaintext odpowiedź `OK` ze statusem HTTP `200`. Zalecamy dodatkowo sprawdzanie adresu IP, z którego przyszedł IPN. Endpoint do pobierania adresów IP: `https://api.simpay.pl/ip` ## Struktura notyfikacji | Pole | Typ | Opis | | --- | --- | --- | | `type` | Enum | Przesyłany event, pełna lista poniżej | | `notification_id` | char(36) | Unikalne ID notyfikacji | | `date` | date (ISO 8601) | Data i godzina wysyłki notyfikacji w formacie ISO 8601 (np. `2025-05-20T16:36:07Z`) | | `data` | Obiekt | Dane przesyłane w tym evencie | | `signature` | string | Wyliczona sygnatura, szczegóły poniżej | ## Wyliczanie sygnatury Sygnature generujemy za pomocą zestawienia ze sobą wartości pola `type`, `notification_id`, wszystkich odebranych parametrów (oprócz signature) do API w kolejności z tabel poniżej, oddzielając je seperatorem `|` i dodając na końcu klucz dostępny w Panelu Klienta w usłudze. Hashowanie musi odbyć się za pomocą sha256. Bazowo ciąg do zahashowania powinnien wyglądać tak: ``` type|notification_id|date|data_value1|data_value2|data_valueN|key ``` Informacja Wartości z pola `data` powinny być dołączane do sygnatury w takiej kolejności, jak zostały wypisane w dokumentacji. Przykładowy sposób generowania sygnatury dla poniższych przykładowych danych: - `type` = example:type - `notification_id` = 0196bf93-aca4-7253-8a2d-0941a037f25b - `date` = 2025-05-20T16:36:07Z - `data` ma pola: - `status` = transaction_paid - `amount` (obiekt) - `amount.currency` = PLN - `amount.value` = 5.25 - `transaction_id` = 123123123123 - `meta` = test - `key` = keyFromPanel Łącząc wszystkie elementy prawidłowo będzie wyglądać tak: ``` example:type|0196bf93-aca4-7253-8a2d-0941a037f25b|2025-05-20T16:36:07Z|transaction_paid|PLN|5.25|123123123123|test|keyFromPanel ``` Jeśli jakieś pole w dokumentacji informuje nas o tym, że może nie występować jeśli np. nie został przekazany podczas inicjacji płatności to pomijamy całkowicie ten element podczas generowania sygnatury. Patrząc na przykład, załóżmy, że pole `meta` może nie występować i faktycznie nie występuje. W takim przypadku konkatenacja elementów będzie wyglądać tak: ``` example:type|0196bf93-aca4-7253-8a2d-0941a037f25b|2025-05-20T16:36:07Z|transaction_paid|PLN|5.25|123123123123|keyFromPanel ``` **Tak przygotowany łańcuch znaków przekazujemy funkcji hashującej sha256.** SimPay przesyła sygnaturę jako string lower-case. Przykład generowania wartości sygnatury w wybranych językach i SDK: PHP ```php flattenArray($payload); $data[] = 'KLUCZ_IPN_USŁUGI'; $signature = hash('sha256', implode('|', $data)); return hash_equals($signature, $payload['signature']); } private function flattenArray(array $array): array { unset($array['signature']); $return = []; array_walk_recursive($array, function ($a) use (&$return) { $return[] = $a; }); return $return; } } $validator = new SimPaySignatureValidator(); if(!$validator->isValid()) { http_response_code(403); echo 'INVALID_SIGNATURE'; die(); } // reszta ipn ``` Node ```js const crypto = require('crypto'); class SimPaySignatureValidator { constructor(payload) { this.payload = payload; } isValidHex(str) { // Sprawdź czy string ma odpowiednią długość dla SHA-256 (64 znaki) if (typeof str !== 'string' || str.length !== 64) { return false; } // Sprawdź czy zawiera tylko znaki hex (0-9, a-f, A-F) return /^[0-9a-fA-F]+$/.test(str); } isValid() { if (!this.payload || typeof this.payload !== 'object') { return false; } const data = this.flattenArray(this.payload); data.push('SERVICE_IPN_KEY'); const signature = crypto .createHash('sha256') .update(data.join('|')) .digest('hex'); const payloadSignature = this.payload.signature || ''; // Sprawdź czy sygnatura z payload jest prawidłowym hexem if (!this.isValidHex(payloadSignature)) { return false; } // Bezpieczne porównanie używając timingSafeEqual return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(payloadSignature, 'hex') ); } flattenArray(array) { const arrayClone = { ...array }; delete arrayClone.signature; const result = []; const flatten = (obj) => { if (Array.isArray(obj)) { obj.forEach(item => flatten(item)); } else if (typeof obj === 'object' && obj !== null) { Object.values(obj).forEach(value => flatten(value)); } else { result.push(obj); } }; flatten(arrayClone); return result; } } // Przykład użycia z Express.js const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhook', (req, res) => { const validator = new SimPaySignatureValidator(req.body); if (!validator.isValid()) { return res.status(403).send('INVALID_SIGNATURE'); } res.send('OK'); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); // Eksport klasy do użycia w innych modułach module.exports = SimPaySignatureValidator; ``` Python (Flask) ```python import hashlib import hmac import json from flask import Flask, request, abort app = Flask(__name__) class SimPaySignatureValidator: SECRET_KEY = 'SERVICE_IPN_KEY' def is_valid(self, payload: dict) -> bool: if not payload: return False signature_from_payload = payload.get('signature') if not signature_from_payload: return False data = self.flatten_array(payload) data.append(self.SECRET_KEY) signature = hashlib.sha256('|'.join(data).encode()).hexdigest() return hmac.compare_digest(signature, signature_from_payload) def flatten_array(self, obj, exclude_key='signature'): result = [] def _recurse(o): if isinstance(o, dict): for k, v in o.items(): if k == exclude_key: continue _recurse(v) elif isinstance(o, list): for item in o: _recurse(item) else: # Zamiana None na pusty string result.append(str(o) if o is not None else '') _recurse(obj) return result @app.route('/', methods=['POST']) def validate_signature(): try: payload = request.get_json(force=True) except Exception: abort(403, 'INVALID_SIGNATURE') validator = SimPaySignatureValidator() if not validator.is_valid(payload): abort(403, 'INVALID_SIGNATURE') return Response("OK", status=200, mimetype="text/plain") if __name__ == '__main__': app.run(port=5000) ``` Laravel SDK ```php // for more details visit https://github.com/SimPaypl/simpay-laravel ipnSignatureValid(request())) { abort(403, 'invalid signature'); } ``` ## Dostępne globalne typy danych: 1. TransactionStatusEnum - Enum(`transaction_new`, `transaction_confirmed`, `transaction_generated`, `transaction_paid`, `transaction_failure`, `transaction_expired`, `transaction_canceled`, `transaction_refunded`) 2. RefundStatusEnum - Enum(`refund_new`, `refund_pending`, `refund_completed`, `refund_rejected`, `refund_failed`) ## Dostępne wartości w polu `type`: ### `transaction:status_changed` - zmiana statusu transakcji Dane w polu data: | Pole | Typ | Opis | Przykładowa wartość | | --- | --- | --- | --- | | `id` | UUID | ID transakcji przesyłany po wygenerowaniu transakcji | 00554475-7ebb-4f16-b30b-0ce21da1a03b | | `payer_transaction_id` | char(8) | ID transakcji pokazywane płacącemu | 4878R2PN | | `service_id` | char(8) | Identyfikator usługi | e65c7519 | | `status` | TransactionStatusEnum | Status transakcji | transaction_paid | | `amount` | Obiekt | Obiekt informacji o kwocie | | | `amount.final_currency` | ISO 4217 | Waluta, w której płacący dokonał płatności | PLN | | `amount.final_value` | string (%.2f) | Finalna kwota, którą płacący zapłacił (np. "0.30", "10.00", "12.37") | 8.47 | | `amount.original_currency` | ISO 4217 | Waluta, która była zadeklarowana przy inicjacji płatności | EUR | | `amount.original_value` | string (%.2f) | Zadeklarowana kwota przy inicjacji płatności (np. "0.30", "10.00", "12.37") | 2.00 | | `amount.commission_system` | string (%.2f) or null | Kwota prowizji, która została pobrana przez SimPay (np. "0.30", "10.00", "12.37") | 0.13 | | `amount.commission_partner` | string (%.2f) or null | Kwota prowizji, która została dla Partnera (np. "0.30", "10.00", "12.37") | 0.13 | | `amount.commission_currency` | ISO 4217 or null | Waluta, w której została pobrana prowizja (PLN/EUR) | PLN | | `control` | string | Pole przesyłane tylko wtedy, gdy zostało przekazane podczas inicjacji płatności | SHOP_ORDER_1 | | `payment` | Obiekt | Pole z informacjami o płatności | | | `payment.channel` | string | Kanał płatności, którym zapłacił płacący | blik | | `payment.type` | string | Typ/grupa metody płatności | blik | | `customer` | Obiekt | Obiekt informacji o płacącym | | `customer.country_code` | ISO 3166-1 Alpha-2 or null | Kraj kupującego (np. "PL") | PLN | | `paid_at` | ISO 8601 or null (może nie być przesyłane, kiedy transakcja nie opłacona) | Data i czas zapłaty | 2025-05-26T15:10:24Z | | `created_at` | ISO 8601 | Data i czas utworzenia transakcji | 2025-05-26T15:09:59Z | Przykładowy wysyłany event (dla ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "transaction:status_changed", "notification_id": "0196fec6-7a61-7219-9458-bcc45237c252", "date": "2025-05-23T22:12:22+02:00", "data": { "id": "dbc87423-b121-4ad4-977f-b63c3d3831e8", "payer_transaction_id": "Q68KLAKN", "service_id": "e65c7519", "status": "transaction_failure", "amount": { "final_currency": "PLN", "final_value": "8.00", "original_currency": "PLN", "original_value": "8.00", "commission_system": "0.06", "commission_partner": "7.94", "commission_currency": "PLN" }, "control": "3e63e31d-f08d-4942-a223-3bad2dce8096", "payment": { "channel": "blik", "type": "blik" }, "customer": { "country_code": null }, "created_at": "2024-08-10T15:41:50+02:00" }, "signature": "095a7be5d77c4bab667dbd0f35d9b1cb0c9cec50d8af42842cc37a95b233925e" } ``` ### `transaction_refund:status_changed` - zmiana statusu zwrotu Dane w polu data: | Pole | Typ | Opis | Przykładowa wartość | | --- | --- | --- | --- | | `id` | UUID | ID zwrotu SimPay | 0194837c-69df-71dd-adff-4b3058f3fb58 | | `service_id` | char(8) | Identyfikator usługi | e65c7519 | | `status` | RefundStatusEnum | Status zwrotu | refund_completed | | `amount` | Obiekt | Obiekt informacji o kwocie | | `amount.currency` | ISO 4217 | Waluta, w której zlecono zwrot | PLN | | `amount.value` | string (%.2f) | Kwota, zlecona w zwrocie | 8.47 | | `amount.wallet_currency` | ISO 4217 | Waluta portfela, z którego pobrano środki | EUR | | `amount.wallet_value` | string (%.2f) | Kwota, którą pobrano z portfela | 2.00 | | `transaction` | Obiekt | Pole z informacjami o płatności | | `transaction.id` | UUID | ID transakcji SimPay | 00554475-7ebb-4f16-b30b-0ce21da1a03b | | `transaction.payment_channel` | string | Kanał płatności, którym zapłacił płacący | blik | | `transaction.payment_type` | string | Typ/grupa metody płatności | blik | Przykładowy wysyłany event (dla ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "transaction_refund:status_changed", "notification_id": "0196ff00-376d-7399-a457-d166c9adf073", "date": "2025-05-23T23:15:26+02:00", "data": { "id": "0194837c-69df-71dd-adff-4b3058f3fb58", "service_id": "e65c7519", "status": "refund_completed", "amount": { "currency": "PLN", "value": "1.00", "wallet_currency": "PLN", "wallet_value": "1.00" }, "transaction": { "id": "e568d9ba-a85a-444c-87c4-3b1e431428d1", "payment_channel": "paysafecard", "payment_type": "paysafe" } }, "signature": "835819c5720c74f01b8d10a58b2dc43185f05379ea26f25a15158061f3bce10c" } ``` ### `ipn:test` - testowe powiadomienie To powiadomienie można wysłać z Panelu Klienta. Dane w polu data: | Pole | Typ | Opis | Przykładowa wartość | | --- | --- | --- | --- | | `service_id` | char(8) | Identyfikator usługi | e65c7519 | | `nonce` | string | Losowy ciąg znaków | 01JVZCXGZ77DJTM08WMSX34ETQ | Przykładowy wysyłany event (dla ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "ipn:test", "notification_id": "0196fece-c3e7-71ba-ac8a-ac64056d7d6b", "date": "2025-05-23T22:21:25+02:00", "data": { "service_id": "e65c7519", "nonce": "01JVZCXGZ77DJTM08WMSX34ETQ" }, "signature": "02df1a420def7e5de9b316d2bd1ef70796f50abc461561a85bb1243f0a08984d" } ``` ### `transaction_blik_level0:code_status_changed` - zmiana statusu kodu BLIK To powiadomienie jest wysyłane tylko przy transakcjach BLIK Level 0. Dane w polu data: | Pole | Typ | Opis | Przykładowa wartość | | --- | --- | --- | --- | | `ticket_status` | string | Status kodu BLIK (zobacz zakładkę BLIK Level 0 w dokumentacji) | VALID | | `transaction` | obiekt | Obiekt transakcji | | | `transaction.id` | Uuid | UUID transakcji | 70bc5ab3-4973-4275-a0eb-08e3f2ab54f2 | | `transaction.payer_transaction_id` | Char(8) | Identyfikator płatności przekazany płacącemu | 6PB8JKKN | | `transaction.service_id` | Char(8) | Identyfikator usługi | e65c7519 | | `transaction.status` | TransactionStatusEnum | Status transakcji | transaction_paid | | `transaction.amount` | Obiekt | Obiekt informacji o kwocie | | | `transaction.amount.final_currency` | ISO 4217 | Waluta, w której płacący dokonał płatności | PLN | | `transaction.amount.final_value` | string (%.2f) | Finalna kwota, którą płacący zapłacił (np. "0.30", "10.00", "12.37") | 8.47 | | `transaction.amount.original_currency` | ISO 4217 | Waluta, która była zadeklarowana przy inicjacji płatności | EUR | | `transaction.amount.original_value` | string (%.2f) | Zadeklarowana kwota przy inicjacji płatności (np. "0.30", "10.00", "12.37") | 2.00 | | `transaction.amount.commission_system` | string (%.2f) or null | Kwota prowizji, która została pobrana przez SimPay (np. "0.30", "10.00", "12.37") | 0.13 | | `transaction.amount.commission_partner` | string (%.2f) or null | Kwota prowizji, która została dla Partnera (np. "0.30", "10.00", "12.37") | 0.13 | | `transaction.amount.commission_currency` | ISO 4217 or null | Waluta, w której została pobrana prowizja (PLN/EUR) | PLN | | `transaction.control` | string or null | Wartość pola control (jeśli brak - null) | 111122223333 | Przykładowy wysyłany event (dla ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "transaction_blik_level0:code_status_changed", "notification_id": "019736c4-50c3-7108-944c-11a0f9c12b72", "date": "2025-06-03T19:08:44+02:00", "data": { "ticket_status": "VALID", "transaction": { "id": "70bc5ab3-4973-4275-a0eb-08e3f2ab54f2", "payer_transaction_id": "6PB8JKKN", "service_id": "e65c7519", "status": "transaction_paid", "amount": { "final_currency": "PLN", "final_value": "360.00", "original_currency": "PLN", "original_value": "360.00", "commission_system": "5.36", "commission_partner": "354.64", "commission_currency": "PLN" }, "control": "111122223333" } }, "signature": "236197b1a75a8e1c33b0feea94cab753f4b0d7f5ffd044abeea36ea8b89566cc" } ```