SimPay Flow uses two independent kinds of notifications:
| Kind | What it covers | Default URL and key |
|---|---|---|
| Flow webhooks | Status changes of submerchants, their payment channels and withdrawals | URL and key set in the Flow service in the SimPay Partner Panel |
| Payment webhooks (IPN) | Submerchant transactions, refunds, BLIK codes, subscriptions and BLIK aliases - standard IPN notifications | Parent service IPN URL and key |
You can override both URLs for each submerchant - see Custom URLs for a submerchant.
Flow webhooks are sent with the POST method as JSON, with the User-Agent: SimPay-IPN/2.0 header.
{
"type": "flow_submerchant_withdraw:status_changed",
"webhook_id": "0199a6c2-5f3e-7d21-8b4a-2c3d4e5f6a7b",
"attempt": 0,
"flow_service": {
"id": "flow_aaaabbbb",
"name": "My platform"
},
"date": "2026-10-01T12:00:00+02:00",
"data": {
"id": "0199a5f0-2b8c-7c1e-9e3a-1f2b3c4d5e6f",
"short_id": "1f2b3c4d",
"amount": 150.5,
"created_at": "2026-10-01T10:00:00+02:00",
"updated_at": "2026-10-01T12:00:00+02:00",
"wallet": {
"id": "0199a5f0-1111-7c1e-9e3a-1f2b3c4d5e6f",
"currency": "PLN"
},
"status": {
"previous": "accepted",
"current": "done"
},
"submerchant": {
"id": "flsm_aaaabbbb",
"custom_id": "user-123"
}
},
"signature": "f54b85c687f2f4dd6f554350561bf9eb970bc51f0c5bbd1bf4b5172ad07f9f4e"
}| Field | Description |
|---|---|
type | Event type (see below) |
webhook_id | Unique notification ID (UUID). Each retry attempt has a new webhook_id. |
attempt | Delivery attempt number, starting from 0 |
flow_service.id | Flow service ID |
flow_service.name | Flow service name |
date | Notification send date (ISO 8601) |
data | Event data - depends on type |
signature | SHA-256 signature - see Signature verification |
A notification is considered delivered when your server responds with:
- code
200with the bodyOK, or - code
202or204.
Any other response, connection error or timeout (5 s to connect, 15 s for the response) means a failed attempt.
Failed notifications are retried - up to 30 attempts:
| Attempts | Interval between attempts |
|---|---|
| 1-10 | approx. 1 minute |
| 11-15 | approx. 5 minutes |
| 16-20 | approx. 30 minutes |
| 21-30 | approx. 1 hour |
If the object's state changes again before a retry (e.g. a withdrawal goes from accepted to done), the outdated notification is no longer sent - you'll only receive the notification about the latest change. Therefore always treat current as the current state and don't assume you'll receive every intermediate change.
You'll find the history of sent webhooks (including your server's responses) in the SimPay Partner Panel, in the Flow service settings. You can also send a test notification from there.
The submerchant status has changed - e.g. after onboarding data is submitted, after SimPay approval or after the account is blocked.
"data": {
"submerchant": {
"id": "flsm_aaaabbbb",
"custom_id": "user-123"
},
"previous": {
"status": "client_verify",
"aml_level": 1,
"aml_status": "sent",
"fully_active": false
},
"current": {
"status": "client_active",
"aml_level": 2,
"aml_status": "accepted",
"fully_active": true
}
}| Field | Description |
|---|---|
status | Submerchant account status at SimPay (e.g. client_new, client_verify, client_active, client_block) |
aml_level | AML verification level (2 - full verification) |
aml_status | AML verification status (e.g. sent, accepted, declined) |
fully_active | true when the submerchant is fully active and can accept payments |
To decide whether the submerchant can accept payments, use the current.fully_active field. The other fields are informational.
The status of a submerchant payment channel has changed - e.g. a channel sent for verification was approved or rejected, the BLIK OneClick status changed, or the active BLIK Recurring Payments models changed.
"data": {
"submerchant": {
"id": "flsm_aaaabbbb",
"custom_id": "user-123"
},
"channel": {
"slug": "blik-level0",
"name": "BLIK Level 0"
},
"previous": {
"status": "active",
"blik_one_click_status": "REGISTERED_WAITING_ACTIVATION",
"blik_recurrent_a": null,
"blik_recurrent_m": null,
"blik_recurrent_o": null
},
"current": {
"status": "active",
"blik_one_click_status": "ACTIVE",
"blik_recurrent_a": null,
"blik_recurrent_m": null,
"blik_recurrent_o": null
}
}| Field | Description |
|---|---|
channel.slug | Payment channel ID (same as id in GET .../payment-channels) |
status | Channel status: new, in_progress, active, rejected |
blik_one_click_status | BLIK OneClick status (only blik-level0): PENDING_REGISTRATION, REGISTERED_WAITING_ACTIVATION, ACTIVE, REJECTED or null |
blik_recurrent_a / _m / _o | Whether the BLIK Recurring Payments model AUTO / MANUAL / OPEN is active (only blik-recurrent) or null |
The status of a submerchant withdrawal has changed. Example data - see Notification structure.
| Field | Description |
|---|---|
id / short_id | Withdrawal ID (UUID) and withdrawal short ID |
amount | Withdrawal amount |
wallet.id / wallet.currency | Wallet the withdrawal is made from and its currency |
status.previous / status.current | Previous and current withdrawal status: new, accepted, waiting, done, rejected |
Test notification sent from the Partner Panel. data contains only a random nonce field:
"data": {
"nonce": "01K6FZ8X3Q9V2N7M4T5R1W0YAB"
}Every Flow webhook contains the signature field - a SHA-256 hash (lowercase hex). The rule is the same as for payment notifications:
- take all values from the notification (without the
signaturefield) in the order they appear in the JSON, flattening nested objects, - join them with the
|separator, - append the Flow webhook key at the end,
- compute SHA-256 and compare it with
signatureusing a timing-safe comparison function.
type|webhook_id|attempt|flow_service.id|flow_service.name|date|data_value1|...|data_valueN|FLOW_WEBHOOK_KEYUnlike payment notifications, Flow webhooks contain true/false, null and numeric values. Before joining, convert them to text the way PHP does:
true→1,false→ empty string,null→ empty string,- numbers → without redundant zeros (
0,2,150.5,100for100.0).
In JavaScript String(true) gives "true", and in Python str(True) gives "True" - without the conversion the signature won't match.
Key: if the submerchant has a custom Flow webhook URL, notifications about it are signed with its key. Otherwise - with the webhook key set in the Flow service.
See the signature calculation for the example above
Key: FLOW_WEBHOOK_KEY_64
flow_submerchant_withdraw:status_changed|0199a6c2-5f3e-7d21-8b4a-2c3d4e5f6a7b|0|flow_aaaabbbb|My platform|2026-10-01T12:00:00+02:00|0199a5f0-2b8c-7c1e-9e3a-1f2b3c4d5e6f|1f2b3c4d|150.5|2026-10-01T10:00:00+02:00|2026-10-01T12:00:00+02:00|0199a5f0-1111-7c1e-9e3a-1f2b3c4d5e6f|PLN|accepted|done|flsm_aaaabbbb|user-123|FLOW_WEBHOOK_KEY_64SHA-256: f54b85c687f2f4dd6f554350561bf9eb970bc51f0c5bbd1bf4b5172ad07f9f4e
<?php
function simpayFlowSignatureValid(array $payload, string $key): bool
{
$signature = $payload['signature'] ?? '';
unset($payload['signature']);
$values = [];
array_walk_recursive($payload, function ($value) use (&$values) {
$values[] = $value;
});
$values[] = $key;
// implode() converts true to "1", and false and null to an empty string
return hash_equals(hash('sha256', implode('|', $values)), $signature);
}
$payload = json_decode(file_get_contents('php://input'), true);
if (!is_array($payload) || !simpayFlowSignatureValid($payload, 'FLOW_WEBHOOK_KEY')) {
http_response_code(403);
exit('INVALID_SIGNATURE');
}
// handle the $payload['type'] event
echo 'OK';Notifications about submerchant transactions, refunds, BLIK codes, subscriptions and BLIK aliases are standard IPN notifications - with the same structure and the same signature calculation. In SimPay Flow they differ in a few details.
| Situation | URL | Signature key |
|---|---|---|
| The submerchant doesn't have a custom IPN URL | Parent service IPN URL | Parent service IPN key |
| The submerchant has a custom IPN URL | Submerchant's custom URL | Submerchant's IPN key (returned when the URL was set) |
This way, in the basic setup you handle notifications for all submerchants with a single endpoint and a single key - the same one you use for the parent service.
In all payment notifications related to submerchants (transaction:status_changed, transaction_refund:status_changed, transaction_blik_level0:code_status_changed, subscription:status_changed, blik:alias_status_changed) the service_id field contains the ID of the parent service - the one you pass in the /payment/{serviceId}/... path. The internal submerchant service never appears in notifications.
Every payment notification related to a submerchant (all events except ipn:test) contains the flow object in data. For regular Online Payments services the object is not present.
| Field | Description |
|---|---|
flow.submerchant_id | Submerchant ID (flsm_...) |
flow.submerchant_custom_id | Submerchant customId |
flow.flow_service_id | Flow service ID |
Since service_id points to the parent service, it's the flow object that lets you assign a transaction, refund, subscription or alias to the right submerchant in your system.
"flow": {
"submerchant_id": "flsm_aaaabbbb",
"submerchant_custom_id": "user-123",
"flow_service_id": "flow_aaaabbbb"
}Additionally, the transaction:status_changed event contains the amount.commission_flow field - your Flow commission on the transaction (string, %.2f format). It's present only when the commission is greater than zero.
{
"type": "transaction:status_changed",
"notification_id": "0199a6d0-1234-7abc-9def-0123456789ab",
"date": "2026-10-01T12:05:00+02:00",
"data": {
"id": "0199a6cf-aaaa-7bbb-8ccc-0123456789ab",
"payer_transaction_id": "Q68KLAKN",
"service_id": "252b85c9",
"status": "transaction_paid",
"amount": {
"final_currency": "PLN",
"final_value": "49.99",
"original_currency": "PLN",
"original_value": "49.99",
"commission_system": "0.60",
"commission_partner": "49.14",
"commission_currency": "PLN",
"commission_flow": "0.25"
},
"control": "order-1001",
"payment": {
"channel": "blik",
"type": "blik"
},
"customer": {
"country_code": "PL"
},
"paid_at": "2026-10-01T12:04:58+02:00",
"created_at": "2026-10-01T12:03:10+02:00",
"flow": {
"submerchant_id": "flsm_aaaabbbb",
"submerchant_custom_id": "user-123",
"flow_service_id": "flow_aaaabbbb"
}
},
"signature": "..."
}The commission_flow and flow fields are included in the signature calculation like any other field of the data object. If your validator dynamically flattens the whole payload (as in the examples), you don't need to change anything.
By default, all notifications go to the URLs shared by the whole service. You can set separate URLs for a selected submerchant - e.g. when each of your Customers has its own application instance.
You get the current settings with GET .../submerchants/{submerchantId}/webhooks and change them with PATCH .../submerchants/{submerchantId}/webhooks:
curl -X PATCH "https://api.simpay.pl/flow/{flowServiceId}/submerchants/user-123/webhooks" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow": { "customUrl": "https://customer-123.example.com/webhooks/flow" },
"payment": { "customUrl": "https://customer-123.example.com/webhooks/ipn" }
}'{
"success": true,
"data": {
"paymentNewIpnKey": "aVeryLongAndSecureKeyFor32Chars0",
"flowNewIpnKey": "..."
}
}When a custom URL is set for the first time, SimPay generates a separate key for the submerchant to verify notification signatures. The key is returned only once - in the response to this request. On subsequent URL changes the key doesn't change, and the response field is null.
To go back to the default URL, send {"flow": {"useDefault": true}} (or payment). The submerchant's custom URL and key will be removed, and notifications will go back to the service URL and key.
If the key leaked or you lost it, generate a new one with PATCH .../webhooks/key with {"type": "flow"} or {"type": "payment"}. This works only for submerchants with a custom webhook URL of the given type. After a new key is generated, the previous one is no longer used.