Crypto payments — integration guide
This page walks through a full integration with the Driftstack crypto-payments surface from scratch. By the end you'll have a working checkout flow, a webhook handler that listens for settlement, and a confidence test that exercises the round trip. Per-endpoint references live behind the links in each section; this page is the glue.
Before you start
- You need an API key with the
writescope. The admin endpoints documented elsewhere (sweep, stats, internal notes) are NOT required for a customer integration. - You need a public endpoint to receive webhook deliveries.
For local development, use a tunnel
(
ngrok,cloudflared, etc.). - You need a wallet at NowPayments — Driftstack does not custody funds.
Step 1 — mint a checkout
When the customer clicks "Pay with crypto," POST to
/v1/billing/crypto-checkout with the tier they're
buying and the fiat-cents price. Always send an
Idempotency-Key so accidental retries don't mint
duplicate orders.
const key = crypto.randomUUID();
const res = await fetch('https://api.driftstack.dev/v1/billing/crypto-checkout', {
method: 'POST',
headers: {
authorization: `Bearer ${API_KEY}`,
'content-type': 'application/json',
'idempotency-key': key,
},
body: JSON.stringify({
product: 'team_manual',
price_cents: 4900,
price_currency: 'USD',
}),
});
const order = await res.json();
The response carries an order_id and either a
payment_address (production NowPayments-backed
checkout) or null with provider: 'stub'
(Driftstack hasn't yet provisioned a merchant account for the
pair). See
/docs/idempotency-keys
for the Idempotent-Replayed response header.
Step 2 — show the customer the payment address
The customer needs three pieces of information to pay:
- The wallet address (
payment_address). - The currency they should send in
(
pay_currency— e.g.BTC,USDT). - How long the address remains valid. Today's window is roughly 60 minutes from order mint.
Render a QR code over the address (any client-side QR library works) and let the customer copy the address verbatim. Don't pretty-print the amount; on-chain payments are measured in crypto units, not fiat cents.
Step 3 — observe settlement
crypto.order.paid and
crypto.order.failed are emitted server-side and
are now on the subscribable webhook event list — see
/docs/webhooks-crypto-events
for the payload contract. For integrations that cannot
accept inbound HTTPS callbacks, poll
GET /v1/billing/crypto-orders/<order_id>
until status transitions to paid or
failed:
async function waitForSettlement(orderId, apiKey) {
while (true) {
const res = await fetch(
`https://api.driftstack.dev/v1/billing/crypto-orders/${orderId}`,
{ headers: { authorization: `Bearer ${apiKey}` } },
);
const order = await res.json();
if (order.status === 'paid') return order;
if (order.status === 'failed') throw new Error('order failed');
await new Promise(r => setTimeout(r, 5000));
}
} Grant entitlements only after observing
status === 'paid'. Whether you arrive
at that observation via the now-live webhook or via the
polling fallback, the rule is the same: never grant on
confirming or partial.
Step 4 — show the customer the receipt
After crypto.order.paid, the customer can fetch a
receipt at:
GET /v1/billing/crypto-orders/:id/receipt— JSON.GET /v1/billing/crypto-orders/:id/receipt.txt— plain text.GET /v1/billing/crypto-orders/:id/receipt.pdf— PDF with a Content-Disposition attachment.
Receipts are immutable; they reflect the order envelope at the
moment of paid.
Step 5 — confidence test
In dev, replay a fake IPN against your own integration. The
troubleshooting
page documents the admin
POST /v1/admin/crypto-orders/:id/apply-ipn route
that lets you drive an order to paid without
sending real coins.
Backfill + reconciliation
For nightly jobs or post-incident catchups, walk every
matching order using the cursor + date-range
filters. The SDK's listAll() manages
the cursor for you:
const since = lastReconcileTimestamp().toISOString();
for await (const o of client.cryptoOrders.listAll({
status: 'paid',
created_after: since,
limit: 100,
})) {
await db.ensureOrderPaid(o.order_id);
} Without the SDK, the same loop with raw fetch:
let cursor;
while (true) {
const url = new URL('https://api.driftstack.dev/v1/billing/crypto-orders');
url.searchParams.set('status', 'paid');
url.searchParams.set('created_after', since);
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { authorization: 'Bearer ds_live_...' },
});
const { orders, next_cursor } = await res.json();
for (const o of orders) await reconcile(o);
if (!next_cursor) break;
cursor = next_cursor;
}
Crypto payments are non-refundable; the reconciliation loop
is naturally idempotent because the local DB key is
(order_id, status). See
polling vs
webhooks for when to pair backfill with a live webhook
handler.
Edge cases
- Customer pays the wrong amount. The
order moves to
partial. No entitlement is granted. Your support team reconciles manually. - Customer pays after the address window expires.
The order is already in
failedwhen the IPN arrives. The IPN is recorded on the order; support reconciles manually. - Customer wants a refund. Crypto payments are non-refundable. The order can be cancelled to stop future billing periods. See /legal/refunds.
- Customer's browser retries the checkout POST.
If you sent
Idempotency-Key, the second call returns the original order. If you didn't, you mint a duplicate; that's why Driftstack's own GUI client always sends the header.