> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Crypto top-up API lifecycle

> Create, poll, and cancel a crypto (NOWPayments) top-up from your integration with runnable request and response samples for the full intent lifecycle.

# Crypto top-up API lifecycle

The [cryptocurrency payments guide](/billing/crypto-payments) walks the
dashboard flow. This page is the same lifecycle driven from an integration:
you create the top-up, open the hosted invoice URL for the payer, poll the
intent until it settles, and optionally cancel it while the invoice is still
unfunded.

Every sample below is copy-pasteable. The three endpoints require an API key
carrying the `billing:write` scope (or an owner, admin, or billing dashboard
session) and are rate-limited as money-moving operations.

***

## 1. Create a top-up intent

`POST /api/v1/billing/balance/top-up-crypto` mints a NOWPayments hosted
invoice and returns its URL. `Idempotency-Key` is required: re-submitting
the same key returns the original invoice instead of minting a new one, and
re-using the key with a different `(amount, currency)` returns
`409 IDEMPOTENCY_KEY_REUSED`.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/billing/balance/top-up-crypto \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crypto_2026-09-08_user_42_abc123" \
  -d '{
    "amount": 10000,
    "currency": "usd",
    "successUrl": "https://orbit.devotel.io/billing?topup=ok",
    "cancelUrl": "https://orbit.devotel.io/billing?topup=cancelled"
  }'
```

```json 200 theme={null}
{
  "data": {
    "checkoutUrl": "https://nowpayments.io/payment/?iid=4522625843",
    "intentId": "cryptoIntent_2vXq7p9yzAbCdEf",
    "paymentMethod": "crypto_nowpayments",
    "feeCents": 0,
    "feePaidByUser": true
  },
  "meta": {
    "request_id": "req_crypto_001",
    "timestamp": "2026-09-08T12:00:00Z"
  }
}
```

* `amount` is in minor units (cents): minimum `100` ($1.00), maximum `1000000` ($10,000).
* `currency` accepts only `usd` today — the wallet ledger is USD-denominated.
* `successUrl` and `cancelUrl` must be on a trusted origin
  (`orbit.devotel.io`).
* `feePaidByUser` reflects the network-fee policy: for top-ups of $50 or
  less Orbit absorbs the fee (`feePaidByUser: false`, `feeCents` is the
  absorbed amount); above $50 the fee is added on top of `amount` at the
  hosted invoice (`feePaidByUser: true`, `feeCents: 0`).
* If crypto top-up is not yet enabled for your account, the endpoint returns
  `503 SERVICE_UNAVAILABLE` — use Stripe Checkout in the meantime.

Open `checkoutUrl` in a browser for the payer. The coin and the exact
crypto amount are chosen on the hosted page; the intent settles once the
network confirms the transfer.

***

## 2. Poll the status

`GET /api/v1/billing/crypto-topup/{intentId}/status` returns the current
state of the intent. Poll it with a few seconds' spacing while the payment
settles; the endpoint self-heals a dropped provider callback, so the poll
always reflects the latest known state. Read scope (`billing:read`) is
sufficient for this call.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/crypto-topup/cryptoIntent_2vXq7p9yzAbCdEf/status" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 theme={null}
{
  "data": {
    "intentId": "cryptoIntent_2vXq7p9yzAbCdEf",
    "paymentStatus": "confirming",
    "priceAmountCents": 10000,
    "priceCurrency": "usd",
    "payCurrency": null,
    "invoiceUrl": "https://nowpayments.io/payment/?iid=4522625843",
    "createdAt": "2026-09-08T12:00:00Z",
    "updatedAt": "2026-09-08T12:04:00Z",
    "metadata": {
      "order_id": "topup_org_8f2c…_crypto_2026-09-08_user_42_abc123",
      "pay_address": "TABB…xyz",
      "pay_amount": "0.0974",
      "expires_at": "2026-09-09T12:00:00Z"
    }
  },
  "meta": {
    "request_id": "req_crypto_status_001",
    "timestamp": "2026-09-08T12:04:00Z"
  }
}
```

Once the payer picks a coin on the hosted page, NOWPayments reports the
transfer details on its callback and the intent `metadata` carries them:
`order_id`, plus on-chain fields such as `pay_address`, `pay_amount`, and
`expires_at`, as the provider surfaces them — not every coin reports
every field. A `404` means the intent does not exist or belongs to a
different organization.

Stop polling when `paymentStatus` reaches a terminal state:

* Success terminal: `finished` — the wallet is credited; `refunded`.
* Retry terminal: `failed`, `expired`, `cancelled` — create a new intent to
  try again. See the state table linked below before retrying.

***

## 3. Cancel a pending top-up

`POST /api/v1/billing/crypto-topup/{intentId}/cancel` cancels an intent
while it is still unfunded. Cancellation succeeds only in `waiting` or
`confirming`; once the payment is `confirmed`, `sending`, `finished`,
`refunded`, `failed`, or already `expired`, the cancel is rejected and the
response carries the live status.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/billing/crypto-topup/cryptoIntent_2vXq7p9yzAbCdEf/cancel" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Success:

```json 200 theme={null}
{
  "data": {
    "intentId": "cryptoIntent_2vXq7p9yzAbCdEf",
    "paymentStatus": "cancelled",
    "cancelled": true
  },
  "meta": {
    "request_id": "req_crypto_cancel_001",
    "timestamp": "2026-09-08T12:05:00Z"
  }
}
```

Too late — the payment is already settling:

```json 409 theme={null}
{
  "error": {
    "code": "PAYMENT_IN_FLIGHT",
    "message": "Payment is being settled and can no longer be cancelled. If funds were sent, your balance will be credited automatically.",
    "details": { "current_status": "finished" }
  },
  "meta": {
    "request_id": "req_crypto_cancel_002",
    "timestamp": "2026-09-08T12:05:00Z"
  }
}
```

`details.current_status` always shows the live state (`finished`,
`confirmed`, `sending`, `refunded`, `failed`, `expired`, or `cancelled`),
so re-poll the status endpoint after a `409`. Cancellation is Orbit-side
only — the provider has no invoice-cancel API, so the receiving address
stays open — and a cancel never loses money: if funds arrive after you
cancel, the settlement callback overrides `cancelled` and credits your
wallet exactly once.

***

## 4. Status taxonomy

The full state machine — every status, its meaning, and which states allow
a cancel — is maintained on the guide:
[Status meanings](/billing/crypto-payments#status-meanings). The common
path is `waiting → confirming → confirmed → sending → finished`.

***

## 5. Webhook events on settlement

When the top-up reaches `finished` and the wallet is credited, Orbit
delivers `balance.topped_up` to your webhook endpoints, tagged
`payment_method: "crypto_nowpayments"` with `amount_cents` and `currency`
in the payload. The same event fires for Stripe (card) top-ups, so one
handler covers both funding sources. Subscribe to it under Dashboard →
Developers → Webhooks; the full billing event list is in the
[webhook event catalog](/webhooks/events).

Use the webhook instead of long-running polling: create the intent, show
the invoice, and treat `balance.topped_up` as the definitive receipt that
the wallet moved. Poll the status endpoint as a fallback when the event
has not arrived yet.

***

## 6. Sandbox behavior

Sandbox mode short-circuits messaging and voice sends before they touch a
carrier; money endpoints behave differently. Crypto top-ups run against the
real NOWPayments service — Orbit's staging integration points at the
NOWPayments sandbox environment (`api-sandbox.nowpayments.io`), where
invoices behave like the real thing without moving real funds.

In practice:

* There are **no magic numbers for crypto top-ups** — the
  [magic-number convention](/sandbox/magic-numbers) applies to message,
  voice, and number-purchase simulation, not to billing.
* On developer staging: exercise both lifecycle paths for free (create →
  poll, and create → cancel) against the NOWPayments sandbox environment.
* On production: cancel an unfunded intent (step 3) to test your lifecycle
  handling without spending funds. Never send test transfers on a live
  account — settled crypto is non-refundable without operator action.

***

## Related pages

* [Cryptocurrency payments](/billing/crypto-payments) — dashboard flow,
  status meanings, refunds, fee policy.
* [Crypto refunds](/billing/crypto-refunds) — operator-driven refund flow.
* [Webhook events](/webhooks/events) — the full billing event catalog.
* [Authentication](/authentication) — API key scopes and headers.
