Skip to main content

Webhooks

The platform posts a plain JSON body to your webhookUrl when an order reaches a final state, signed with the platform Ed25519 key. The SDK verifies the request and returns the parsed event; you never handle the headers yourself. The wire format is documented under API Reference, Payment webhook and Payout webhook.

Hand the SDK the exact bytes received. Any framework that parses JSON before your handler runs changes the bytes and breaks the digest. Disable body parsing for the webhook route or read the raw stream.

Two methods​

Verify onlyVerify and parse
GoVerifyWebhook(r) ([]byte, error)ParsePaymentWebhook(r), ParsePayoutWebhook(r)
JavaScriptverifyWebhook({ method, path, headers, body, rawQuery })parsePaymentWebhook(...), parsePayoutWebhook(...)
Pythonverify_webhook(method=, path=, headers=, body=, raw_query=)parse_payment_webhook(...), parse_payout_webhook(...)
PHPverifyWebhook($method, $path, $headers, $body, $rawQuery = '')parsePaymentWebhook(...), parsePayoutWebhook(...)
JavaverifyWebhook(method, path, headers, body, rawQuery)parsePaymentWebhook(...), parsePayoutWebhook(...)

Verify returns the verified raw body. Parse additionally decodes it, checks the five required fields and enforces orderType: a payout event handed to the payment parser is rejected, so a mis-routed webhook cannot update the wrong table. Use the parse methods unless you route events yourself.

Every check runs in this order: request shape, Content-Digest over the body, Webhook-Event-Id equals the body eventId, created/expires window, platform key selected by keyid, Ed25519 signature. Any failure is a webhook error; respond with 4xx and do not process the body.

Handlers​

func paymentWebhook(w http.ResponseWriter, r *http.Request) {
wh, err := c.ParsePaymentWebhook(r) // reads and verifies r.Body itself
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if seen(wh.EventID) { // at-least-once delivery
w.WriteHeader(http.StatusOK)
return
}
switch wh.Status {
case deepayment.StatusSucceeded:
credit(wh.MerchantOrderNo, wh.PaidAmount)
case deepayment.StatusFailed:
fail(wh.MerchantOrderNo, wh.Failure.Msg)
}
w.WriteHeader(http.StatusOK)
}

The event​

FieldPaymentPayoutMeaning
eventIdyesyesUnique per event, stable across retries. Your deduplication key.
orderTypePAYMENTPAYOUTEnforced by the parse methods.
orderNo, merchantOrderNoyesyesBoth numbers.
statusyesyesFinal state only: SUCCEEDED, FAILED, and for payments EXPIRED, CANCELED.
currency, amountyesyesDecimal strings.
paidAmountyesnoWhat settled.
channelTradeNooptionaloptionalChannel-side reference.
failurewhen failedwhen failedcode, msg, message.
attachechoedechoed

Failures​

When status is FAILED, branch on failure.msg; failure.message is free text for logs and support.

failure.msgMeaningDo
ORDER_REJECTEDRisk control or business rule refused the orderDo not retry the same request
CHANNEL_ERRORThe upstream channel failedThe order is final; create a new one if the business allows
INSUFFICIENT_BALANCEPayout could not be fundedTop up, then create a new payout

Rules​

  • Respond 2xx only after the event is durably stored. Anything else triggers a retry. A retry is re-signed with fresh timestamps and nonce; the body and eventId stay the same.
  • Deduplicate by eventId. Delivery is at least once.
  • Make state updates idempotent by merchantOrderNo. Webhooks for different orders can arrive in any order.
  • When in doubt, query. A webhook that conflicts with your local state, or arrives late, is settled by querying the order.
  • Keep both webhook keys during a rotation. See Configuration.