Skip to content
Test money only. BitMomo currently runs on test networks. Payments use testnet coins and no real money moves. Live keys are not available yet.

Webhooks

BitMomo sends an HTTPS POST to your endpoint when one of your payments changes status. Webhooks tell you that something happened; always confirm the payment with the API before acting on it.

In the dashboard, open Settings → Webhooks:

  1. Enter your endpoint. It must be a public HTTPS URL.
  2. Choose the events to receive. Most integrations need payment.completed and payment.needs_review.
  3. Save, then copy the signing secret. It is shown only once; if you lose it, rotate it to get a new one.

Using the API instead, a key with webhooks:write can call PUT /api/v1/gateway/webhooks/config with url, events and is_active. The first configuration returns secret once. GET /gateway/webhooks/config never returns the secret.

One configuration applies to your whole merchant account. Saving again replaces the URL and events.

Events are named payment.<status>, after the status the payment just entered, for example:

Event Sent when
payment.confirming The customer’s transaction is on chain and waiting for confirmations.
payment.completed The payment is confirmed and credited.
payment.needs_review The payment is held for a manual check.
payment.expired The quote expired without an accepted payment.
payment.cancelled The payment was cancelled.
payment.failed The payment failed.

You only receive the events your configuration subscribes to.

  • If you create a payment without callback_url, its events go to your active webhook configuration (if it subscribes to that event).
  • If you pass callback_url, it must exactly match your registered, active endpoint, otherwise payment creation fails with 400. Events for that payment then go only to that endpoint.
  • With no webhook configuration, no events are sent. Your integration can still poll the API.
POST /webhooks/bitmomo HTTP/1.1
Content-Type: application/json
Idempotency-Key: 6b0d4a3e-2f5a-4c4e-9a43-9f7c1e2d8b10
X-Webhook-ID: 6b0d4a3e-2f5a-4c4e-9a43-9f7c1e2d8b10
X-Webhook-Event: payment.completed
X-Webhook-Attempt: 1
X-Webhook-Signature: 3f2a…(64 hex characters)
X-Webhook-Timestamp: 2026-11-15T10:04:05.123456+00:00
X-Webhook-Key-Version: 1
X-Webhook-Sequence: 42
X-BitMomo-Environment: test
{
"event": "payment.completed",
"payment_id": "1b113181-14c5-4cf7-97fc-64a180f2eda3",
"status": "completed",
"environment": "test",
"order_id": "order_1001",
"tx_hash": "0x…",
"confirmations": 19,
"occurred_at": "2026-11-15T10:04:01.000000+00:00",
"_meta": {
"delivery_id": "6b0d4a3e-2f5a-4c4e-9a43-9f7c1e2d8b10",
"event": "payment.completed",
"attempt": 1,
"sequence": 42,
"key_version": 1,
"occurred_at": "2026-11-15T10:04:01.000000+00:00",
"sent_at": "2026-11-15T10:04:05.123456+00:00"
}
}
_meta field Meaning
delivery_id Stable ID for this event delivery. Retries reuse it. Also in Idempotency-Key and X-Webhook-ID.
attempt 1 for the first send, then 2, 3… on retries.
occurred_at When the event happened. Never changes between retries.
sent_at When this attempt was signed. Fresh on every attempt; equals X-Webhook-Timestamp.
sequence Increasing delivery number for your endpoint. Deliveries can still arrive out of order.
key_version Version of the secret that signed this request; equals X-Webhook-Key-Version.

X-Webhook-Signature is the lowercase hex HMAC-SHA256 of the exact raw request body, keyed with your signing secret. Verify before parsing or trusting anything. Reject the request if:

  1. the signature does not match (compare in constant time);
  2. _meta.sent_at is more than 5 minutes from your clock (replay protection);
  3. X-Webhook-Timestamp or X-Webhook-Key-Version disagree with the signed _meta values.

The SDK does all three:

import { CryptoPOS } from '@bitmomo/merchant-sdk';
const verified = CryptoPOS.verifyWebhookEvent(rawBody, headers['x-webhook-signature'], {
secrets: [{ version: 1, secret: process.env.BITMOMO_WEBHOOK_SECRET! }],
timestamp: headers['x-webhook-timestamp'],
keyVersion: headers['x-webhook-key-version'],
});
if (!verified.valid) {
// verified.reason is e.g. 'bad-signature' or 'stale-timestamp'
return respond(400);
}
const { deliveryId, event, occurredAt } = verified.envelope;

Most frameworks parse JSON before your handler runs. Read the raw bytes instead: express.raw({ type: 'application/json' }) in Express, or Buffer.from(await request.arrayBuffer()) in a Next.js route handler.

Without the SDK, compute HMAC-SHA256(secret, rawBody) in hex and compare it with the header using a constant-time comparison, then apply the checks above.

  • Reply with any 2xx status within 10 seconds. Any other status, a timeout or a connection error counts as a failure.
  • Store the event durably before replying, keyed by delivery_id, and do the real work (fulfilment, emails) afterwards from your own queue.
  • If storing fails, reply with an error so BitMomo retries.

Failed deliveries are retried with exponential backoff, about 2, 4, 8 and 16 seconds apart, for up to 5 attempts in total. Retries reuse the same delivery_id and carry a fresh signature and sent_at.

The retry window is short, roughly half a minute. If your endpoint is down for longer, catch up by listing your payments when it comes back. Do not rely on webhooks alone.

Delivery is at least once. You will sometimes receive the same event twice, so deduplicate on delivery_id with a unique constraint in your database. The SDK’s createDeliveryDedupeCache() is an in-memory helper for simple cases; it does not survive restarts.

Rotate in Settings → Webhooks, or with POST /api/v1/gateway/webhooks/config/rotate (scope webhooks:write). Rotation returns a new secret with the next version number, shown once. The previous secret keeps working for 24 hours, so in-flight retries still verify.

During the changeover, give the verifier both secrets; the key version on each request picks the right one:

secrets: [
{ version: 1, secret: process.env.BITMOMO_WEBHOOK_SECRET_V1! },
{ version: 2, secret: process.env.BITMOMO_WEBHOOK_SECRET_V2! },
],

Rotate immediately if a secret may have leaked. Never log secrets or full signatures.