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.
Events
Subscribe to
"*" to receive all of these, including events added later.
Receiving a delivery
Spritz sends aPOST 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.
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 sixofframp.* 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 fiveachDebit.* 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:
- Begin a database transaction.
- Insert
eventIdinto a webhook inbox with a unique constraint. If it already exists, commit and return2xxwithout enqueueing another push. - Compare
sequencewith the highest accepted sequence forid. If it is lower or equal, record it as stale, commit, and return2xx. - Update the deposit’s highest sequence and insert one push-outbox row keyed by
eventIdin the same transaction. - Commit and return
2xx. A separate worker sends the push.
eventId
as the push provider’s idempotency or collapse key when the provider supports one.
Never mark an event accepted only in memory.
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 withPOST /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 a2xx 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.
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
Respond fast, process later
Respond fast, process later
Acknowledge with a
2xx immediately and hand off to a queue. Slow responses can
trigger retries and duplicate processing.Make handlers idempotent
Make handlers idempotent
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.Don't rely on event order
Don't rely on event order
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.Treat events as triggers, not truth
Treat events as triggers, not truth
On receipt, fetch the current resource from the API to get authoritative state
rather than relying solely on the event body.
Reconcile after an outage
Reconcile after an outage
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.