Skip to content
Last updated

SimPay Flow uses two independent kinds of notifications:

KindWhat it coversDefault URL and key
Flow webhooksStatus changes of submerchants, their payment channels and withdrawalsURL 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 notificationsParent service IPN URL and key

You can override both URLs for each submerchant - see Custom URLs for a submerchant.


Flow webhooks

Notification structure

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"
}
FieldDescription
typeEvent type (see below)
webhook_idUnique notification ID (UUID). Each retry attempt has a new webhook_id.
attemptDelivery attempt number, starting from 0
flow_service.idFlow service ID
flow_service.nameFlow service name
dateNotification send date (ISO 8601)
dataEvent data - depends on type
signatureSHA-256 signature - see Signature verification

Response

A notification is considered delivered when your server responds with:

  • code 200 with the body OK, or
  • code 202 or 204.

Any other response, connection error or timeout (5 s to connect, 15 s for the response) means a failed attempt.

Retries

Failed notifications are retried - up to 30 attempts:

AttemptsInterval between attempts
1-10approx. 1 minute
11-15approx. 5 minutes
16-20approx. 30 minutes
21-30approx. 1 hour
Only the current state is sent

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.


Flow events

flow_submerchant:status_changed

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
  }
}
FieldDescription
statusSubmerchant account status at SimPay (e.g. client_new, client_verify, client_active, client_block)
aml_levelAML verification level (2 - full verification)
aml_statusAML verification status (e.g. sent, accepted, declined)
fully_activetrue when the submerchant is fully active and can accept payments
Tip

To decide whether the submerchant can accept payments, use the current.fully_active field. The other fields are informational.

flow_submerchant_payment_channel:status_changed

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
  }
}
FieldDescription
channel.slugPayment channel ID (same as id in GET .../payment-channels)
statusChannel status: new, in_progress, active, rejected
blik_one_click_statusBLIK OneClick status (only blik-level0): PENDING_REGISTRATION, REGISTERED_WAITING_ACTIVATION, ACTIVE, REJECTED or null
blik_recurrent_a / _m / _oWhether the BLIK Recurring Payments model AUTO / MANUAL / OPEN is active (only blik-recurrent) or null

flow_submerchant_withdraw:status_changed

The status of a submerchant withdrawal has changed. Example data - see Notification structure.

FieldDescription
id / short_idWithdrawal ID (UUID) and withdrawal short ID
amountWithdrawal amount
wallet.id / wallet.currencyWallet the withdrawal is made from and its currency
status.previous / status.currentPrevious and current withdrawal status: new, accepted, waiting, done, rejected

flow:test

Test notification sent from the Partner Panel. data contains only a random nonce field:

"data": {
  "nonce": "01K6FZ8X3Q9V2N7M4T5R1W0YAB"
}

Signature verification

Every Flow webhook contains the signature field - a SHA-256 hash (lowercase hex). The rule is the same as for payment notifications:

  1. take all values from the notification (without the signature field) in the order they appear in the JSON, flattening nested objects,
  2. join them with the | separator,
  3. append the Flow webhook key at the end,
  4. compute SHA-256 and compare it with signature using a timing-safe comparison function.
type|webhook_id|attempt|flow_service.id|flow_service.name|date|data_value1|...|data_valueN|FLOW_WEBHOOK_KEY
Booleans, null and numbers

Unlike 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, 100 for 100.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_64

SHA-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';

Payment webhooks (IPN) in SimPay Flow

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.

URL and key

SituationURLSignature key
The submerchant doesn't have a custom IPN URLParent service IPN URLParent service IPN key
The submerchant has a custom IPN URLSubmerchant's custom URLSubmerchant'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.

service_id

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.

The data.flow object

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.

FieldDescription
flow.submerchant_idSubmerchant ID (flsm_...)
flow.submerchant_custom_idSubmerchant customId
flow.flow_service_idFlow 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"
}

Changes in transaction:status_changed

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 signature covers the new fields

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.


Custom URLs for a submerchant

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": "..."
  }
}
Save the keys from the response

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.

New 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.