Skip to main content

Webhooks

Webhooks send an HTTPS POST to your server whenever something changes, so you don't have to poll.

Set up​

In the dashboard, go to Settings → Webhooks → New and fill in:

  • Endpoint: an https:// URL on your server.
  • Challenge: a secret string that you choose. It is sent back in every webhook so you can check that the request came from IntaSend.
  • Events: the events to subscribe to.

Events​

topicFires whenPayload
collection_eventA payment (invoice) is created or changes stateInvoice
send_money_eventA payout batch is created or changes statePayout batch with its transactions
reversal_eventA chargeback or refund is created or changes statusChargeback
wallet_transfer_eventFunds move between your wallets (a transfer or a split)Both wallets and transactions
subscription_eventA subscription's status, completed cycles or failure reason changesSubscription with its payments

Payload format​

Every webhook is a POST with Content-Type: application/json. The body is the resource itself (the same fields the API returns), with two fields added at the top level:

FieldDescription
topicThe event type, for example collection_event
challengeThe challenge you set on the webhook. Compare it with your own before trusting the request
{
"invoice_id": "KQ9PLXR",
"state": "COMPLETE",
"...": "...other resource fields...",
"topic": "collection_event",
"challenge": "my-secret-challenge"
}
Data types in webhooks
  • Amounts are strings in webhooks, for example "value": "100.00", even where the REST API returns a number. Parse them as decimals, not floats.
  • Timestamps are ISO 8601 with the East Africa Time offset, for example "2026-09-30T10:15:42.519204+03:00". Dates without a time are YYYY-MM-DD.
  • Fields with no value are sent as null. They are never left out.

A resource can send several events as it moves through its states (for example PENDING → PROCESSING → COMPLETE). Events may arrive out of order, so use the state or status in the payload rather than the order they arrive in.


collection_event​

This event is sent when a payment (invoice) is created and every time its state changes. It covers every collection method: M-Pesa, mobile money, checkout, cards, payment links and bank. Invoices with a value of zero don't send events.

{
"invoice_id": "KQ9PLXR",
"state": "COMPLETE",
"provider": "M-PESA",
"charges": "3.00",
"net_amount": "97.00",
"currency": "KES",
"value": "100.00",
"account": "254712345678",
"api_ref": "order-123",
"provider_ref": "SJT8H2K9QW",
"clearing_status": "CLEARING",
"mpesa_reference": "SJT8H2K9QW",
"host": "102.215.33.10",
"card_info": {"bin_country": null, "card_type": null},
"splits": [],
"retry_count": 0,
"failed_reason": null,
"failed_code": null,
"failed_code_link": null,
"created_at": "2026-09-30T10:15:02.114203+03:00",
"updated_at": "2026-09-30T10:15:42.519204+03:00",
"topic": "collection_event",
"challenge": "my-secret-challenge"
}
FieldTypeDescription
invoice_idstringInvoice ID. Use it to check payment status
statestringPENDING, PROCESSING, RETRY, PARTIAL, COMPLETE, FAILED or CANCELED (meanings)
providerstringPayment method, for example M-PESA, INTASEND-XB-PUSH, CARD-PAYMENT, APPLE-PAY, GOOGLE-PAY, PESALINK, BITCOIN, COOP_B2B
valuestringAmount the customer was asked to pay
chargesstringIntaSend fees
net_amountstringvalue minus charges
currencystringInvoice currency
accountstringThe customer's phone number (mobile methods) or email (card and other methods)
api_refstring or nullThe reference you sent when creating the payment
provider_refstring or nullThe provider's reference, for example the M-Pesa receipt number
mpesa_referencestring or nullM-Pesa receipt number, for M-Pesa payments
clearing_statusstring or nullStatus of the funds in your wallet once paid: CLEARING, AVAILABLE or ON-HOLD
hoststring or nullWhere the payment was started: your site URL or the client IP
card_infoobjectbin_country and card_type for card payments, otherwise both null
splitsarraySplit results, once the payment has cleared. Otherwise []
retry_countintegerNumber of retries
failed_reasonstring or nullWhy the payment failed or was cancelled
failed_codestring or nullProvider failure code, for example an M-Pesa result code like 1032 (cancelled) or 1037 (phone unreachable)
failed_code_linknullReserved
created_at, updated_atstringTimestamps
Fulfil on COMPLETE

Only release goods when state is COMPLETE. Match the payment to your order with api_ref, and check that value and currency are what you expected.


send_money_event​

This event is sent when a payout batch is created and every time its state changes: waiting for approval, balance check, sending, completed, and so on. The payload is the full batch, with every transaction in it. Batches with a total of zero don't send events.

{
"file_id": "Y8Q2KLM",
"tracking_id": "8b0a3d0c-6f4e-4a8e-9a53-2f3c1b1f6e0d",
"batch_reference": "payroll-2026-09",
"status": "Completed",
"status_code": "BC100",
"transactions": [
{
"transaction_id": "KQ9PLXR",
"status": "Successful",
"status_code": "TS100",
"status_description": "The service request is processed successfully.",
"request_reference_id": "3f6c2a8e-1b7d-4f0a-9c2e-5d8b7a6f4e21",
"provider": "MPESA-B2C",
"bank_code": null,
"name": "Jane Doe",
"account": "254712345678",
"account_type": null,
"account_reference": null,
"notify_phone": null,
"provider_reference": "SJT8H2K9QW",
"provider_account_name": "JANE DOE",
"amount": "150.00",
"charge": "10.00",
"narrative": "Salary",
"file_id": "Y8Q2KLM",
"idempotency_key": "payout-4411",
"currency": "KES",
"created_at": "2026-09-30T09:00:01.100200+03:00",
"updated_at": "2026-09-30T09:00:19.402113+03:00"
}
],
"actual_charges": "10.00",
"paid_amount": "150.00",
"failed_amount": "0.00",
"wallet": {
"wallet_id": "XZ3LKQ",
"label": "default",
"can_disburse": true,
"currency": "KES",
"wallet_type": "SETTLEMENT",
"current_balance": "12340.00",
"available_balance": "12340.00",
"updated_at": "2026-09-30T09:00:19.402113+03:00"
},
"charge_estimate": "10.00",
"total_amount_estimate": "160.00",
"total_amount": "150.00",
"transactions_count": 1,
"created_at": "2026-09-30T09:00:01.100200+03:00",
"updated_at": "2026-09-30T09:00:19.402113+03:00",
"topic": "send_money_event",
"challenge": "my-secret-challenge"
}

Batch fields:

FieldTypeDescription
file_idstringBatch ID
tracking_idstring (UUID)The tracking ID returned by initiate. Use it with status
batch_referencestring or nullYour own reference for the batch
status, status_codestringBatch state, for example Completed / BC100 (all codes)
transactionsarrayEvery transaction in the batch (below)
total_amountstringSum of the transaction amounts
charge_estimate, total_amount_estimatestringEstimated fees, and the amount plus fees
actual_chargesstringFees actually charged
paid_amountstringAmount paid out successfully
failed_amountstringAmount that failed and is refunded to the wallet
transactions_countintegerNumber of transactions
walletobjectThe wallet the batch was paid from, with balances at the time of the event
created_at, updated_atstringTimestamps

Transaction fields:

FieldTypeDescription
transaction_idstringTransaction ID. Use it for OTP confirmation
status, status_codestringTransaction state, for example Successful / TS100 (all codes)
status_descriptionstringThe provider's result message, or a default description
request_reference_idstring (UUID)Unique reference for this transaction
providerstringPayout provider, for example MPESA-B2C or PESALINK
account, name, narrativestringAs sent in the request
bank_code, account_type, account_reference, notify_phonestring or nullAs sent in the request
provider_referencestring or nullThe provider's reference, for example the M-Pesa receipt
provider_account_namestring or nullRecipient name reported by the provider
amount, chargestringAmount sent and the fee for it
currencystringWallet currency
file_idstringThe batch this transaction belongs to
idempotency_keystring or nullYour idempotency key
created_at, updated_atstringTimestamps
note

A completed batch (BC100) can contain failed transactions. Check each transaction's status_code before marking a payout as paid.


reversal_event​

This event is sent when a chargeback or refund is created and every time its status changes.

{
"chargeback_id": "CB7X2QL",
"session_id": "5b9d6c1e-8a3f-4e2b-9d7c-1f0e2a3b4c5d",
"transaction": {
"transaction_id": "TR8LQ3M",
"invoice": {
"invoice_id": "KQ9PLXR",
"state": "COMPLETE",
"provider": "M-PESA",
"value": "100.00",
"currency": "KES",
"account": "254712345678",
"api_ref": "order-123",
"...": "...all invoice fields, as in collection_event..."
},
"currency": "KES",
"value": "97.00",
"running_balance": "12437.00",
"narrative": "Payment from 254712345678",
"trans_type": "SALE",
"status": "CHARGEBACK-PENDING",
"created_at": "2026-09-30T10:15:42.519204+03:00",
"updated_at": "2026-09-30T12:01:05.771020+03:00"
},
"amount": "100.00",
"reason": "Duplicate payment",
"status": "PENDING",
"resolution": null,
"staff_created": false,
"created_at": "2026-09-30T12:01:05.771020+03:00",
"updated_at": "2026-09-30T12:01:05.771020+03:00",
"topic": "reversal_event",
"challenge": "my-secret-challenge"
}
FieldTypeDescription
chargeback_idstringChargeback ID
session_idstring or nullPayment session of the original payment
transactionobjectThe original payment's wallet transaction, with the full invoice nested inside. Its status shows the effect on your funds, for example CHARGEBACK-PENDING or REFUNDED
amountstringAmount being refunded
reasonstringUnavailable service, Delayed delivery, Wrong service, Duplicate payment or Other
statusstringPENDING, PROCESSING, COMPLETED, DISPUTED, OVERDUE or CANCELLED
resolutionstring or nullHow it was resolved
staff_createdbooleantrue if it was raised by IntaSend staff rather than by you
created_at, updated_atstringTimestamps

wallet_transfer_event​

This event is sent when funds move between your own wallets: intra-wallet transfers and each executed split. Unlike the other events, the payload has a from_data / to_data structure:

{
"from_data": {
"wallet": {
"wallet_id": "XZ3LKQ",
"label": "default",
"can_disburse": true,
"currency": "KES",
"wallet_type": "SETTLEMENT",
"current_balance": "11840.00",
"available_balance": "11840.00",
"updated_at": "2026-09-30T13:20:11.004512+03:00"
},
"transaction": {
"transaction_id": "TR2PX9A",
"invoice": null,
"currency": "KES",
"value": "-500.00",
"running_balance": "11840.00",
"narrative": "Float top-up",
"trans_type": "PAYOUT",
"status": "AVAILABLE",
"created_at": "2026-09-30T13:20:11.004512+03:00",
"updated_at": "2026-09-30T13:20:11.004512+03:00"
}
},
"to_data": {
"wallet": {
"wallet_id": "WK7NB3R",
"label": "Nairobi branch",
"can_disburse": true,
"currency": "KES",
"wallet_type": "WORKING",
"current_balance": "2500.00",
"available_balance": "2500.00",
"updated_at": "2026-09-30T13:20:11.004512+03:00"
},
"transaction": {
"transaction_id": "TR2PX9B",
"invoice": null,
"currency": "KES",
"value": "500.00",
"running_balance": "2500.00",
"narrative": "Float top-up",
"trans_type": "PAYOUT",
"status": "AVAILABLE",
"created_at": "2026-09-30T13:20:11.004512+03:00",
"updated_at": "2026-09-30T13:20:11.004512+03:00"
}
},
"amount": "500.00",
"topic": "wallet_transfer_event",
"challenge": "my-secret-challenge"
}
FieldTypeDescription
from_data.walletobjectThe wallet the money left, with balances after the transfer
from_data.transactionobjectThe debit. Its value is negative
to_data.walletobjectThe wallet that received the money
to_data.transactionobjectThe credit. Its value is positive
amountstringAmount moved

trans_type tells the two cases apart:

  • PAYOUT: an intra-wallet transfer that you made. invoice is null.
  • SPLIT: an automatic split. The credit's invoice is the payment that was split.

subscription_event​

This event is sent when a subscription is created, and whenever its status, completed_cycles or fail_reason changes. That means activation, each successful billing cycle, failures and cancellation.

{
"subscription_id": "SB4KQ9M",
"reference": "sub-42",
"customer": {
"customer_id": "CU8XL2P",
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"reference": "user-42",
"address": null,
"city": null,
"state": null,
"zipcode": null,
"country": "KE",
"created_at": "2026-09-01T08:00:00.000000+03:00",
"updated_at": "2026-09-01T08:00:00.000000+03:00"
},
"plan": {
"name": "Pro monthly",
"plan_id": "PL3MNQ7",
"frequency": 1,
"frequency_unit": "M",
"billing_cycles": 12,
"currency": "KES",
"amount": "1000.00",
"reference": null,
"plan_url": "https://payment.intasend.com/subscriptions/charge/PL3MNQ7/plan/",
"redirect_url": null,
"created_at": "2026-09-01T08:00:00.000000+03:00",
"updated_at": "2026-09-01T08:00:00.000000+03:00"
},
"payment_method": "CARD-PAYMENT",
"card_mask": "****1111",
"card_type": "VISA",
"card_expiry": "2026-09-01",
"status": "ACTIVE",
"start_date": "2026-09-01",
"next_date": "2026-11-01",
"completed_cycles": 2,
"fail_reason": null,
"setup_url": "https://payment.intasend.com/subscriptions/charge/…",
"redirect_url": "https://example.com/done",
"payments": [
{
"transaction_id": "SP9QX1A",
"invoice": {"invoice_id": "RZ4MNQ2", "state": "COMPLETE", "value": "1000.00", "currency": "KES", "...": "...all invoice fields..."},
"created_at": "2026-10-01T08:00:04.221900+03:00",
"updated_at": "2026-10-01T08:00:09.874100+03:00"
}
],
"created_at": "2026-09-01T08:00:00.000000+03:00",
"updated_at": "2026-10-01T08:00:09.874100+03:00",
"topic": "subscription_event",
"challenge": "my-secret-challenge"
}
FieldTypeDescription
subscription_idstringSubscription ID
referencestring or nullYour reference
customerobjectThe customer (fields)
planobjectThe plan (fields)
statusstringPENDING, ACTIVE, CANCELED, COMPLETE or FAILED
payment_methodstring or nullCARD-PAYMENT
card_mask, card_typestring or nullThe saved card (last 4 digits and brand), once the customer has added it
card_expirystring or nullA date (YYYY-MM-DD) recorded when the card was saved
start_date, next_datestring or nullStart date and next billing date (YYYY-MM-DD)
completed_cyclesintegerBilling cycles paid so far
fail_reasonstring or nullWhy the last charge failed
setup_urlstringWhere the customer adds their card
paymentsarrayEvery charge so far, each with its full invoice
created_at, updated_atstringTimestamps

Payout callback_url​

The callback_url you can pass on send money is a separate, simpler mechanism. It is not a dashboard webhook:

Dashboard webhook (send_money_event)Per-request callback_url
BodyBatch payload, plus topic and challengeThe same batch payload, without topic or challenge
WhenEvery batch state changeOnly when the batch completes, fails on low balance, or is cancelled
Recorded and replayableYesNo

Because a callback_url request carries no challenge, confirm the result with /send-money/status/ before acting on it.

Handling webhooks​

  1. Verify the challenge. Reject any request whose challenge doesn't match yours.
  2. Respond with 200 or 201 quickly, and do heavy work in the background. Any other status counts as a failure.
  3. Be idempotent. You'll get one event per state change, and a replay sends the same event again. Key your handling on the resource ID (invoice_id, tracking_id, chargeback_id, subscription_id) together with its state.
  4. Confirm critical events. Before releasing goods or marking a payout as paid, look the resource up with the API, for example payment status.
import hmac, os
from flask import Flask, request

app = Flask(__name__)
CHALLENGE = os.environ["INTASEND_WEBHOOK_CHALLENGE"]


@app.post("/intasend/webhook")
def intasend_webhook():
event = request.get_json(force=True)
if not hmac.compare_digest(str(event.get("challenge", "")), CHALLENGE):
return "", 401

topic = event.get("topic")
if topic == "collection_event" and event["state"] == "COMPLETE":
fulfil_order(event["api_ref"], event["invoice_id"])
elif topic == "send_money_event":
for txn in event["transactions"]:
update_payout(txn["idempotency_key"], txn["status_code"])
elif topic == "reversal_event":
record_refund(event["chargeback_id"], event["status"])
elif topic == "subscription_event":
sync_subscription(event["subscription_id"], event["status"])

return "", 200

Failures and replay​

Failed deliveries are recorded under Webhooks → Events in the dashboard, and you can replay them from there. A replay sends the same payload again.

If your endpoint keeps failing, IntaSend emails you a warning and then disables the endpoint. Re-enable it in the dashboard once it's fixed.