Skip to main content
Endl pushes signed events to your endpoint as things happen. This guide covers the delivery contract that applies to every event — headers, envelope, signature verification and replay protection — then the account events specifically.

How delivery works

You register an endpoint and receive a signing secret. Endl then POSTs each event to that endpoint as a compact JSON body, signed so you can prove it came from us.

Signed

Every PUSH delivery carries an HMAC-SHA512 signature over the raw body. Verify it before you act on anything.

At least once

Retries and manual re-sends can deliver the same event more than once. Deduplicate on the event id.

After the fact

Events are emitted once the underlying operation has committed, so an event never describes something that did not happen.
Always verify before acting. An unverified request is just a request from the internet. Verify the signature first, then dedupe on the event id, then act.

Setting up a webhook

Register a subscription for the events you care about. Endl then either pushes each matching event to your endpoint, or holds it for you to poll.

Choose a delivery mode

Those are the only two. SQS and SSE are reserved but not available — asking for either returns 422.
1

Create the subscription

Register your receiver URL and the event types you want.
POST /api/v0/webhooks/subscriptions
2

Store the signing secret

The response carries webhook_secret once. Store it before you do anything else — it cannot be read back, and there is no rotate endpoint. If you lose it, delete the subscription and create a new one.
201 Created
3

Receive and verify

Endl POSTs each matching event with an X-WEBHOOK-SIGNATURE header. Verify it against the raw request body before trusting anything in the payload.
4

Inspect deliveries

Every attempt is recorded, with the response your endpoint returned. List them, drill into one, or retry the ones that failed.

Request fields

enum
default:"PUSH"
PUSH or PULL.
string
required
Required for PUSH. The HTTPS endpoint Endl signs and POSTs every matching event to. Must be absent for PULL. Maximum 512 characters. Stored in canonical form — read data.target back rather than assuming your spelling was kept.
string[]
default:"[\"*\"]"
Which event types to receive. At most 50 entries, each non-blank and at most 128 characters. An entry is *, an exact type, or one trailing wildcard such as account.*. Patterns are trimmed, lower-cased, and underscores folded to dots — so ACCOUNT_OPENED and account.opened mean the same thing.
string
default:"2026-09.1"
The payload shape every delivery is rendered in. Fixed at creation and cannot be changed afterwards.

Response fields

PULL subscriptions. Omit target and set deliveryMode: "PULL". A PULL subscription never delivers anything — it authorises and configures the event feed. You may have one live PULL subscription, and it needs the events permission rather than webhooks. A PULL create returns neither webhook_secret nor secretHint, because a PULL feed is never signed.

Validation order

Validation runs in a fixed order, which is worth building your error handling against. The first failing check is the one you get back, so fixing errors in this order converges fastest.
  1. Delivery-mode availability
  2. target presence
  3. The active-subscription cap
  4. Egress and target policy — this performs a DNS lookup
  5. Duplicate-target and one-live-PULL conflicts
  6. eventFilter bounds
  7. schemaVersion

Conventions on webhook endpoints

Webhook endpoints use the response envelope — accounts do not. Subscription and delivery endpoints answer with { code, message, status, data, errors }, so the subscription is under data. This is different from the Accounts endpoints, where a success is the bare object. If you share response-handling code across both, branch on the path.

Delivery headers

Three things to get right. It is SHA-512, not SHA-256. The hex is lowercase. And you must HMAC the raw received body bytes — parsing the JSON and re-serialising it changes those bytes and breaks the signature.

The envelope

The top level is always the same six keys, frozen across schema versions. Only data is version-specific.

How data is rendered

data.id is prefixed — txn_, acct_, cus_ — and that prefix is the only thing that says what the event is about. data.customerId is omitted entirely when the event carries no customer. No subscription id is sent.
Treat the catalogue as the current shape, not a closed schema. Because there is no allowlist, new keys can appear inside data without a version change. Handle an unrecognised eventType — or an extra field inside data — gracefully rather than rejecting the delivery.

Example delivery

The body is a single compact line, and the signature covers exactly those bytes.
POST /webhooks/endl

Verifying signatures

Compute HMAC-SHA512(secret, timestamp + "." + rawBody), hex-encode it lowercase, and compare it to X-WEBHOOK-SIGNATURE in constant time. Accept a list of secrets from the start — that is what makes a secret cutover safe.

Replay protection

The timestamp is covered by the signature, so you can trust it. Reject any delivery whose X-WEBHOOK-TIMESTAMP is more than about five minutes old. A captured request cannot be replayed with a fresh timestamp, because rewriting it invalidates the signature. Then deduplicate on X-WEBHOOK-EVENT-ID. Retries and manual sends can deliver the same event more than once, and a receiver that is idempotent on the event id handles all of it for free.

Secrets and cutovers

There is no rotate endpoint. The signing secret is returned once, when the subscription is created, and cannot be read back afterwards. If it is lost, delete the subscription and create a new one — that mints a fresh secret bound to a new subscription id, and leaves the old one’s delivery history intact. Each subscription has its own secret, so moving traffic to a new receiver means both secrets are briefly live while the old subscription drains. Write your verifier against a list of secrets from the start, as the examples above do, and drop the retired one once nothing signed under it is still in flight.
secretHint is a masked form of the current secret — whsec_… plus its last four characters. It exists so a support ticket can answer “which secret are you signing with?” without either side handling the secret itself. Comparing hints is safe to paste into a ticket. Comparing secrets is not.
PULL feeds are never signed. A PULL subscription has neither a webhook secret nor a secretHint. Signature verification applies to PUSH deliveries only.

Event catalogue

Account events. These are the two the accounts surface produces — reading an account never emits anything. Other families exist and are documented with their own objects: deposit.created, deposit.received, deposit.failed for money arriving; transaction.initiated, transaction.updated, transaction.completed, transaction.failed, transaction.refunded for movements; and wallet.created, wallet.deleted for wallets.
Money landing in an account is a deposit event, not an account event — subscribe to that family if you are watching for funds. The full list is in the event catalogue.

Account event payloads

Both account events carry the same three producer fields: category, currency and status, after the two metadata keys. Remember the envelope rules — every leaf is a string, and a null field is omitted rather than sent.

Payload fields

string
The account, as acct_….
string
The customer that owns it, as cus_….
string
FIAT or ONRAMP. Often absent — the event carries the stored value, and it is not always set. Get Account derives one even when the event omits it.
string
The account currency, lower-case ("gbp"). The REST API upper-cases it; this does not. Compare case-insensitively.
string
Endl’s internal account state. Not the value the API returns — see the warning below.
data.status is not the status the API returns, and can be stale.Different vocabulary. You will see activated and deactivated here where Get Account returns ACTIVE and DEACTIVATED, and values with no API equivalent at all. Do not compare data.status against the statuses documented for the REST API.It can be the pre-change value. Activate and deactivate are queued, and account.updated is emitted when the request is accepted, not when it lands. Treat the event as “something changed, go look” and call Get account for the real state. Never write data.status into your own records.

Coverage gaps

These matter more than the events themselves, because each one is a state change you will never be told about.
  • A queued account open never fires account.opened. If Open Account returned no id, the account did not exist yet, and nothing emits an event when it later appears. Poll GET /api/v0/accounts/{userId} for those.
  • Status changes Endl makes on its own emit nothing. If an account is closed, frozen or activated by the banking partner or by compliance rather than by your API call, no account.updated is sent. Re-read accounts you care about on a schedule.
  • Funding instructions becoming available emits nothing. An AED account whose IBAN is issued after opening produces no event. Poll Get Account.
Absence of an event is not proof that nothing happened. A delivery can be skipped without failing the underlying operation. Anything you must be certain of should be confirmed by reading the resource, not inferred from silence.

Common pitfalls

Almost always the raw body. If your framework parsed the JSON and you re-serialised it to verify, the bytes differ and the HMAC will never match. Capture the raw body on the webhook route. After that, check you are using SHA-512, lowercase hex, and signing timestamp + "." + body — not the body alone.
Each subscription has its own secret, and both are live while the old one drains. Verify against a list of secrets and keep the retired one until nothing signed under it is in flight.
Delivery is at-least-once by design. Deduplicate on X-WEBHOOK-EVENT-ID and make your handler idempotent.
Expected. The event carries the internal state name, the API returns the partner-facing one. Call Get Account rather than mapping the event value.
Null-valued fields are omitted rather than sent as null. Treat absence as “no value”, and never index into data assuming a fixed shape.
Check the coverage gaps above — queued account opens, provider-driven status changes and late funding instructions all emit nothing. Poll for those.
Webhook delivery, signing and replay rules apply to every event family. Account-specific behaviour is covered in the Accounts guide. The endpoints themselves are in the Webhooks API reference.