Skip to main content
Driftstack DRIFTSTACK

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

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:

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:

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

Related