Skip to main content
Money movement is asynchronous. An off-ramp is requested, then converts, then settles to a bank account over time. Webhooks let Spritz notify your backend the moment something changes, so you don’t have to poll. Webhooks are configured per integrator, so you manage them with your integrator credentials (HMAC). See Authentication.
Webhooks are global, not per user. You subscribe once and receive events for all of your users. There’s no need to register a webhook per user, and you don’t pass a user API key when managing webhooks. These are integrator endpoints, so they use your integrator signing headers only.

Register an endpoint

Create a webhook by pointing it at an HTTPS URL on your backend and listing the events you want. Use "*" to subscribe to every current and future event.
You can list, update, and delete webhooks too:

Events

Subscribe to "*" to receive all of these, including events added later.

Receiving a delivery

Spritz sends a POST to your URL with a JSON body identifying what changed:
id is omitted for events whose subject is the user rather than a separate resource — capabilities.updated is the case you are most likely to meet. Key your handler off event and treat id as optional:
Generic payloads carry no occurrence id and no timestamp, so two deliveries of the same event on the same resource can be byte-identical (and a replay produces an identical signature). That’s why the dedupe guidance is what it is: treat deliveries as triggers, keep handlers idempotent, and reconcile from the API instead of trying to distinguish occurrences.
The body tells you which resource changed, not its full state. Treat it as a trigger: fetch the resource from the API to get authoritative state before acting on it. For achDebitReturn.*, id is the public dr_... return ID. Fetch it with GET /v1/integrator/ach-debit/returns/{id}. Then use its depositId and sourceId to refresh the deposit and funding source. For onramp.*, fetch the on-ramp. When its source contains a depositId, fetch GET /v1/deposits/{depositId} with the user’s authorization. These generic resource events keep your UI and local state current. Do not send ACH push notifications from generic onramp.* or achDebitReturn.* events. A retry can produce the same current state, so state comparison alone cannot identify one notification occurrence. For payment.*, id is the off-ramp id — fetch GET /v1/off-ramps/{id} with the user’s authorization (or your HMAC credentials acting for them). A typical off-ramp fires payment.created and payment.updated when it’s created, payment.updated as it moves through the rails, and payment.completed when fiat lands. There is no payment.failed event — a failure arrives as payment.updated, so never infer state from the event name; read the off-ramp’s status. You’ll also see account.updated fire on the destination bank account as its state changes. For user-facing notifications, prefer the offramp.* milestone events — they carry copy-ready snapshots; use payment.* only to keep local state in sync.

Off-ramp milestone events

The six offramp.* events are the notification contract for crypto-to-fiat: one event per user-meaningful moment, each with an immutable snapshot taken at the transition. The identity and handling contract is identical to achDebit.* — stable eventId across retries and replays, monotonic per-off-ramp sequence, durable-inbox handling (see the handler rules above).
These events are rolling out now: you can subscribe to them today, and they start firing as the emitters deploy. Until then, payment.* remains the way to track off-ramp progress.
The event and milestone pair is fixed. quoteId links the off-ramp to its quote (null for auto-ramp-address deposits, which have no quote). transaction carries the funding transaction’s hash and explorer link once known. failureMessage is reserved for public-safe failure copy and is currently always null — branch on the milestone, not on absent detail.
The onrampCredit.* family (depositDetected, completed, failed, reversed, refunded) is the same contract for auto-ramp-account funding deposits — fiat in, crypto out. Same rollout: subscribable now, fires as the emitter deploys.

ACH debit communication events

The five achDebit.* events are the push-notification contract. Unlike generic resource events, each delivery contains an immutable milestone snapshot:
eventId identifies one notification occurrence and stays unchanged across delivery retries and replays. sequence starts at 1 and increases for each communication milestone on that deposit. Deliveries are still at least once and can arrive out of order. id, deposit.id, and fundingSource.id are the same public IDs used by the deposit and funding-source APIs. Each achDebit.* payload is versioned and fully typed in the OpenAPI contract. These fields are always present: The event and milestone pair is fixed: Your webhook handler must durably accept the event before returning 2xx:
  1. Begin a database transaction.
  2. Insert eventId into a webhook inbox with a unique constraint. If it already exists, commit and return 2xx without enqueueing another push.
  3. Compare sequence with the highest accepted sequence for id. If it is lower or equal, record it as stale, commit, and return 2xx.
  4. Update the deposit’s highest sequence and insert one push-outbox row keyed by eventId in the same transaction.
  5. Commit and return 2xx. A separate worker sends the push.
This is a small generic inbox/outbox, not ACH-specific state inference. Use eventId as the push provider’s idempotency or collapse key when the provider supports one. Never mark an event accepted only in memory.
HTTP delivery cannot guarantee that an external push provider displays a message exactly once. The stable event ID, sequence guard, transactional push outbox, and provider idempotency key close every duplicate path your integration can control.
Use the immutable event snapshot for push copy. Refetch the deposit separately for the current UI; a later state must not change the meaning of an earlier notification. See ACH debit user experience for the exact copy.

Verifying signatures

Each delivery is signed so you can confirm it came from Spritz and wasn’t modified in transit. Set a webhook secret with POST /v1/integrator/webhook-secret, then verify the Signature header against the raw request body using HMAC-SHA256. Always verify against the raw body, before parsing JSON.

Retries and delivery outcomes

Return a 2xx as soon as you have durably accepted the event. Spritz does not retry a 4xx. Spritz retries a 5xx or timeout twice, for three attempts total. Those attempts happen within roughly 17 seconds; there is no long-lived redelivery queue. A delivery-log record summarizes the final outcome after those attempts; it is not one record per attempt. Webhook configuration is cached by delivery workers and can take about 60 seconds to propagate. Wait before using a newly registered endpoint or changed secret in a test. Inspect outcomes with GET /v1/integrator/webhooks/deliveries. Results are newest first and cursor-paginated. Spritz retains each delivery record and its exact payload for at least 30 days. It remains recovery history, not permanent event storage. Store accepted events durably, run delivery reconciliation at least every 24 hours, and reconcile again immediately after your receiver recovers from an outage. Follow cursors until no more records remain.
Read error before interpreting responseStatus: payload is the exact body Spritz sent and signed. Delivery history is diagnostic, not a queue. After a final generic resource-event failure, recover through the resource read API instead of waiting for another delivery. A current resource read cannot reconstruct a missed historical communication occurrence. For a failed achDebit.* delivery, pass the delivery record’s exact payload through the same durable inbox handler as a live event; its stable eventId makes that replay safe.

Best practices

Acknowledge with a 2xx immediately and hand off to a queue. Slow responses can trigger retries and duplicate processing.
Retries mean the same payload can arrive more than once. The same resource can also produce several legitimate updated events, so do not permanently deduplicate on id. Fetch current state and make applying it safe to repeat. See Idempotency.ACH communication events are the exception: deduplicate them permanently on their stable eventId. Different milestones for the same deposit have different event IDs and increasing sequence values.
Events are dispatched concurrently and are not ordered. onramp.completed can arrive before onramp.created for the same on-ramp, and does so most often when the two transitions happen close together.The payload carries no timestamp, so order cannot be reconstructed from the event alone. Key your handler off the resource id, fetch current state from the API, and make each handler safe to run regardless of what has already been processed for that resource — including the case where the first event you ever see for a resource is its last one.For achDebit.* communication events, atomically ignore a sequence that is not greater than the highest sequence you already accepted for that deposit. This prevents a delayed partial-delivery event from producing a push after full delivery.
On receipt, fetch the current resource from the API to get authoritative state rather than relying solely on the event body.
Webhooks are notifications, not your only record. Page through the relevant public list endpoint after downtime. For linked-bank deposits, page through GET /v1/deposits/; this endpoint is user-scoped, not integrator-scoped, so an integrator-wide recovery must iterate your own user roster and authorize each user’s read. For ACH returns, use GET /v1/integrator/ach-debit/returns. Then inspect GET /v1/integrator/webhooks/deliveries if you need to diagnose delivery or recover an exact missed achDebit.* communication payload. Run this reconciliation at least every 24 hours; delivery payloads are guaranteed for 30 days. Do not synthesize a historical push from current resource state.