Detecting order state changes
A crypto order's status moves between
pending → confirming →
paid (or failed /
partial / cancelled). Two
customer-facing patterns are supported: poll
GET /v1/billing/crypto-orders, or subscribe a
webhook endpoint to the now-live
crypto.order.paid /
crypto.order.failed events. See
/docs/webhooks-crypto-events
for the event payload contract.
Today: polling
Polling stays a first-class path for backfill,
reconciliation, and any environment where you cannot accept
an inbound HTTPS callback. Both
crypto.order.paid and
crypto.order.failed are also subscribable via
POST /v1/webhooks — see the hybrid section
below for the recommended pattern.
// Watch a single order until it settles.
for (;;) {
const order = await client.cryptoOrders.get(orderId);
if (order.status === 'paid' || order.status === 'failed' ||
order.status === 'partial' || order.status === 'cancelled') {
break;
}
await sleep(5_000);
} Polling cadence
Pick a cadence that matches your latency tolerance:
- 1-5 seconds: user is watching a "Pay with crypto" page. Stop polling once status is terminal.
- 30-60 seconds: background process tied to a dashboard tab. Default for the V-534 history view.
- Hourly / nightly: reconciliation + backfill. Use the cursor (V-666.BU) to walk every order once per window.
Combining cursor pagination + status filter
For backfill, scan only the orders you care about by
combining ?status=paid with cursor pagination:
for await (const o of client.cryptoOrders.listAll({
status: 'paid',
limit: 100,
})) {
await reconcile(o);
}
The SDK's listAll() manages cursors internally
and yields one envelope at a time, so consumers can
break early without paying for the full scan.
What the customer dashboard does
The Driftstack customer dashboard polls
GET /v1/billing/crypto-orders every 60s while
any visible order is still pending (V-534.BS).
Polling stops automatically as soon as everything settles.
For an SDK-driven checkout flow that needs faster feedback,
shorten the poll interval to 5-10s — the rate-limit budget
on the customer list endpoint is generous enough for that
pattern.
Recommended: hybrid (webhooks + reconciliation polling)
Both crypto.order.paid and
crypto.order.failed are in
SubscribableWebhookEventTypeSchema. The
recommended pattern is hybrid: webhooks for sub-second
notification, periodic polling as a safety net for the
rare delivery that does not land within the retry window.
// Subscribe both terminal-state events.
await client.webhooks.create({
url: 'https://example.com/webhooks/driftstack',
events: ['crypto.order.paid', 'crypto.order.failed'],
});
app.post('/webhooks/driftstack', verifySignature, async (req, res) => {
const event = req.body;
if (event.type === 'crypto.order.paid') {
await db.markOrderPaid(event.data.order_id);
} else if (event.type === 'crypto.order.failed') {
await db.markOrderFailed(event.data.order_id);
}
res.status(200).end();
}); Idempotency for hybrid receivers
Webhook delivery for crypto.order.* follows the
same retry policy as the rest of the surface, so receivers
must be idempotent — the same event may be delivered more
than once. Key your local DB upsert on
(order_id, status) so duplicate
crypto.order.paid events for the same order are
a no-op. See
/docs/webhooks for the signing
scheme + retry schedule shared across all subscribable
event types.