Webhooks

Ebda Store receives webhooks from payment and billing gateways. (Looking for events Ebda sends to your own server? See Merchant webhooks.) Verification always runs over the exact raw bytes the gateway sent, so the routes are mounted before the JSON body parser.

Subscription billing webhooks

POST /api/v1/webhooks/billing/:provider

Order of operations: verify the signature, de-duplicate by the provider's event id, then settle, all inside one database transaction.

ProviderHeaderScheme
kashierx-kashier-signatureHMAC-SHA256 (hex) keyed by the payment API key, over the fields named in the payload's data.signatureKeys
stub (tests only)x-stub-signaturehex(HMAC-SHA256(secret, rawBody))

For Kashier the payload must list the signed fields in data.signatureKeys, and that list must cover the required identifying fields, otherwise the event is refused. The signature comparison is constant time.

StatusMeaning
200Processed, or a replay (duplicate: true), or an ignored event
401 INVALID_SIGNATUREBad or missing signature. Nothing was parsed or stored
500Processing failed. The gateway should retry; nothing was marked as seen

Payment webhooks

POST /api/v1/webhooks/payment/:provider

Each merchant connects their own Paymob, Kashier or Fawry account, and each gateway signs its webhook differently. Every delivery must be signed; without a signature the API answers 401-class MISSING_SIGNATURE and records the attempt. The signature is then verified against the per-store secret the merchant saved:

ProviderWhere the signature isScheme
paymobthe hmac query parameterHMAC-SHA512 (hex) over 20 callback fields in a fixed order (amount, ids, success / pending / voided / refunded flags, ...)
kashierthe x-kashier-signature headerHMAC-SHA256 (hex) over the fields named in data.signatureKeys, which must cover the order, amount, currency and status
fawrymessageSignature in the bodySHA-256 over the notification fields with the merchant's secure key appended (a plain hash, not an HMAC)

The payment is matched to its store by the provider's own reference (an exact match through a narrow database lookup), and a verified delivery is recorded before it is acted on, so a replay is a no-op. A signature that does not verify never moves money.

StatusMeaning
200Accepted. Work is queued durably; a duplicate delivery answers 200 Webhook already processed so the gateway stops retrying
400 MISSING_TRANSACTION_IDThe payload has no transaction id we recognise, so it cannot be de-duplicated safely
400 INVALID_PROVIDERUnknown provider segment

Receiving your own events

Ebda Store does not yet send outbound webhooks to merchants. If you need order events, use the merchant API or ask support.

Implementing a verifier (your own endpoint)

If you build a service that receives gateway callbacks in the same style, the safe pattern is:

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, signatureHex, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest();
  const given = Buffer.from(signatureHex.trim().toLowerCase(), 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}