Skip to main content
VEXiON cards delivers events to your HTTPS receivers with at-least-once delivery, HMAC-SHA-256 signing, and a 24-hour retry curve. Register receivers with the Webhook endpoints section of the API reference; this page covers what your receiver must do.

How a delivery works

1

Event occurs

An action inside VEXiON cards - a transaction, a customer update, a pass install - creates an event.
2

Signed POST to your endpoint

We POST the event envelope to your endpoint, with the three signature headers attached.
3

You verify and ack

Your receiver checks the HMAC, queues the real work, and returns a 2xx status within 10 seconds.
Anything other than a 2xx response within that window starts the retry schedule below.

The event envelope

Every delivery uses the same outer envelope:
  • id is a ULID, stable across retries - this is your dedup key
  • data.object is the full sanitized resource at the moment the event fired
  • request is present only when an API call caused the event (server-emitted events omit it)

The 22-event catalog

A balance change arrives as transaction.created. Branch on the embedded transaction’s type field. A level change is not an event: on a reward or cashback card nothing is sent when a customer moves to another level (by earning, by a recount or by level retention), so read reward_tier on the customer.
Lifecycle of a customer record (created · updated · deleted) plus wallet pass state (pass_installed · pass_uninstalled).customer.updated is sent when a customer’s profile or card status is edited (dashboard, API, or the customer’s own profile page). A balance change does not send it: that arrives as transaction.created.customer.deleted carries a minimal deletion record, not the customer: id, object, deleted: true, erased, voided_card_count, preserved_transaction_count and deleted_at. deleted_at is in unix seconds. No personal data is included.
Loyalty card object lifecycle - created · updated · deleted.
Catalog changes - created · updated.
Custom field definition lifecycle - created · updated · deleted.
The money- and points-moving events - created · reversed. This is also where balance changes surface, per the note above. transaction.created is sent for stamp, points, cashback and discount entries, and for a coupon redeem made through POST /v1/transactions. Gift card and membership entries are listed by GET /v1/transactions but send no event.The transaction in data.object carries team_member_id (the staff member who booked it, when a team member did) and products (the line items of the bill). The customer events carry currency on the card, the business currency. See Money and currency.
Outbound customer messaging - sent and delivery status changes (delivery_updated).
Location metadata changes - updated.
Staff account lifecycle on your organization - added · updated · removed.
Meta events about your own endpoints - delivery_failed, sent to other subscribed endpoints when one endpoint’s delivery exhausts its retries. The payload carries webhook_endpoint_id, url, failed_event_id, failed_event_type, attempts, last_status_code and last_error. last_status_code and last_error are null when the last attempt did not produce that value.

Signature verification

Every delivery carries the three Standard Webhooks headers, so battle-tested verification libraries exist for every language:
The signed content is {webhook-id}.{webhook-timestamp}.{raw body} - exact body bytes, no normalization. The HMAC-SHA256 key is the base64-decoded part of your whsec_... secret: strip whsec_, then decode the remainder with standard base64. Since 2026-08-23 secrets use the standard alphabet and can contain + and /. Legacy secrets use - and _, so use a decoder that accepts both, as the manual examples below do. A library that decodes the secret as strict standard base64 can fail on a legacy secret. If that happens, rotate the secret: a new secret uses the standard alphabet. During a secret rotation the header carries TWO space-delimited v1, signatures, the new one first and then the old one - accept if any matches.

Manual verification

Verify against the raw request bytes - parsing and re-serializing JSON changes the bytes and breaks the signature. Reject timestamps older than 5 minutes (replay protection). Compare signatures with a constant-time function.

Read past events

GET /v1/events lists the events that were sent to at least one of your enabled endpoints, newest first. Use it to catch up after your receiver was down. GET /v1/webhook_endpoints/event_types lists the event types you can subscribe to. Both need the webhooks:read scope.

Test your receiver

POST /v1/webhook_endpoints/{id}/test sends a signed ping (or a realistic example of any catalog event via event_type) synchronously and returns what happened - TLS, signature, and 2xx proven in one call before you go live. Test sends never count toward auto-disable.

Delivery contract

  • Success = any 2xx within 10 seconds. Ack fast; do the real work in a background queue.
  • At-least-once - the same event id may arrive multiple times. Dedup on event.id from the verified body (the header is an unsigned hint). Retain seen ids for ≥7 days.
  • No ordering guarantee - events can arrive in a different order than they happened. Use the created_at on the envelope and the state of the resource to decide what is current.
  • Fresh connection per attempt - no keep-alive reuse; every attempt re-resolves DNS and re-validates the IP (rebinding defense).

Retry schedule

12 attempts across ~24 hours with exponential backoff + 20 % jitter: After attempt 12 the delivery is failed and a webhook_endpoint.delivery_failed event fires to any other subscribed endpoint.

Status code semantics

Auto-disable: 50 failed delivery attempts in a row, counted across all events sent to the endpoint, move it to auto_disabled. Any 2xx resets the count, and the count can reach 50 in the middle of a retry cycle. Events emitted while an endpoint is disabled or auto_disabled are not delivered to it and are not queued for it. Deliveries already queued stop at send time. Re-enable via dashboard or PATCH with status: "enabled" to receive new events.

Retry and disable flow

Security checklist

  1. Verify the HMAC on every request with timing-safe comparison - reject everything unsigned
  2. Enforce the 5-minute anti-replay window
  3. Dedup on the event id - repeated ids get a 200 no-op, not an error
  4. Respond within 10 seconds - queue long work
  5. HTTPS with a publicly-trusted certificate on every endpoint
  6. Keep whsec_ out of source control and logs - rotate on suspected exposure
  7. Tolerate unknown fields - we add optional fields under date versions
  8. Handle both signatures during rotation
  9. Never trust the source IP as auth - the signature is the only auth