Payments

Authentication

API keys, their environment (Sandbox or Live) and permissions, Sandbox and Live in requests, the Idempotency-Key header, the error format, pagination and rate limits.

Last updated Oct 27, 2026

The API lives at https://api.aliquot.cc. Every merchant endpoint is under /api/v1/crypto and authenticates with an API key. So does webhook management under /api/v1/organizations/{organizationId}/webhooks (see Webhooks). The buyer-facing endpoints under /api/v1/public (checkout, payment link pages, refund claims) take no authentication: the invoice ID, link slug or claim token in the path is the credential.

Requests and responses are JSON, with snake_case field names.

API keys

Create keys in the dashboard under Developers → API keys. Give the key a name, pick its environment (Sandbox or Live, see below) and its permissions and, optionally, an expiry date. The secret is shown once, when the key is created; Aliquot stores only a hash of it, so a lost secret cannot be recovered. Create a new key and revoke the old one instead.

A key belongs to your organization and acts only on its data. You cannot give a key a permission that your own role in the organization does not have.

Revoke a key from the same page. A revoked or expired key is refused immediately.

Keys and environments

Each key is bound to one environment, chosen when you create it. Its prefix says which:

PrefixEnvironmentAccepts
aq_sandbox_SandboxOnly mode: "test" and test networks
aq_live_LiveOnly mode: "live" and mainnets

In the dashboard, the key form asks for the environment (Sandbox by default). Through the API, send "environment": "sandbox" or "environment": "live" to POST /api/v1/organizations/{organizationId}/api-keys; it defaults to sandbox, or to the environment of the calling key when the request is made with a Sandbox or Live key. A key cannot create a key for the other environment: that answers 403 API_KEY_ENVIRONMENT_MISMATCH.

Every request under /api/v1/crypto made with the key stays in its environment:

  • A mode query parameter you leave out takes the key's environment: a Sandbox key lists Sandbox invoices without ?mode=test.
  • An invoice created without network or mode is created in the key's environment.
  • A mode, network or payment-link accepted_pairs network of the other environment answers 403 API_KEY_ENVIRONMENT_MISMATCH, and nothing is created.
  • An invoice, payment link or payout of the other environment, read, voided or edited by ID, answers 404 NOT_FOUND, as if it did not exist.

Data never crosses environments: a Sandbox invoice is never listed in Live, and Sandbox balances can't be paid out to a mainnet.

Legacy keys. Keys created before environments existed start with fk_live_ and are bound to neither: they work in both, and each request picks its environment as described in Sandbox and Live in requests (lists default to live). The dashboard lists them as "Legacy · both environments". Replace them with a Sandbox or Live key.

Sending the key

Send the secret as a bearer token in the Authorization header:

curl "https://api.aliquot.cc/api/v1/crypto/invoices?mode=test" \
  -H "Authorization: Bearer $ALIQUOT_API_KEY"

A missing key answers 401 UNAUTHORIZED. A key that is invalid, revoked or expired answers 401 UNAUTHORIZED with the message Invalid or revoked API key. A valid key without the permission an endpoint needs answers 403 FORBIDDEN with Missing permission: <permission>.

Requests made with API keys are recorded in the request log: Developers → Logs in the dashboard, or GET /api/v1/crypto/requests. Each entry has the method, path, status, duration, the Idempotency-Key if any, and whether the response was a replay.

Permissions

EndpointPermission
POST /api/v1/crypto/invoicescrypto-invoices:create
GET /api/v1/crypto/invoices, GET /api/v1/crypto/invoices/{id}crypto-invoices:read
POST /api/v1/crypto/invoices/{id}/voidcrypto-invoices:update
POST /api/v1/crypto/quotecrypto-invoices:read
GET /api/v1/crypto/networkscrypto-invoices:read
GET /api/v1/crypto/refundscrypto-invoices:read
GET /api/v1/crypto/overviewcrypto-invoices:read
GET /api/v1/crypto/requestscrypto-invoices:read
POST /api/v1/crypto/payment-linkscrypto-links:create
GET /api/v1/crypto/payment-links, GET /api/v1/crypto/payment-links/{id}crypto-links:read
PATCH /api/v1/crypto/payment-links/{id}crypto-links:update
POST /api/v1/crypto/accountscrypto-accounts:create
GET /api/v1/crypto/accounts, GET /api/v1/crypto/accounts/{id}crypto-accounts:read
GET /api/v1/crypto/balances, GET /api/v1/crypto/ledger/transactions, GET /api/v1/crypto/ledger/accountscrypto-ledger:read
POST /api/v1/crypto/payoutscrypto-payouts:create
GET /api/v1/crypto/payouts, GET /api/v1/crypto/payouts/{id}crypto-payouts:read
/api/v1/organizations/{organizationId}/webhooks/… (endpoints, deliveries, resend)webhooks:manage

The key form in the dashboard offers every permission in this table, grouped by resource, each with a one-line description. crypto-invoices:create, crypto-invoices:read, crypto-links:create, crypto-links:read, crypto-accounts:read and crypto-ledger:read are preselected; crypto-invoices:update, crypto-links:update, crypto-accounts:create, crypto-payouts:read, crypto-payouts:create and webhooks:manage are opt-in.

Keys that can pay out

A key with crypto-payouts:create moves money out of your balances without a per-payout code. Creating one is therefore stepped up: you need two-factor authentication enabled on your user (Settings → Security) and a current code from your authenticator app when you create the key: in the dashboard, picking crypto-payouts:create shows a field for it; through the API, send it as totp_code. Without 2FA the request answers 422 TWO_FACTOR_NOT_ENABLED; without a valid code, 403 STEP_UP_REQUIRED. A key that was not created this way answers 403 STEP_UP_REQUIRED on POST /api/v1/crypto/payouts. See Payouts.

Only crypto-payouts:create makes a key payout-capable. Permissions match exactly, so crypto-payouts:manage does not include crypto-payouts:create or crypto-payouts:read and grants no payout endpoint.

Sandbox and Live in requests

Sandbox (mode: "test" in the API) and Live are separated by network, not by endpoint:

  • Creating an invoice: the network you send decides the environment (BASE_SEPOLIA is Sandbox, BASE_MAINNET is Live). If you let the buyer choose the network, the invoice takes your key's environment; with a legacy key, send "mode": "test" or "mode": "live". A network and a mode that disagree are a 422 (with a Sandbox or Live key, the one that does not match the key is a 403 first).
  • Payment links take their environment from their accepted_pairs, and payouts from their network.
  • Reading: list endpoints take a mode query parameter, which defaults to your key's environment (live with a legacy fk_live_ key). With a legacy key, add mode=test to see Sandbox invoices, payouts, refunds, payment links, balances and ledger entries.

Every invoice, payout and payment link carries livemode: false in Sandbox, true in Live.

Idempotency

POST /api/v1/crypto/invoices and POST /api/v1/crypto/payouts require an Idempotency-Key header (1–255 characters). Use a value that identifies the operation in your system, such as an order or withdrawal ID, and reuse it when you retry.

SituationResponse
No header, or longer than 255 characters400 IDEMPOTENCY_KEY_REQUIRED
First request with the keyThe normal response (201)
Same key, same request, first one succeededThe stored response, with status 200 and the header Idempotent-Replay: true
Same key, different method, path or body409 IDEMPOTENCY_KEY_REUSED
Same key while the first request is still running409 IDEMPOTENCY_IN_PROGRESS; retry shortly

Keys are scoped to your organization. Two requests count as the same when the method, path and JSON body match; the order of keys inside the body does not matter. Only successful (2xx) responses are stored: if the first request fails, the key is released and you can correct the request and send it again with the same key.

Errors

Errors share one shape:

{
  "code": "VALIDATION_FAILED",
  "message": "mode (test | live) is required when no network is given",
  "issues": [{ "path": "mode", "message": "mode (test | live) is required when no network is given" }]
}

code is stable and meant for your code; message is for people. issues is present on validation errors and points at the field (path uses dots for nesting, such as splits.0.percent).

StatuscodeWhen
400BAD_REQUESTThe body is not valid JSON.
400IDEMPOTENCY_KEY_REQUIREDIdempotency-Key is missing on an endpoint that needs it.
401UNAUTHORIZEDNo key, or an invalid, revoked or expired key.
403FORBIDDENThe key lacks the permission, or the action is not allowed.
403STEP_UP_REQUIREDThe key was not authorized for payouts when it was created.
403API_KEY_ENVIRONMENT_MISMATCHA Sandbox key used for Live (a mode, network or new key of Live), or a Live key for the Sandbox. See Keys and environments.
404NOT_FOUNDThe object does not exist in your organization (or, with a Sandbox or Live key, belongs to the other environment).
409CONFLICTThe object is in a state that does not allow the action (for example voiding a detected invoice), or a reference is already taken.
409IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESSSee Idempotency.
409INSUFFICIENT_BALANCEA payout exceeds the available balance. Nothing was created.
413PAYLOAD_TOO_LARGEThe body is larger than 1 MB.
422VALIDATION_FAILEDA field is missing or invalid; see issues.
422TWO_FACTOR_NOT_ENABLEDCreating a payout-capable key without 2FA.
429RATE_LIMITEDToo many requests; see below.
429STEP_UP_LOCKEDToo many failed two-factor codes; wait 15 minutes.
500INTERNAL_ERRORAn unexpected error on Aliquot's side.
503CRYPTO_NOT_CONFIGUREDPayments are unavailable on the server.

Handle codes you do not recognize by their HTTP status.

Rate limits

Requests are counted per minute, separately for reads (GET) and writes (POST, PATCH, DELETE). The defaults are 300 reads and 60 writes per minute. Requests made with an API key count against the user who created the key; the public checkout endpoints count per IP address.

Responses carry the standard RateLimit and RateLimit-Policy headers with the limit, what remains and when the window resets. Over the limit, the API answers 429 with:

{ "code": "RATE_LIMITED", "message": "Too many requests. Try again later." }

Pagination

GET on invoices, payouts, refunds, ledger transactions and the request log returns a page:

{ "items": [], "next_cursor": "inv_01JB7Q8M3X4K9TPZ2R6WVN5HDC" }

Pass next_cursor back as cursor to get the next page; it is null on the last page. limit is 1–100 (default 25; 50 for the request log). Items are newest first. The other list endpoints (networks, accounts, payment links, balances, ledger accounts) return { "items": [...] } in one response.

Amounts and IDs

  • Amounts are decimal strings, never numbers. USD prices have at most two decimals ("100", "100.5", "100.00"); amounts in tokens are exact, with at least two decimals ("102.50", "76.875").
  • Percentages (markup_percent, splits[].percent) are JSON numbers with at most two decimals (2.5 is 2.5 %).
  • Object IDs carry a prefix: inv_ invoices, acct_ accounts, po_ payouts, rfd_ refunds, plink_ payment links, ltx_ ledger transactions. They sort by creation time.
  • Timestamps are ISO 8601 in UTC.