Paying with crypto
Every self-serve paid tier can be bought with cryptocurrency instead of a card. Payments run through NowPayments — Driftstack never custodies your coins. You get a single-use payment address, send the exact amount shown, and Driftstack activates the tier once the payment settles on-chain. Crypto payments are final and non-refundable (refund policy), so this page also covers what each order state means and how to retry safely.
This is the walkthrough. The endpoint-by-endpoint reference lives at Crypto checkout.
The dashboard flow (no code)
- Open Select tier in the dashboard and pick a tier.
- Click or pay with crypto → under the tier card (instead of the card-checkout button).
- The modal shows three things: the exact amount, the currency to send it in, and a single-use deposit address. Copy the address verbatim and send the exact amount from your wallet.
- You can close the window — the order is tracked even if you
navigate away. Once the payment settles, the order shows as
paidunder Billing → Crypto payments, where you can also download a PDF receipt.
What you need for the API flow
- Authentication that carries the
admin:billingscope. A dashboard (account-owner) session has it; a plainread/writeAPI key does not — starting a checkout is a subscription-change action. See API key scopes. - A tier to buy. The six self-serve paid tiers are purchasable with
crypto (
solo_manual,team_manual,agency_manual,api_starter,api_builder,api_scale); the free tier is not a purchasable product.
Step 1 — mint an order
Create a checkout order with POST /v1/billing/crypto-checkout.
Always send an Idempotency-Key header so a retried request returns
the original order instead of minting a duplicate:
import { randomUUID } from 'node:crypto';
const order = await client.cryptoOrders.createCheckout(
{
product: 'team_manual',
price_cents: 24900,
price_currency: 'USD',
},
{ idempotencyKey: randomUUID() },
);
curl -X POST https://api.driftstack.dev/v1/billing/crypto-checkout \
-H "authorization: Bearer ds_live_…" \
-H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "product": "team_manual", "price_cents": 24900, "price_currency": "USD" }'
Two things to know about the request body:
- The price you send is validated for shape but ignored. The
amount charged always comes from the server-side price table, so a
tampered
price_centscan’t buy a tier at the wrong price. Fetch the current price first withPOST /v1/billing/crypto-checkout/quote(sameproductbody) if you don’t want to hardcode it. - The settlement currency is locked to USD server-side. NowPayments converts the USD amount to crypto with its own rate engine at address-mint time.
The 201 response carries the order_id (an ord_-prefixed id —
quote it in every support request), the status (always pending at
mint), and the payment details:
{
"order_id": "ord_a1b2c3d4e5f6",
"product": "team_manual",
"price_cents": 24900,
"price_currency": "USD",
"status": "pending",
"provider": "nowpayments",
"payment_address": "bc1qexample...",
"pay_currency": "btc",
"pay_amount": 0.00123,
"created_at": "2026-07-07T10:00:00.000Z"
}
If provider is "stub" the payment fields are null — the
NowPayments side couldn’t mint an address for this order and support
completes the payment setup manually. The order still exists and is
trackable by its order_id.
A replayed request (same Idempotency-Key) returns the original
order verbatim plus an Idempotent-Replayed: 1 response header —
including the original payment address, so a retry never shows you a
second address. Keys replay for as long as the order exists; see
Idempotency keys.
Step 2 — pay the address
Send exactly pay_amount in pay_currency to
payment_address, on the network the checkout shows. The address is
single-use and network-specific — a token sent on a different
network than the one displayed can be lost. While the order is
pending, its envelope carries an expires_at hint
(created_at + 60 minutes) that the dashboard renders as a
countdown; treat it as the window in which to broadcast your
payment.
Don’t round the amount. An under-payment (beyond a ~1% slippage
tolerance) parks the order in partial instead of paid, and
resolving a partial takes a manual support step.
Step 3 — watch the order settle
Two supported patterns, and they compose:
- Webhooks (push): subscribe to
crypto.order.paidandcrypto.order.failed— both are live, subscribable event types. Payload contract and receiver guidance: Crypto order events. - Polling (pull): read the order until its
statusgoes terminal.
for (;;) {
const order = await client.cryptoOrders.get(orderId);
if (['paid', 'failed', 'partial', 'cancelled'].includes(order.status)) break;
await new Promise((r) => setTimeout(r, 5_000));
}
Grant anything on your side only when you observe
status === 'paid' — never on confirming (not settled yet) and
never on partial (under-paid; not credited). Driftstack applies the
same rule: the tier purchase is activated on the paid transition,
not before.
Activation applies your account’s tier automatically on that paid
transition, and it is upgrade-only: if your account already sits
on a higher tier by the time the order settles (say a card
subscription started after you minted the order), the paid order
never downgrades you — the payment is still recorded on the order and
support reconciles it with you. Paying for the tier you already hold
changes nothing (no duplicate emails). Each activation is written to
your audit log as a subscription.tier_changed
entry carrying the order_id.
The order lifecycle
| Status | What it means | What you can do |
|---|---|---|
pending | Order minted; no payment seen yet. | Pay the address, or cancel the order if you changed your mind. |
confirming | NowPayments has seen the payment; waiting on network confirmations. | Wait. Cancelling now needs support so your on-chain funds can be reconciled. |
paid | Confirmations complete and the received amount reconciled against the quote. Tier purchase is activated. | Download the receipt. |
partial | A payment arrived but the crypto-denominated amount is short of the quote (or in a mismatched currency). | Contact support with the order_id + TX hash — resolution is a top-up, never a send-back. |
failed | NowPayments reported failed / expired / refunded, or the order sat unpaid and was retired as stale. | Open a fresh order. If funds left your wallet, escalate to support with the TX hash. |
cancelled | You cancelled while the order was still pending. Terminal. | Open a fresh order whenever you’re ready. |
Terminal states (paid, failed, cancelled) never transition out.
partial is semi-terminal: only paid (after a top-up) or failed
can follow. Every transition is recorded on the order’s append-only
events timeline, which the order endpoints return — useful when
you need to reconstruct what happened.
Step 4 — receipts
Every order exposes a receipt in three formats (the download is
meaningful once the order is paid):
# JSON envelope — programmatic consumers
curl -H "authorization: Bearer ds_live_…" \
https://api.driftstack.dev/v1/billing/crypto-orders/ord_a1b2c3d4e5f6/receipt
# Plain text — cron / curl pipelines
curl -H "authorization: Bearer ds_live_…" \
https://api.driftstack.dev/v1/billing/crypto-orders/ord_a1b2c3d4e5f6/receipt.txt
# PDF — accounting / archival (Content-Disposition download)
curl -H "authorization: Bearer ds_live_…" -O -J \
https://api.driftstack.dev/v1/billing/crypto-orders/ord_a1b2c3d4e5f6/receipt.pdf
The receipt’s paid_at comes from the order’s event log — the
moment of the actual paid transition. The dashboard’s
Billing → Crypto payments list has the same PDF as a one-click
download.
Backfill + reconciliation
For nightly jobs, walk your paid orders with the cursor-managed iterator and a date-range filter:
const since = lastReconcileTimestamp().toISOString();
for await (const order of client.cryptoOrders.listAll({
status: 'paid',
created_after: since,
limit: 100,
})) {
await db.ensureOrderPaid(order.order_id);
}
listAll() (alias iterate()) yields one order at a time and stops
when the server stops emitting a next_cursor. created_after is
inclusive, created_before exclusive, and a window where
created_before <= created_after is rejected with a 400.
Cancelling + refunds
While an order is still pending you can abandon it yourself with
POST /v1/billing/crypto-orders/:order_id/cancel. Once any payment
activity exists, the cancel returns 409 and reconciliation goes
through support. Cancelling a subscription stops future billing
periods, but the current period is not refunded — crypto payments
are non-refundable end to end
(policy). If you expect to
need cash refunds, the card path (Stripe) is the right channel.
Related
- Crypto checkout API reference — every endpoint, full response shapes
- Crypto order events —
crypto.order.paid/crypto.order.failedpayloads - Crypto payment troubleshooting — the “something looks stuck” decision tree
- Idempotency keys — safe retries on checkout
- API key scopes — why checkout needs
admin:billing