Orange DOCS
Webhooks

Get told when a payment moves

Instead of polling, hand Orange a URL and we'll POST a JSON event to it every time a payment changes status — createddepositedsuccess. Confirm it's really us, update your records, respond 200.

Status changes
on Orange's side
📨
JSON POST
to your webhookUrl
Check secret & 200
update your DB
POST /payments

Register a webhook

There's no separate webhook resource — you attach webhookUrl and webhookSecret when you create the payment. Each payment can have its own endpoint.

Fields
webhookUrloptional
string
HTTPS endpoint that receives the event POSTs
webhookSecretoptional
string
A secret string you choose. It is echoed back in every event so you can confirm the call is from Orange.
POST /payments
{
  "merchantId": "9f430126-99da-42e8-afb1-4cab619cda5a",
  "amount": 100,
  "reference": "INV12345",
  "webhookUrl": "https://your-server.com/webhooks/orange",
  "webhookSecret": "a-random-secret-you-choose"
}
POST your webhookUrl

The event payload

Each event is a flat JSON object — no envelope. It carries the current status, your echoed webhookSecret, an integrity hash, and the full payment object. It's sent with a single header, Content-Type: application/json — there are no signature headers.

Body fields
paymentId
string
The payment this event is about
status
string
Current payment status (the event identity)
log
string
Latest human-readable status line
webhookSecret
string
The secret you set — echoed back
receivedAmount
number
Amount received so far
sentAmount
number
Amount paid out so far
date
string
Server timestamp (local format, not ISO-8601)
hash
string
SHA256 integrity digest (see Verify events)
payment
object
Full payment object, same shape as GET /payments/:id
event
{
  "paymentId": "eafdea43-63a7-4f61-b8ae-823c1b3aa236",
  "status": "success",
  "log": "payment has been completed successfully",
  "webhookSecret": "a-random-secret-you-choose",
  "receivedAmount": 100,
  "sentAmount": 102.28,
  "date": "2024-09-27 21:01:50",
  "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "payment": {
    "id": "eafdea43-63a7-4f61-b8ae-823c1b3aa236",
    "merchantId": "9f430126-99da-42e8-afb1-4cab619cda5a",
    "amount": 100,
    "amountSettled": 102.28,
    "status": "success",
    "destinationCurrency": "ZAR",
    "reference": "INV12345",
    "type": "PURCHASE"
  }
}
POST authenticity check

Verify events

There's no HMAC signature header. The reliable check is the echoed webhookSecret: compare the webhookSecret in the body to the one you set on that payment, and reject anything that doesn't match.

A hash field is also included — a plain SHA256 (hex) of paymentId + status + log + webhookSecret + receivedAmount + sentAmount + date + payment. Because the trailing payment segment is a fixed server-side marker (not the payment JSON) and date is a server-local timestamp string, the secret comparison above is the dependable test — treat the hash as a best-effort tamper check.

⚠️
Keep your webhookSecret private and use a unique, hard-to-guess value. Anyone who knows it could forge an event, since it travels in the payload.
verify.js
// 1) Primary check — the secret you set is echoed back in the body.
function isAuthentic(body, expectedSecret) {
  return body.webhookSecret === expectedSecret;
}

// 2) Optional integrity check — recompute the SHA256 hash.
//    NOTE: the trailing "payment" segment is a fixed server-side marker
//    (not the payment JSON), and "date" is the server's local timestamp
//    string, so treat this as best-effort and rely on the secret above.
const crypto = require('crypto');

function hashMatches(body) {
  const PAYMENT_MARKER = 'OrangeHelpers.Models.Orange.Payment';
  const data =
    body.paymentId + body.status + body.log + body.webhookSecret +
    body.receivedAmount + body.sentAmount + body.date + PAYMENT_MARKER;
  const expected = crypto.createHash('sha256').update(data, 'utf8').digest('hex');
  return expected === body.hash;
}

Retries & best practice

We retry on failure

Anything other than a 2xx (or a timeout — we wait ~30s per attempt) is retried up to 3 times with exponential backoff (~1s, 2s, 4s). Return 200 fast, do work async.

🔁

Be idempotent

The same event may arrive more than once. Key on paymentId + status so duplicates are harmless.

🔐

Always check the secret

Compare the body's webhookSecret to the one you set before touching your database. Drop anything that doesn't match.

📋

Reconcile as backup

Webhooks are best-effort. Periodically poll payments to catch anything your endpoint missed during downtime.