# CyclePay > Fiat-and-stablecoin gateway for the Internet Computer. Buy ICP cycles with a card (Stripe), USDC on Base (x402), or ck-USDC on ICP (ICRC-2). Cycles land on a target canister via the CMC, or on a Cycles Ledger account (destination_type "ledger": the buyer's own, or on card orders the one named by beneficiary_principal), minus 0.0002T in Cycles Ledger fees. Live at (also, always, at , the canister's own URL; both give a user the same Internet Identity principal). Backend canister: `2bwus-iiaaa-aaaac-bee3a-cai`. The full HTML docs at `/docs` and `/api` are the canonical reference; this file is a machine-readable index for AI agents and CLI tooling. ## API - [API Reference](/api): Bearer-auth REST endpoints over HTTPS. API base URL is `https://backend-production-9bca7.up.railway.app` (e.g. `POST https://backend-production-9bca7.up.railway.app/v1/topup`). The frontend canister at `4w3yu-fyaaa-aaaac-beeoa-cai.icp0.io` serves only the static site. It does not proxy `/api` or `/v1` routes: a `/v1/*` request there returns the site's HTML app shell, not JSON. An HTML response means you called the wrong host. On the API host, an unknown route answers 404 `{"error":"Not found"}`. Every 429 carries a `Retry-After` header in seconds. - [CLI flow for agents](/docs#cli-quick-start): Sign in from the terminal with `icp-cli` and Internet Identity. No released icp-cli command buys from CyclePay yet, so finish purchases on this site. - [Documentation](/docs): Architecture, fund flow, security model, canister IDs, and pricing. - Buy-page links for terminal tools: `https://www.cyclepay.org/?dest=ledger&to=&usd=<1..200>&from=icp-cli` opens the Buy page with that principal's Cycles Ledger account and the amount preselected, and `?dest=canister&to=&usd=<1..200>&from=icp-cli` does the same for a canister. Short forms for QR codes, in any case: `/b//` and `/c//` (e.g. `HTTPS://WWW.CYCLEPAY.ORG/C//10`; the canister URL takes the same paths). The user pays by card, without a sign-in for $5 to $100 while guest checkout is on; the page then says to return to the terminal. ## Endpoints - [POST /v1/checkout](/api#post-v1-checkout): Create a Stripe Checkout session. Returns `checkout_url` (https://checkout.stripe.com/...) for the user to complete payment. Body: `{ destination_type?: "canister"|"ledger", canister_id?: string, beneficiary_principal?: string, amount_usd: number, confirm_system_canister?: boolean, project_id?: string }` where `canister_id` is required for "canister", `beneficiary_principal` (ledger only) names the principal whose Cycles Ledger account is credited (default: the signed-in buyer), `amount_usd` is 1 to 200 USD with at most 2 decimals, and `confirm_system_canister: true` proceeds past the 400 `SYSTEM_CANISTER_WARNING` that a known system canister gets, and `project_id` assigns the order to a project you are a member of (otherwise 403 `Not a member of this project`). A canister_id whose checksum is wrong gets 400 `INVALID_CANISTER_ID` ("That canister ID has a typo (bad checksum)") on every purchase route and on `/v1/subscribe`. The card fee is 2.9% rounded half up to the cent, plus $0.30; the response's `stripe_fee_usd` and `net_converted_usd` are what delivery uses too. Ledger orders deliver 0.0002T less than they mint. Bearer auth required. - [POST /v1/topup](/api#post-v1-topup): Agent-friendly alias for `/v1/checkout` with identical request and response shape (including `beneficiary_principal`). Use this from CLI tools, AI agents, and scripts so call-site intent is obvious in logs. - [POST /v1/x402/topup](/api#post-v1-x402-topup): USDC on Base via the x402 v2 protocol over its standard HTTP transport, so standard x402 v2 clients work with it (verified with the client in `@x402/core`). First call without a payment returns `402` with the payment requirements base64-encoded in the `PAYMENT-REQUIRED` header and as JSON in the body (atomic USDC `amount`, `payTo`, `asset`, network `eip155:8453`, the EIP-712 domain in `extra`, and `resource: { url, description, mimeType }`). Caller signs an EIP-3009 `transferWithAuthorization`, sends it base64-encoded in the `PAYMENT-SIGNATURE` header (legacy `X-PAYMENT` also accepted), and POSTs again; the settlement receipt comes back in `PAYMENT-RESPONSE`. x402 clients cap each payment at $1 by default: to pay more than $1, raise your client's `spendControls.maxAmountPerPayment` (for example to `"$200"`). Only scheme `exact` with an EIP-3009 authorization is accepted (not a Permit2 payload), and the paid repost must carry the same JSON body. A payment the facilitator rejects answers 402 `payment_invalid` with `PAYMENT-REQUIRED` again (the reason in its `error`); a declined settle answers 402 `settle_failed` with `PAYMENT-RESPONSE` `{ success: false, errorReason }`. A retry of a payment the server already has answers with its status, without re-verifying it or counting against the per-payer rate limit, but only for its own principal (a ledger payment needs that principal's Bearer token; a caller signed in as another principal gets 403 `forbidden`): 409 `duplicate_payment` (delivered), `duplicate_payment_failed`, `duplicate_payment_refunded`, `payment_failed` or `payment_in_progress` (still being processed: wait and retry), or 202 while delivering. A resumed payment always goes to its stored destination; the retry's body cannot change it. Exception: a retry that takes over a payment still `pending` (not settled) after its first request went quiet re-runs `/verify` and the rate limit like a new payment before settling it. The per-payer 429 `rate_limited` carries `Retry-After` set to the seconds until that payer's 24-hour window frees up. Cycles are paid from CyclePay's backend canister ICP balance immediately on settle. Body: `{ destination_type?: "canister"|"ledger", canister_id?: string, amount_usd_cents: integer, confirm_system_canister?: boolean }` where `amount_usd_cents` is 100 to 20000 ($1 to $200 per order). Bearer (II delegation) is optional for canister destinations (send it to tie the order to your principal) and required for "ledger", which credits that principal; an `Authorization` header that is not a valid delegation is refused with `401 auth_invalid`; `beneficiary_principal` is not accepted. A payment whose ICP already moved while delivery is still finishing answers 202 `{ success: true, status: "delivering", code: "delivery_in_progress", order_id, message }`: the cycles arrive automatically (or the payment is refunded); do not pay again. - [POST /v1/ckusdc/topup](/api#post-v1-ckusdc-topup): Pay with ck-USDC on ICP. The customer signs in with Internet Identity, then, as that same II principal, signs an ICRC-2 `approve` on the ck-USDC ledger `xevnm-gaaaa-aaaar-qafnq-cai` for `amount + fee` (fee 10_000 atoms = 0.01 ck-USDC) with CyclePay's hot wallet `nsp5q-aelxk-xax2u-3sqxb-xeod6-fduhf-zmoir-qxpp3-xusb4-nez2q-5ae` as the spender, then POSTs here with that II Bearer token. Plug, OISY, NNS dapp and Stoic are not supported today: approve from the same Internet Identity principal that signs the Bearer token. The server pulls funds via `icrc2_transfer_from` and fulfils cycles via the same canister path Stripe and x402 use. Synchronous: cycles land in the response, no checkout URL. Body: `{ destination_type?: "canister"|"ledger", canister_id?: string, amount_usdc_atomic: string|number, request_id?: string, confirm_system_canister?: boolean, project_id?: string }` ("ledger" credits the signed-in principal; `beneficiary_principal` is not accepted) where `amount_usdc_atomic` is 6-decimal ck-USDC atoms (10_000_000 = 10.00 USDC; $1 to $200 per order; must be a multiple of 10_000 atoms, $0.01, or it is rejected with 400) and `request_id` is a UUID in 8-4-4-4-12 form (anything else is rejected with 400), required for safe retries: generate a fresh one per purchase and send the same one on every retry of it (without it the server keys the payment on principal, amount, destination and a 10-second window, so a later retry counts as a new purchase). `project_id` assigns the payment to a project you are a member of (otherwise 403 `not_project_member`, before any pull). Bearer (II delegation) required; the principal pulled-from is the cryptographically verified delegation principal, never trusted from the body. A payment whose ICP already moved while delivery is still finishing answers 202 `{ success: true, status: "delivering", code: "delivery_in_progress", order_id, message }`: the cycles arrive automatically (or the payment is refunded); do not pay again. A retry can also get 409 `payment_in_progress` (another request is processing the same payment: wait and retry), 409 `payment_failed` (delivery failed, refund under way) or 409 `duplicate_payment_refunded`; 503 `canister_unavailable` and 500 `db_error` are transient. - [GET /v1/ckusdc/balance](/api#get-v1-ckusdc-balance): Returns the authed principal's ck-USDC balance. Useful for showing "you have $X" before the user picks an amount. Mirrors `/v1/ledger-balance` for the ck-USDC ledger. - [GET /v1/order-status/:id?token=](/api#get-v1-order-status): A card order's `status`, `amount_usd`, `destination` (`{ type, id }`, id shortened), `cycles_deposited`, `detail` (why a held or failed order is not delivered: `delivery_pending`, `delivery_unconfirmed`, `needs_review`, `refund_retrying`, `not_paid`, `amount_mismatch`, `guest_cap_exceeded`, else null), `created_at` and `updated_at`, with no sign-in, for the holder of its `view_token` (returned by `/v1/checkout`, `/v1/topup` and `/v1/guest-checkout`, and in the Stripe success URL as `view_token`). 404 for a wrong or missing token. Nothing about the buyer or the payment. - [GET /v1/guest-checkout](/api#get-v1-guest-checkout): `{ available: true, min_usd, max_usd }` while card checkout without a sign-in is on; 404 while it is off, and while the rail switches refuse card orders. No auth. - [POST /v1/guest-checkout](/api#post-v1-guest-checkout): Card checkout without a sign-in, for any canister or a named Cycles Ledger account; 404 while off. Body: `{ destination_type?: "canister"|"ledger", canister_id?: string, beneficiary_principal?: string, amount_usd: number, confirm_system_canister?: boolean }` where `beneficiary_principal` is required for "ledger" (400 `BENEFICIARY_REQUIRED`) and `amount_usd` is $5 to the guest cap (default $100; less answers 400 `GUEST_MINIMUM_NOT_MET` with `min_guest_usd`, more 400 `GUEST_LIMIT_EXCEEDED` with `max_guest_usd`: sign in and use `/v1/checkout`; under $1, over $200 or not a number, 400 with no code). Every destination check of `/v1/checkout` applies. The destination checks of `/v1/checkout` (400 `INVALID_CANISTER_ID` for a typo); no `project_id` (400 `PROJECT_NEEDS_SIGN_IN`); the same rail switches (503 `maintenance`, 410 `card_rail_moved`). No Stripe customer is created, the card is not saved, and 3-D Secure is attempted on every card that supports it; an unpaid session expires after 31 minutes. Returns the `/v1/checkout` fields, `view_token` included (read the order with `GET /v1/order-status/:id`), plus `min_guest_usd`, `max_guest_usd` and `expires_at`. Limits: 5 sessions per IP per hour (IPv6: per /64) and 60 in total per minute, paid or not; at most 10 guest orders delivered per destination per day, 100 in total per hour and $1,000 in total per day, counted by payment time and enforced when the payment is claimed (an order paid over a cap is refunded, not delivered: status `refund_due` until Stripe confirms, `detail` `guest_cap_exceeded`); other buyers' unpaid sessions never count against a destination and share only the per-minute ceiling (`limit: sessions`: sign in to pay, or retry in a minute). Over a limit at creation: 429 `GUEST_RATE_LIMITED` with `limit` (`ip`, `sessions`, `destination`, `global`, `daily_dollars`) and `Retry-After`. No auth. - [GET /v1/order/:id](/api#get-v1-order): Reconcile order status. Returns local DB state and authoritative on-chain canister state. Bearer auth required; only the order's principal can read it. - [GET /v1/history](/api#get-v1-history): Past orders for the authenticated principal. Ledger orders carry `destination: { type: "ledger", beneficiary_principal }`. Rows carry `amount_refunded_cents` (card orders: cents refunded so far; 0 on the crypto rails) and `error` (card orders: why it failed or what it waits on; null on the crypto rails). - [GET /v1/console/stats](/api#get-v1-console-stats): Card-order totals for the authenticated principal (`GET /v1/projects/:id/stats`, members only, covers the project's orders and subscriptions, every member's, plus your own that belong to no project): `month_delivered_usd` (delivered this month, net of partial refunds; `month_spent_usd` is the same) and `month_spent_toward_cap_usd` (every charged, unrefunded card order of yours this month in any project, what each subscription's `max_monthly_usd` is checked against). - [GET /v1/ledger-balance](/api#get-v1-ledger-balance): Cycles Ledger balance for the authenticated principal. Reads `icrc1_balance_of` on `um5iw-rqaaa-aaaaq-qaaba-cai`. - [POST /v1/subscribe](/api#post-v1-subscribe) / `PUT /v1/subscribe/:id` / `DELETE /v1/subscribe/:id` / `GET /v1/subscriptions`: Auto-refill configuration for a canister or for your own Cycles Ledger account (`destination_type: "ledger"`). A canister subscription requires CyclePay's backend canister `2bwus-iiaaa-aaaac-bee3a-cai` as a controller of the canister (the worker reads the balance through it). `GET /v1/subscriptions` and `PUT /v1/subscribe/:id` return exactly the documented fields plus `action_needed` (`"authentication_required"` when the saved card needs authenticating before auto-refill can charge again, otherwise null). - Maintenance and moved rails: `503 { error, code: "maintenance" }` with `Retry-After: 300` pauses new orders on every rail and console changes for a short window. `410` with `code: "card_rail_moved"` (new card payments), `"ckusdc_rail_moved"` (`/v1/ckusdc/topup`) or `"console_moved"` (every console route) means that part moved on-chain; the body carries `buy_url` or `console_url`. x402 and ck-USDC can also answer `202 { success: true, status: "under_review", code: "needs_review", order_id, message }`: the payment arrived and is held for a manual check; do not pay again. ## Authentication All authenticated endpoints expect `Authorization: Bearer ` where the body is the JSON form of an Internet Identity `DelegationChain`. The server verifies every hop in the chain (Ed25519 / secp256k1 / canister signatures), enforces optional `targets` scope, and only then derives the principal. A bare principal sent as the token is rejected with 401 `auth_invalid`, a delegation from a read-only Internet Identity session with 401 ("Read-only Internet Identity sessions cannot use this API"), and a token over 8 KiB on every route with 400 `token_too_large`. Token = `btoa(JSON.stringify(chain.toJSON()))` where `chain = (await authClient.getIdentity()).getDelegation()`. A token is valid for up to five minutes (Internet Identity renews the delegation as it ages), so build one per request. ### From a terminal (icp-cli) `icp identity link web` (icp-cli 0.3.0 and later) signs you in with Internet Identity and stores the delegation locally. Pass `--app` so the terminal gets the same principal the CyclePay website shows you. First enable CLI access in your identity settings on https://id.ai (it is off by default, per browser). ```sh icp identity link web cyclepay --app 4w3yu-fyaaa-aaaac-beeoa-cai.icp0.io # Check the principal: it should match the one shown on the CyclePay site. icp canister call --query --identity cyclepay -n ic ivcos-eqaaa-aaaab-qablq-cai whoami '()' # When the delegation expires, sign in again as the same identity: icp identity reauth cyclepay ``` No released icp-cli command buys from CyclePay yet, so finish purchases on this site. Card payment happens in the browser either way. ### From a browser (AuthClient) Browser calls to the API are accepted only from the CyclePay site itself (CORS), and Internet Identity gives every origin its own principal, so a delegation minted on another site is a different user. ```js import { AuthClient } from '@icp-sdk/auth/client'; const authClient = new AuthClient(); // mainnet Internet Identity (id.ai) await authClient.signIn({ maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7-day session }); // Right before each request: the delegation lasts five minutes. const identity = await authClient.getIdentity(); await identity.refresh(); // mints a new delegation if this one is about to lapse const token = btoa(JSON.stringify(identity.getDelegation().toJSON())); ``` ## Optional - [GET /v1/rate](/api#get-v1-rate): Live ICP/USD/XDR rate plus cycles-per-USD. ICP/XDR from the CMC, ICP/USD from CoinGecko spot, cached 5 minutes. `xdr_usd` is the CyclePay canister's XRC-refreshed XDR/USD when fresh (`xdr_usd_source: "canister-xrc"`), else derived (`"derived"`). No auth required. - [GET /health](/api#get-health): Service health probe. ## Canonical canister IDs - CyclePay backend: `2bwus-iiaaa-aaaac-bee3a-cai` - CyclePay frontend (this site): `4w3yu-fyaaa-aaaac-beeoa-cai` - Cycles Minting Canister: `rkp4c-7iaaa-aaaaa-aaaca-cai` - ICP Ledger: `ryjl3-tyaaa-aaaaa-aaaba-cai` - Cycles Ledger: `um5iw-rqaaa-aaaaq-qaaba-cai` - Internet Identity: `rdmx6-jaaaa-aaaaa-aaadq-cai`