How delivery works
You register an endpoint and receive a signing secret. Endl thenPOSTs 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.
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.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.- Delivery-mode availability
targetpresence- The active-subscription cap
- Egress and target policy — this performs a DNS lookup
- Duplicate-target and one-live-PULL conflicts
eventFilterboundsschemaVersion
Conventions on webhook endpoints
Delivery headers
The envelope
The top level is always the same six keys, frozen across schema versions. Onlydata 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.
Example delivery
The body is a single compact line, and the signature covers exactly those bytes.POST /webhooks/endl
Verifying signatures
ComputeHMAC-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 whoseX-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.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.
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 noid, the account did not exist yet, and nothing emits an event when it later appears. PollGET /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.updatedis 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.
Common pitfalls
Every signature fails to verify
Every signature fails to verify
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.Signatures started failing after I moved receivers
Signatures started failing after I moved receivers
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.
I am processing the same event twice
I am processing the same event twice
Delivery is at-least-once by design. Deduplicate on
X-WEBHOOK-EVENT-ID and
make your handler idempotent.data.status does not match any status in the API docs
data.status does not match any status in the API docs
Expected. The event carries the internal state name, the API returns the
partner-facing one. Call Get Account rather than mapping the event value.
A field I expected is missing from data
A field I expected is missing from data
Null-valued fields are omitted rather than sent as
null. Treat absence as “no
value”, and never index into data assuming a fixed shape.I am waiting on an event that never arrives
I am waiting on an event that never arrives
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.