Skip to main content

Events

Contract §6. Section numbers are contract-wide and never change; the contract overview maps every section to its page.

Conventions — authentication, the response envelope, idempotency, retry safety, pagination, rate limits — are defined in the Partner Integration Guide and apply to every endpoint here.

An event is a thin envelope carrying an allowlisted set of fields. The service resolves which of your clients it belongs to, stamps its own id, and pushes it once. Delivery, signature verification and the catch-up cursor are in Partner Guide §8; this section is the payload contract.

Event envelope

Captured from a live delivery, 2026-08-22:

{
"id": "evt_01M0MTVS6BMYM7YHGWNQRFBD33",
"type": "company.updated",
"occurredAt": "2026-08-22T13:34:15.000Z",
"subject": { "clientId": "cl_01M0MTVJNRW207WNMNHJ60Y0BA" },
"data": { "applicationStatus": "approved" }
}
  • id — dedup key, and the event_id in the signature input. Read it from the X-Oxen-Event-Id header and verify the HMAC over the raw bytes before parsing.
  • occurredAt — ISO-8601, when the service received the event.
  • subject — service public ids (clientId, cardholderId, cardId) so you can correlate without parsing data. Only the keys that apply are present. Processor identifiers never appear here or anywhere else.
  • data — see below. Not the processor's payload.
  • There is no sequence field in the envelope. The catch-up cursor is a sequence, but it is returned as nextCursor on the list response, not stamped on each event.

The data allowlist

Only the fields below are delivered. Default-deny: unlisted fields are dropped, and a type with no entry arrives as data: {} rather than with unreviewed processor content. Nested paths flatten to their leaf name (collateral.amountamount).

An event is a notification, not a record. For anything beyond these fields, read the resource through the REST API using the ids in subject.

Events delivered today

typedataMeaning
company.updatedapplicationStatusClient KYB outcome
user.updatedapplicationStatusCardholder KYC outcome. For the account-owner user the processor creates during onboarding, subject carries clientId only — no cardholderId
transaction.created (collateral only)type, amount, currency, postedAtA collateral deposit landed. type here is the transaction's kind, not the event's

card.* events are attributed and delivered, but have no allowlist entry yet, so they arrive with data: {} — a signal to re-read GET /cards/:id.

type is the card processor's vocabulary, not the service's own vocabulary. Match the exact strings above and ignore types you do not recognize — more will appear, and they are additive.

Events not delivered yet

Earlier drafts of this section published a full catalog (company.approved, card.activated, transaction.declined, contract.funded, challenge.requested, and a rich spend payload). None of those reach you today. Building handlers for them now means building against something that will not fire.

TodayWhy
contract.* (funding)❌ never deliveredNo attribution rule — the events are ignored at ingest
challenge.* (3-D Secure)❌ never deliveredSame
Spend transactions❌ not delivered todayAttribution currently reads only collateral.companyId, which a spend does not carry, so spends are dropped rather than guessed at. Verified live 2026-08-22. This one is expected to change — the payload shape has since been measured and the gap is a small, deliberate decision away; it will be announced before it ships
*.created / *.approved / *.rejected variants⚠ delivered only if the processor emits them; data is {} without an allowlist entryAllowlist entries are added from captured payloads, never from a schema

Poll for what is not evented. Funding balance has no webhook and is not getting one — use GET /clients/:id/funding. Spend history is GET /transactions.

Forward contract — the spend payload

Published so you can design for it; it is not deliverable today. Two gotchas that will apply: amounts are minor units (integer cents — divide by 100), and the decline field is spelled declinedReason. The service's own REST responses use the decimal-string amount from §3.

{
"type": "spend",
"amount": 4290,
"currency": "USD",
"status": "completed",
"declinedReason": "…",
"merchantName": "…",
"merchantCategoryCode": "5814",
"authorizedAt": "…",
"postedAt": "…"
}

Recovery endpoints

EndpointPurpose
GET /events?since=<integer>&limit=Cursor catch-up. since is an integer sequence, not an event id; start at 0 and pass back nextCursor. nextCursor is null only when you are caught up. A non-numeric cursor is rejected (INVALID_CURSOR) rather than treated as 0. ⚠ The page-size param here is limit (default 50), not _limit — this route predates the _limit convention.
GET /events/:idOne event, same projection as the push
POST /events/:id/resendRe-push one event with a fresh signature. Asking twice while one is pending is rejected — it is an explicit request, not a retry knob

Events are retained for 180 days — the contractual recovery window. Once the archive sweep lands, a cursor older than the window answers 410 naming the oldest still-available event.

⚠ In this version nothing is pruned yet and there is no 410, so history currently reaches further than the guarantee. That is the archive milestone being outstanding, not a longer promise — keep your own record of what you have processed.