Crypto orders — ops runbook
This page documents the support workflow for the crypto- payments surface. It's published publicly because the non-refundable policy means every decision support takes is consequential, and customers deserve to read the same playbook the support team follows.
"Here's a NowPayments payment id — what order is that?"
The fastest reverse-lookup path: the admin list endpoint
accepts an exact-match payment_id query param.
GET /v1/admin/crypto-orders?payment_id=np_abc123
Authorization: Bearer $ADMIN_KEY
Returns the (at most one) matching order. Available in the
admin GUI as the "Payment ID" filter input next to the search
box. Distinct from the fuzzy search param (which
walks order_id / product / customer_note) so the lookup is
O(scan) but unambiguous.
"My order is stuck in pending"
- Pull the order:
GET /v1/admin/crypto-orders/:idas an admin key. - Check the age. Pay windows are ~60 minutes. Orders past
that without an IPN are candidates for sweep
(
POST /v1/admin/crypto-orders/sweep-expired). - If the customer says they paid, ask for the chain +
tx-hash. Cross-reference against NowPayments's dashboard.
If the payment landed but the IPN never fired, manually
replay the IPN:
POST /v1/admin/crypto-orders/:id/apply-ipnwith the recordedpayment_id+provider_status: 'finished'. - If the payment never landed, sweep the order to
failedand explain the pay window.
"I see two charges on my card"
Crypto orders don't touch cards — that's the Stripe surface. Confirm whether the duplicate is crypto-vs-crypto (likely double-click → handled by V-666.AO idempotency keys) or crypto-vs-Stripe (handle on the Stripe side). For two crypto orders on the same customer/product:
- Confirm both order ids exist + are both
paid. - Check the
idempotency-metricsendpoint — does thereplayscount match what you'd expect from this customer's flow? - If both orders truly settled, this is a customer-side bug (their integration sent two distinct Idempotency-Keys for what they intended as one intent). Crypto is non- refundable; the resolution is to credit the customer's next billing cycle, not refund.
"What happened to this order, in order?"
Pull the timeline:
GET /v1/admin/crypto-orders/:id/events
Authorization: Bearer $ADMIN_KEY
Returns the order's append-only event log oldest-first. Each
event carries the destination status, an ISO-8601 timestamp,
and a source tag — create,
ipn, cancel, expired,
or swept. Same timeline is rendered inline on
the admin detail drawer.
"Why is my order failed?"
Look at the order's payment_id + the most
recent IPN payload. The internal status transitions to
failed in three cases:
- NowPayments reported a terminal failure
(
failed/expired/refunded). The IPN payload tells you which. - The order's pay window elapsed and the customer-facing
cancelendpoint marked it expired. - An admin sweep retired a long-pending order.
Server-side, the crypto.order.failed event
emitted by these transitions carries a reason
field with one of ipn / expired /
swept. This event is customer-subscribable
via POST /v1/webhooks (in
SubscribableWebhookEventTypeSchema as of
2026-05-22, migration 0064); see
/docs/webhooks-crypto-events
for the payload contract. Ops can read the same source via
the /events endpoint above for retrospective
audit.
"How do I bulk-export today's orders for reconciliation?"
Use /docs/admin-csv-export from the admin GUI, or hit the CSV endpoint directly. For anything beyond 1000 rows, walk the cursor-paginated JSON list (cursor docs).
V-666.BY: scope the export to a date window with
?created_after + ?created_before
(ISO 8601). Both work on the JSON list, the CSV endpoint, and
the admin GUI's From/To inputs. Combine with
?status=paid for a nightly paid-only reconcile.
GET /v1/admin/crypto-orders.csv?status=paid&created_after=2026-05-01T00:00:00Z&created_before=2026-05-12T00:00:00Z
Authorization: Bearer ds_admin_… "A customer's idempotency-key is misbehaving"
Check the body_mismatches counter on
/v1/admin/crypto-orders/idempotency-metrics. A
non-zero number means at least one client is reusing keys
across distinct intents — usually a hardcoded constant where
a generated UUID belongs. The structured warn log
(event: 'crypto_checkout_idempotency_body_mismatch')
carries the account id; grep for the offending integration.