Overview
A Feddi event reports a domain change that already committed: a wallet balance moved, a top-up settled, a promotional grant accrued. Each event carries a stableevent_id you dedup on and a signature you verify before you trust the body.
Webhooks are server-to-server deliveries: Feddi makes an HTTP POST to your endpoint with the event envelope in the body and the signature in a header. The envelope and signature contract is stable to build against now, even before outbound delivery is mounted.
This page is the contract you build your receiver against: the event catalog, the event envelope, and the signature-verification steps. All are stable. Build your verifier now, validate it against your own test vectors, and you are ready to receive deliveries the moment they are available to you.
Outbound delivery endpoints are not yet mounted. Until they ship, reconcile settled activity through
the transactions read API at Transactions. The envelope and signature
contract on this page is what you verify when delivery becomes available, so you can write the
receiver against it today.
The event envelope
Every event carries the same envelope. Process each event idempotently on itsevent_id: the same event can arrive more than once, so dedup before you act.
required
Globally unique. This is your idempotency key for processing. Store it, and treat a second arrival
of the same
event_id as a no-op.required
The domain change, for example
wallet.balance_changed, topup.confirmed, or
promo_grant.accrued. Branch your handler on this value.required
RFC3339 timestamp of when the underlying domain change happened.
required
RFC3339 timestamp of when Feddi emitted the event, always after the database transaction committed.
An event never precedes the change it describes.
required
The Feddi merchant tenant this event belongs to.
required
The event-type-specific payload. Read the fields your handler needs by
event_type.data always follows the same shape: a minor-units integer paired with an explicit ISO-4217 currency, for example credited_actual_minor: 5000 with currency: QAR.
Event catalog
These are the event types Feddi emits, grouped by domain, with when each one fires.Identity & enrollment
Money & wallet
Promotion & incentive
Lifecycle & reconciliation
Every payload follows the same envelope; branch your handler on
event_type and read the fields your handler needs.
Delivery model
How deliveries behave on the wire, so your receiver tolerates the realities of an at-least-once channel.- At-least-once delivery. The same event can arrive more than once. Dedup on
event_id. - Per-resource ordering. A monotonic
sequence_numberon the envelope orders events within a single resource. Do not assume global order across resources. - Retry with backoff. Any non-2xx response or timeout is retried with exponential backoff, then dead-lettered after retries are exhausted.
- Replay window. The signed timestamp enforces a replay window. Reject anything outside it before you read the body.
- Idempotent processing. Key your processing on
event_idso a redelivery is a no-op. - Endpoint auto-disable. Sustained delivery failure auto-disables the endpoint. Re-enable it from the dashboard.
Subscriptions
This is a forward contract, not yet mounted. When delivery ships, you will:- Register a delivery endpoint URL per integration.
- Receive a per-subscription signing secret.
- List and disable subscriptions.
- Rotate the signing secret.
Verify every delivery
Feddi signs each delivery with HMAC-SHA256 over a canonical base string, keyed by your subscription secret. Verify the signature and reject anything outside the replay window before you read the body. An unsigned or stale request is not a Feddi event. The signature travels in theFeddi-Signature header as t=<unix-timestamp>,v1=<hex-hmac>. The signing base string is the signed timestamp, a literal ., then the exact raw request body:
1
Read the header
Parse
Feddi-Signature. Take t (the signed Unix timestamp) and v1 (the expected hex
signature).2
Reject stale timestamps
If
t is outside your replay window (compare against your own clock), reject the request. This is
what stops a captured payload from being replayed against you later. We recommend a tolerance of
300 seconds or less.3
Recompute and compare
Build
t + "." + raw_body, compute HMAC-SHA256 with your stored secret, and compare to v1 using
a constant-time equality check. Sign the raw bytes you received, never a re-serialized object: any
whitespace or key-order change breaks the match.4
Process idempotently, then 2xx
On a match inside the window, dedup on
event_id, act, and return a 2xx. A non-2xx response (or
a timeout) signals failure to Feddi.Reconcile through the transactions read API
Until delivery endpoints are available, treat the transactions read API as your source of truth for what settled. Poll it on a schedule, or pull it on demand to confirm an order reconciled.- List with
GET /{integrationId}/transactions, cursor-paginated and filterable bystatus, a date window, andbranch_id. - Read one with
GET /{integrationId}/transactions/{transactionId}.
x-api-key. A mismatched {integrationId} resolves to 401 or 403, never another tenant’s rows.
See Transactions for the full projection fields each read returns.
Where to go next
Transactions
The settlement reads you reconcile against today, with their full projection fields.
Idempotency & errors
Dedup on event_id the same way you key mutating calls, and handle every typed error code.
Conventions
The response envelope, money-as-minor-units, and the meta block on every response.
Authentication
The x-api-key your reconciliation reads authenticate with.