Turn outcomes into one ledger entry
Use webhooks for low-latency notification and the deposit API as the current source of truth. A scheduled poller closes gaps caused by downtime or exhausted retries.
| Path | Best use | Required protection |
|---|---|---|
| Webhook | Prompt processing | Signature verification and event deduplication |
GET /v1/deposits/{id} | Current state lookup | API authentication and rate-aware retry |
GET /v1/deposits | Periodic sweep | Cursor/checkpoint and bounded time windows |
GET /v1/deposits/{id}/events | Investigation | Treat as history, not a second credit trigger |
Credit exactly once
Use the Verify Checkout deposit ID as a unique ledger key inside the same database transaction that credits your customer.
await database.transaction(async (transaction) => {
const inserted = await transaction.creditEvents.insertIfAbsent({
externalId: deposit.id,
amount: deposit.amount,
currency: deposit.currency,
});
if (inserted) await transaction.wallets.credit(deposit.merchant_customer_id, deposit.amount);
});succeeded is the crediting outcome. Failed and expired deposits must not be
credited. Hold review outcomes for operational resolution. For any unfamiliar or
non-terminal state, store it and wait rather than assuming success.
Recovery loop
- Verify and durably enqueue each webhook before returning
2xx. - Deduplicate by event ID and apply ledger changes by unique deposit ID.
- Retrieve the deposit before acting when events arrive out of order.
- Periodically list unresolved deposits and poll their current state.
- Use the event timeline and
meta.requestIdto investigate discrepancies.
Retries are normal
At-least-once delivery means repeated events are expected, not exceptional. Correctness comes from idempotent storage, not from assuming one delivery.
Combine webhooks and polling to credit deposits exactly once and recover safely.
Last updated October 6, 2026