Webhooks
Speend sends a POST to url_callback when a status changes. Which status changes actually trigger a webhook differs between payments and payouts — see below.
Where the delivery address comes from
Which address wins — the one from the merchant dashboard or the one from the request body — depends on the method:
| Method | Priority |
|---|---|
| Static wallet (createStaticWallet) | The address from the merchant dashboard — url_callback from the request is ignored |
| Invoice (createPayment) | The address from the request wins over the one from the merchant dashboard |
| Payout (payout/create) | The address from the merchant dashboard — the same as for a static wallet |
Webhook — invoice and static wallet
It is sent on many meaningful transitions, intermediate statuses included (not just the final ones) — on the move to check, on a partial underpayment, on an AML release and so on.
{
"type": 2,
"uuid": "019f69ef-f130-7064-b822-09b39a40b99c",
"amount": "2",
"paid_amount": "6.170554",
"paid_amount_usd": "2.00210147",
"merchant_amount": "6.07799569",
"commission_percent": "1.5",
"commission_fixed": "0",
"commission": "0.09255431",
"is_final": true,
"status": "paid",
"network": "TRON",
"currency": "TRX",
"txid": "36540b5842d27b23da70415439281e2...",
"from": "TQn9Y2*****LVLTiPKX",
"to": "TCEUYh*****bNhPMK6W",
"order_id": "1002",
"additional_data": null,
"sign": "91be60615455288c41ee177ab8b02b19"
}
A static wallet additionally carries the wallet_address_uuid and wallet_id fields. When auto-conversion is enabled for the merchant, a convert object is present as well (with the same structure as in payment/info, see the "Payment details" section).
type — what produced the notification (payments and static wallet)
| type | Source |
| 1 | Static wallet |
| 2 | Invoice (createPayment) |
Webhook — payouts (Payout)
Unlike payments, a payout webhook is not sent on every status change but only for a limited set of statuses — see the table below. Intermediate technical statuses never leave the system.
{
"uuid": "019f89aa-bfe9-7308-8e40-fb5b1345a5a4",
"type": 1,
"status": "success",
"is_final": true,
"amount": "7",
"commission": "0",
"network": "SOL",
"currency": "USDT",
"address": "8Q3zsm*****tVUzasKQ",
"txid": "DgR9Tg*****AFe5nA9j6zJ6",
"order_id": "13",
"description": null,
"comment": null,
"created_at": "2026-07-22T11:51:36+00:00",
"updated_at": "2026-07-22T11:52:04+00:00",
"sign": "c8980a53111b04abea70c2371ff2fca7"
}
type — how the payout was created (Payout only)
| type | How the payout was created |
| 1 | Through the public API (payout/create) |
| 2 | By hand in the merchant dashboard |
| 3 | By a Speend administrator |
The statuses a webhook is sent for (Payout)
| Status in the API (getStatus) | Status in the webhook |
|---|---|
| ON_QUEUE | process |
| SUCCESS | success |
| CANCELED | cancel |
| FAILED | fail |
| AML_FAILED | aml_fail |
The webhook arrives for these 5 statuses only, no matter how the payout was created. REQUESTED, AWAITING_CONFIRMATION and the internal stages (ON_QUEUE_CONVERSION, ON_NETWORK_QUEUE) are never sent out.
The sign signature
Every webhook carries a sign field.
The signature lets you make sure the webhook really came from Speend and was not forged by a third party. Verifying it is mandatory before you trust the status from a webhook.
type — what produced the notification (payments and static wallet)
// webhook_password is optional
signKey = api_key + webhook_password
// the body — without the sign field, keys in the order of the original JSON
sign = MD5(
base64( json_encode(body, JSON_UNESCAPED_UNICODE) ) . signKey
)
Which key to use
The key for the formula depends on the event, not directly on the type field. Payout and Static wallet share the same type value (1), but the keys differ:
| Event | Key (api_key in the formula) |
|---|---|
| Payout (sending funds out) | The payout key (api_key_withdraw) |
| Static wallet (top-up) | The main key (api_key_main) |
| Invoice (createPayment) | The main key (api_key_main) |