Skip to main content
PayGrid delivers two JSON payload shapes depending on which URL received the notification. Both are signed identically — see Verify signatures. For payment status definitions and the lifecycle diagram, see Track payments.

Payment webhook events

Webhooks fire on every status transition — not at creation time. There is no payment.pending webhook: pending is the initial state, so the first webhook is the first transition (usually a terminal status). The event field is {transaction_type}.{new_status}:
Eventdata.statusTypical data.previous_statusWhen it fires
payment.processingprocessingpendingRare — not a standard step for mobile payments
payment.completedcompletedpendingPayment captured successfully
payment.failedfailedpendingPayment was declined or rejected
payment.cancelledcancelledpendingPayment cancelled
payment.expiredexpiredpendingPayment not completed within ~30 minutes
No webhook is emitted for refund statuses (partially_refunded, refunded). Don’t wait for payment.partially_refunded or payment.refunded events.
Deduplicate with event_id — one per status transition. Retries reuse the same event_id (only X-MeetPay-Delivery-ID changes per attempt).

Payout webhook events

Payouts use merchant default URLs (captured at payout creation — not overridable per payout). Events follow the same {type}.{status} pattern. As with payments, there is no webhook at payout creation (pending is the initial state).
EventWhen it fires
payout.processingProvider accepted the payout and it is in flight
payout.completedFunds delivered to the recipient
payout.failedDisbursement failed
Callbacks fire once when a payout reaches a terminal status (completed, failed, and other terminal states such as voided where applicable).

Test event

Dashboard Test webhook sends:
EventPurpose
webhook.testSignature and connectivity check — transaction_id is all zeros; data.status is "test"

Webhook payload

Sent to webhook_url on each status change:
{
  "version": "1.0",
  "event": "payment.completed",
  "event_id": "d4f8a1b2-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
  "transaction_id": "f5d238bd-f8ab-4379-9832-0f1ce6d65cbe",
  "merchant_reference": "ORDER_123",
  "data": {
    "amount": 5000,
    "currency": "TZS",
    "status": "completed",
    "previous_status": "pending",
    "payment_type": "mobile",
    "provider_reference": "EXT-20250101-001",
    "completed_at": "2026-06-09T12:54:05Z"
  },
  "metadata": {
    "order_id": "ORD-9876"
  },
  "timestamp": "2026-06-09T12:54:06Z"
}
Optional fields (failure_reason, completed_at, metadata) are omitted when empty — they are never sent as null.

Webhook fields

FieldTypeDescription
versionstringPayload schema version ("1.0")
eventstringEvent name, e.g. payment.completed
event_idUUIDUnique per event (status transition); stable across retries — use for deduplication
transaction_idUUIDPayGrid payment ID (same as GET /api/v1/payments/{id})
merchant_referencestringYour reference from payment creation (always present on webhooks; may be an empty string)
data.amountnumberOriginal payment amount
data.currencystringCurrency code (TZS, USD, KES, UGX)
data.statusstringNew status after this transition
data.previous_statusstringStatus before this transition
data.payment_typestringmobile, card, or dynamic-qr
data.provider_referencestringUpstream payment reference — quote it in support requests
data.failure_reasonstringOnly on payment.failed when a reason is available; omitted otherwise
data.completed_atdatetimePresent once the payment reaches a terminal status; omitted otherwise
metadataobjectYour original metadata echoed back; omitted when empty
timestampdatetimeWhen PayGrid generated this notification

Example: failure event

{
  "version": "1.0",
  "event": "payment.failed",
  "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transaction_id": "f5d238bd-f8ab-4379-9832-0f1ce6d65cbe",
  "merchant_reference": "ORDER_123",
  "data": {
    "amount": 5000,
    "currency": "TZS",
    "status": "failed",
    "previous_status": "pending",
    "payment_type": "mobile",
    "provider_reference": "EXT-20250101-001",
    "failure_reason": "Customer declined USSD prompt",
    "completed_at": "2026-06-09T12:54:11Z"
  },
  "timestamp": "2026-06-09T12:54:12Z"
}

Callback payload

Optional channel — only sent if you configured callback_url. Most integrations use webhook_url only and never receive callbacks. Sent to callback_url once when the payment reaches a terminal status. Simpler shape — no event or previous_status:
{
  "version": "1.0",
  "type": "transaction.callback",
  "transaction_id": "f5d238bd-f8ab-4379-9832-0f1ce6d65cbe",
  "merchant_reference": "ORDER_123",
  "amount": 5000,
  "currency": "TZS",
  "status": "completed",
  "payment_type": "mobile",
  "provider_reference": "EXT-20250101-001",
  "completed_at": "2026-06-09T12:54:05Z",
  "metadata": {
    "order_id": "ORD-9876"
  },
  "timestamp": "2026-06-09T12:54:06Z"
}
As with webhooks, failure_reason (failed only), completed_at, and metadata are omitted when empty; in callbacks merchant_reference is also omitted when empty. Deduplicate callbacks with transaction_id (one callback per payment).

Webhook vs callback

AspectWebhookCallback
Fires onEvery status changeTerminal states only
FrequencyMultiple times per transactionOnce per transaction
Deduplication keyevent_idtransaction_id
Includes previous_statusYesNo
Event fieldevent (e.g. payment.completed)type: "transaction.callback"
See Handle events for processing guidance. Timestamps in payloads (timestamp, completed_at) are ISO 8601 UTC strings from Go’s JSON encoder (e.g. "2026-06-09T12:54:06Z"). Back to start: Overview