IdealBoxPay

Documentation

Take your first payment.

Everything below is the real contract: the routes, the statuses, the failure codes, and the rules a correct integration follows.

Event catalogue

Generated from the same module the emitter is typed against, so it cannot list an event nothing sends. Your dashboard serves the same list, with a sample payload for each.

typeWhen it firesState
payment_intent.createdA payment was created and is waiting to be confirmed.
payment_intent.processingThe payment was routed to a provider and is under way.
payment_intent.requires_actionThe customer has to approve a prompt, enter a code or finish a redirect.
payment_intent.succeededThe funds are confirmed. Fulfil the order here.
payment_intent.failedTerminal failure, with a canonical code saying why.
payment_intent.canceledCancelled before it completed.
payment_intent.expiredThe customer never completed it in time.
charge.succeededOne attempt took the money.
charge.failedOne attempt was declined. The payment may still succeed elsewhere.
charge.refundedA reversal settled.Not yet emitted
disbursement.succeededA payout to a third party settled.Not yet emitted
payout.paidA withdrawal to your own account settled.Not yet emitted
balance.updatedYour available balance changed.Not yet emitted
dispute.openedA payer’s bank opened a chargeback.Not yet emitted

The payload

One envelope for every event: an id to deduplicate on, a type to branch on, the environment, and the object itself under data.object. Amounts are decimal strings and dates are epoch milliseconds, exactly as everywhere else.

{
  "id": "wd_01JAY7BQ2M4X8ND6VKQ0R3PZ5W",
  "object": "event",
  "type": "payment_intent.succeeded",
  "created": 1785869000,
  "environment": "TEST",
  "data": {
    "object": {
      "id": "pi_01JAY2K5RQ9M3W7ZB8XF4TC6VD",
      "object": "payment_intent",
      "status": "SUCCEEDED",
      "amount": "500000",
      "amountCaptured": "500000",
      "currency": "XAF",
      "country": "CM",
      "methodType": "MOBILE_MONEY",
      "provider": "SIMULATOR",
      "flowKind": "DEVICE_PUSH",
      "reference": "order-4471",
      "created": 1785869000000
    }
  }
}

Verifying the signature

The IdealBox-Signature header carries a timestamp and an HMAC over "{timestamp}.{raw body}". Verify it before you trust a single field.

# IdealBox-Signature: t=1785869000,v1=5257a869e7…
#
# signed_payload = "{t}.{raw_body}"
# expected       = HMAC-SHA256(endpoint_secret, signed_payload)
#
# Compare in constant time, and reject anything where |now - t| is more than
# five minutes. Without the timestamp in the signed string, a captured request
# would verify forever.
#
# During a secret rotation BOTH the new and the previous secret sign, so you
# can deploy the new one without a synchronised cutover.

Four rules that are not optional

Every integration that has ever gone wrong here went wrong on one of these.

  • Fulfil on payment_intent.succeeded from the webhook, never on the HTTP response to confirm. A device-push payment is not complete when that call returns.
  • Expect duplicates. Delivery is at-least-once — deduplicate on the envelope id.
  • Expect events out of order. Compare against your own stored state rather than assuming a sequence.
  • Verify the signature against the raw bytes. A parsed and re-serialised body will not match the HMAC, and that mistake is invisible until production.
  • Answer 2xx quickly and do your work afterwards. Anything else is counted as a failure, and twenty consecutive failures disables the endpoint.

Replaying an event

Any past event can be sent again from Developers → Events, either to every subscribed endpoint or to one that missed it.

{
  "id": "wd_01JB0C4XN2Q8T5R7YH3MF6PW9K",
  "object": "event",
  "type": "payment_intent.succeeded",
  "created": 1785872600,
  "environment": "TEST",
  "replay": {
    "of": "wd_01JAY7BQ2M4X8ND6VKQ0R3PZ5W",
    "requestedAt": 1785872600000
  },
  "data": { "object": { "…": "unchanged" } }
}