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 theevent_idin the signature input. Read it from theX-Oxen-Event-Idheader 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 parsingdata. 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
sequencefield in the envelope. The catch-up cursor is a sequence, but it is returned asnextCursoron 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.amount → amount).
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
type | data | Meaning |
|---|---|---|
company.updated | applicationStatus | Client KYB outcome |
user.updated | applicationStatus | Cardholder 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, postedAt | A 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.
| Today | Why | |
|---|---|---|
contract.* (funding) | ❌ never delivered | No attribution rule — the events are ignored at ingest |
challenge.* (3-D Secure) | ❌ never delivered | Same |
| Spend transactions | ❌ not delivered today | Attribution 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 entry | Allowlist 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
| Endpoint | Purpose |
|---|---|
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/:id | One event, same projection as the push |
POST /events/:id/resend | Re-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.