# Online Payment Notifications Handling (IPN v2) Information Notifications are sent via POST method using Content-Type application/json. Each notification must return a plaintext response `OK` with HTTP status `200`. We also recommend additionally verifying the IP address from which the IPN was received. Endpoint for retrieving IP addresses: `https://api.simpay.pl/ip` ## Notification Structure | Field | Type | Description | | --- | --- | --- | | `type` | Enum | Transmitted event, full list below | | `notification_id` | char(36) | Unique notification ID | | `date` | date (ISO 8601) | Date and time of notification sending in ISO 8601 format (e.g. `2025-05-20T16:36:07Z`) | | `data` | Object | Data transmitted in this event | | `signature` | string | Calculated signature, details below | ## Signature Calculation Signatures are generated by concatenating the values of the `type` field, `notification_id`, all received parameters (except signature) to the API in the order from the tables below, separating them with the separator `|` and adding the key available in the Client Panel for the service at the end. Hashing must be done using sha256. The basic string to be hashed should look like this: ``` type|notification_id|date|data_value1|data_value2|data_valueN|key ``` Information Values from the `data` field should be added to the signature in the same order as they are listed in the documentation. Example method of generating a signature for the following sample data: - `type` = example:type - `notification_id` = 0196bf93-aca4-7253-8a2d-0941a037f25b - `date` = 2025-05-20T16:36:07Z - `data` has fields: - `status` = transaction_paid - `amount` (object) - `amount.currency` = PLN - `amount.value` = 5.25 - `transaction_id` = 123123123123 - `meta` = test - `key` = keyFromPanel Joining all elements correctly will look like this: ``` example:type|0196bf93-aca4-7253-8a2d-0941a037f25b|2025-05-20T16:36:07Z|transaction_paid|PLN|5.25|123123123123|test|keyFromPanel ``` If any field in the documentation informs us that it may not occur if e.g. it was not passed during payment initiation, we completely skip this element when generating the signature. Looking at the example, let's assume that the `meta` field may not occur and actually does not occur. In this case, the concatenation of elements will look like this: ``` example:type|0196bf93-aca4-7253-8a2d-0941a037f25b|2025-05-20T16:36:07Z|transaction_paid|PLN|5.25|123123123123|keyFromPanel ``` **This prepared string is passed to the sha256 hashing function.** SimPay sends the signature as a lower-case string. Example of generating signature value in selected languages and SDK: PHP ```php flattenArray($payload); $data[] = 'SERVICE_IPN_KEY'; $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(); } // rest of ipn ``` Node ```js const crypto = require('crypto'); class SimPaySignatureValidator { constructor(payload) { this.payload = payload; } isValidHex(str) { // Check if string has appropriate length for SHA-256 (64 characters) if (typeof str !== 'string' || str.length !== 64) { return false; } // Check if contains only hex characters (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 || ''; // Check if signature from payload is valid hex if (!this.isValidHex(payloadSignature)) { return false; } // Secure comparison using 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; } } // Example usage with 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}`); }); // Export class for use in other modules 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: # Convert None to empty 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'); } ``` ## Available Global Data Types: 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`) ## Available Values in `type` Field: ### `transaction:status_changed` - transaction status change Data in the data field: | Field | Type | Description | Example Value | | --- | --- | --- | --- | | `id` | UUID | Transaction ID sent after transaction generation | 00554475-7ebb-4f16-b30b-0ce21da1a03b | | `payer_transaction_id` | char(8) | Transaction ID shown to the payer | 4878R2PN | | `service_id` | char(8) | Service identifier | e65c7519 | | `status` | TransactionStatusEnum | Transaction status | transaction_paid | | `amount` | Object | Amount information object | | | `amount.final_currency` | ISO 4217 | Currency in which the payer made the payment | PLN | | `amount.final_value` | string (%.2f) | Final amount the payer paid (e.g. "0.30", "10.00", "12.37") | 8.47 | | `amount.original_currency` | ISO 4217 | Currency that was declared during payment initiation | EUR | | `amount.original_value` | string (%.2f) | Declared amount during payment initiation (e.g. "0.30", "10.00", "12.37") | 2.00 | | `amount.commission_system` | string (%.2f) or null | Commission amount taken by SimPay (e.g. "0.30", "10.00", "12.37") | 0.13 | | `amount.commission_partner` | string (%.2f) or null | Commission amount for the Partner (e.g. "0.30", "10.00", "12.37") | 0.13 | | `amount.commission_currency` | ISO 4217 or null | Currency in which commission was taken (PLN/EUR) | PLN | | `control` | string | Field sent only when passed during payment initiation | SHOP_ORDER_1 | | `payment` | Object | Field with payment information | | | `payment.channel` | string | Payment channel used by the payer | blik | | `payment.type` | string | Type/group of payment method | blik | | `customer` | Object | Payer information object | | `customer.country_code` | ISO 3166-1 Alpha-2 or null | Buyer's country (e.g. "PL") | PLN | | `paid_at` | ISO 8601 or null (may not be sent when transaction is not paid) | Date and time of payment | 2025-05-26T15:10:24Z | | `created_at` | ISO 8601 | Date and time of transaction creation | 2025-05-26T15:09:59Z | Example sent event (for 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` - refund status change Data in the data field: | Field | Type | Description | Example Value | | --- | --- | --- | --- | | `id` | UUID | SimPay refund ID | 0194837c-69df-71dd-adff-4b3058f3fb58 | | `service_id` | char(8) | Service identifier | e65c7519 | | `status` | RefundStatusEnum | Refund status | refund_completed | | `amount` | Object | Amount information object | | `amount.currency` | ISO 4217 | Currency in which the refund was requested | PLN | | `amount.value` | string (%.2f) | Amount requested for refund | 8.47 | | `amount.wallet_currency` | ISO 4217 | Wallet currency from which funds were taken | EUR | | `amount.wallet_value` | string (%.2f) | Amount taken from wallet | 2.00 | | `transaction` | Object | Field with payment information | | `transaction.id` | UUID | SimPay transaction ID | 00554475-7ebb-4f16-b30b-0ce21da1a03b | | `transaction.payment_channel` | string | Payment channel used by the payer | blik | | `transaction.payment_type` | string | Type/group of payment method | blik | Example sent event (for 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` - test notification This notification can be sent from the Client Panel. Data in the data field: | Field | Type | Description | Example Value | | --- | --- | --- | --- | | `service_id` | char(8) | Service identifier | e65c7519 | | `nonce` | string | Random string | 01JVZCXGZ77DJTM08WMSX34ETQ | Example sent event (for 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` - BLIK code status change This notification is sent only for BLIK Level 0 transactions and BLIK Recurrent Payment transactions. Data in the data field: | Field | Type | Description | Example value | | --- | --- | --- | --- | | `ticket_status` | string | BLIK code status (see the BLIK Level 0 tab in the documentation) | VALID | | `transaction` | object | Transaction object | | | `transaction.id` | Uuid | Transaction UUID | 70bc5ab3-4973-4275-a0eb-08e3f2ab54f2 | | `transaction.payer_transaction_id` | Char(8) | Payment ID provided to the payer | 6PB8JKKN | | `transaction.service_id` | Char(8) | Service ID | e65c7519 | | `transaction.status` | TransactionStatusEnum | Transaction status | transaction_paid | | `transaction.amount` | Object | Amount information object | | | `transaction.amount.final_currency` | ISO 4217 | Currency in which the payer made the payment | PLN | | `transaction.amount.final_value` | string (%.2f) | Final amount paid by the payer (e.g. "0.30", "10.00", "12.37") | 8.47 | | `transaction.amount.original_currency` | ISO 4217 | Currency declared at payment initiation | EUR | | `transaction.amount.original_value` | string (%.2f) | Declared amount at payment initiation (e.g. "0.30", "10.00", "12.37") | 2.00 | | `transaction.amount.commission_system` | string (%.2f) or null | Commission amount charged by SimPay (e.g. "0.30", "10.00", "12.37") | 0.13 | | `transaction.amount.commission_partner` | string (%.2f) or null | Commission amount for the Partner (e.g. "0.30", "10.00", "12.37") | 0.13 | | `transaction.amount.commission_currency` | ISO 4217 or null | Currency in which the commission was charged (PLN/EUR) | PLN | | `transaction.control` | string or null | Value of the control field (null if not provided) | 111122223333 | Example sent event (for 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" } ### `blik:alias_status_changed` - Change of BLIK Alias Status This notification is sent only for BLIK Recurring Payments when the alias status changes. Data fields: | Example event (for ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "blik:alias_status_changed", "notification_id": "019972b2-0233-73d7-ad91-c114b62d56e3", "date": "2025-09-22T20:31:32+02:00", "data": { "id": "019972b1-e4c0-714f-a10b-f88a158bee50", "service_id": "e65c7519", "type": "PAYID", "value": "AABBCCDD", "label": "test", "status": "alias_active", "created_at": "2025-09-22T20:31:25+02:00", "updated_at": "2025-09-22T20:31:32+02:00" }, "signature": "8f1a0f6de3eb5c050e70ca0d7407588e380e8e607e131e289afb0a3a2996d4a1" } ``` ### `subscription:status_changed` - Change of Subscription Status This notification is sent only for payment methods related to subscriptions/Recurring Payments. Data fields: | Field | Type | Description | Example Value | | --- | --- | --- | --- | | `id` | uuid | Subscription ID | 019972b1-e4df-70c4-8c9b-6a89f6ccc948 | | `service_id` | Char(8) | Service identifier | e65c7519 | | `status` | SubscriptionStatusEnum | Subscription status | subscription_active | | `mode` | SubscriptionModeEnum | Subscription mode | BLIK | | `created_at` | datetime (ISO 8601) | Alias creation date | 2025-09-22T20:31:25+02:00 | | `updated_at` | datetime (ISO 8601) | Alias last update date | 2025-09-22T20:31:32+02:00 | | `blik` | object | Object containing BLIK subscription details (only included when mode == BLIK) | | `blik.model` | BlikSubscriptionModelEnum | BLIK subscription model | O | | `blik.currency` | char(3) | BLIK subscription currency | PLN | | `blik.alias` | object | BLIK alias data | | `blik.alias.id` | uuid | BLIK alias ID | 019972b1-e4c0-714f-a10b-f88a158bee50 | | `blik.alias.type` | string | BLIK alias type | PAYID | | `blik.alias.value` | string | BLIK alias value | AABBCCDD | | `blik.alias.label` | string | Label displayed in the payer’s banking application | test | | `blik.alias.status` | BlikAliasStatusEnum | BLIK alias status | alias_active | | `blik.alias.created_at` | datetime (ISO 8601) | Alias creation date | 2025-09-22T20:31:25+02:00 | | `blik.alias.updated_at` | datetime (ISO 8601) | Alias last update date | 2025-09-22T20:31:32+02:00 | Example event (for ipn key = `UwSkKiIwlxIeOMF8MIq9iDkQWBTtjoJQ`): ```json { "type": "subscription:status_changed", "notification_id": "019972b2-0360-7177-a635-f26b05f632b8", "date": "2025-09-22T20:31:32+02:00", "data": { "id": "019972b1-e4df-70c4-8c9b-6a89f6ccc948", "service_id": "e65c7519", "status": "subscription_active", "mode": "BLIK", "created_at": "2025-09-22T20:31:25+02:00", "updated_at": "2025-09-22T20:31:32+02:00", "blik": { "model": "O", "currency": "PLN", "alias": { "id": "019972b1-e4c0-714f-a10b-f88a158bee50", "type": "PAYID", "value": "AABBCCDD", "label": "test", "status": "alias_active", "created_at": "2025-09-22T20:31:25+02:00", "updated_at": "2025-09-22T20:31:32+02:00" } } }, "signature": "0851c6bc63e92bff424a83ab9067c192d6e9a1b5d67ade9960f0cce67216d3f1" } ```