API reference

Create an invoice

POST/v1/invoices

Contract v1 / 1.0.0 62 / 2026-10-10

On this page

Create a Payments invoice. Supply chain and amount for fixed payment details, or omit chain, asset, amount, address and tolerance and supply invoice_currency plus invoice_amount for a payer-selected method. The latter requires a live write key and Idempotency-Key; creation and an identical retry return the interactive checkout URL. GET/list retain read-only links. After a timeout, retry the same body with the same Idempotency-Key; different parameters return 409. A keyed retry returns 201 with the same ID. Without a key, a fixed-method retry can create another invoice. Does not create a blockchain payment.

Key permissions: invoices / write / payments.

Parameters

ParameterInRequiredDescription
Idempotency-KeyheaderNo16–80 ASCII letters, digits, dashes, or underscores. Unique logical order ID within the account. Required for a payer-selected payment method.
string minLength: 16; maxLength: 80; pattern: ^[A-Za-z0-9_-]+$

Request body

FieldTypeRequiredDescription
projectstringNoPayments project ID; explicit selection is recommended. A project-scoped key uses its project.
chainstringNoNetwork code.
minLength: 1
assetstringNoSupported payment asset: native, symbol, or known identifier. Accepted assets depend on the network; omission selects native.
Default: native
amountstringNoPositive crypto amount in asset units; precision must not exceed the asset's decimals.
pattern: ^[0-9]+(?:\.[0-9]+)?$
referencestringNoYour order reference; not a substitute for Idempotency-Key.
maxLength: 120
addressstringNoOptional address from the project's payment pool. Otherwise the service selects one.
tolerancestringNoAllowed underpayment in asset units, strictly less than amount. Omit for zero tolerance.
pattern: ^[0-9]+(?:\.[0-9]+)?$
customerobjectNo
productobjectNo
payerobjectNo
metadataobjectNoUp to 20 pairs, at most 8192 JSON bytes. Do not include secrets.
invoice_currencystringNoThree-letter order quote currency; only together with invoice_amount.
pattern: ^[A-Za-z]{3}$
invoice_amountstringNoPositive order quote in invoice_currency. With chain and amount this is the merchant snapshot. Without payment details it is the fixed invoice value used to quote the payer's selected asset.
maxLength: 80; pattern: ^[0-9]+(?:\.[0-9]+)?$
expires_inintegerNoPayment window in minutes, starting at successful issuance of details. Omission or 0 uses the project policy (60 by default); maximum 10080. Fixed-method legacy requests also treat negative values as omitted; payer-selected checkout rejects negative values.
Default: 60; maximum: 10080
application/json
{
  "project": "2002",
  "invoice_currency": "USD",
  "invoice_amount": "12.50",
  "reference": "order-42"
}

Request examples

Examples perform a real operation in your account. Set the environment variables named in the code. Keep your key private; write requests are not retried automatically.

Shell / cURL
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
  local text="$1" char code index output='"'
  for ((index=0; index<${#text}; index++)); do
    char="${text:index:1}"
    case "$char" in
      '"'|'\') output+="\\$char" ;;
      *) printf -v code '%d' "'$char"
         if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
         output+="$char" ;;
    esac
  done
  printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
  --silent --show-error --max-time 30 --request POST \
  --header "Authorization: Bearer $CRYPTANIO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
  --data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
  "https://api.cryptanio.com/v1/invoices" \
  --output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
  cat "$response_file"
  exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
Download example
Go
// Go; standard library only. Run: go run request.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "time"
)

func env(name string) string {
    value := os.Getenv(name)
    if value == "" { fmt.Fprintln(os.Stderr, "Set", name); os.Exit(1) }
    return value
}
func run() error {
    path := "/v1/invoices"

    endpoint, err := url.Parse("https://api.cryptanio.com" + path)
    if err != nil { return err }
    query := endpoint.Query()

    endpoint.RawQuery = query.Encode()
    var payload map[string]any
    if err := json.Unmarshal([]byte("{\"project\":\"2002\",\"invoice_currency\":\"USD\",\"invoice_amount\":\"12.50\",\"reference\":\"order-42\"}"), &payload); err != nil { return err }
    payload["project"] = env("CRYPTANIO_PROJECT_ID")
    body, err := json.Marshal(payload)
    if err != nil { return err }
    request, err := http.NewRequest("POST", endpoint.String(), bytes.NewReader(body))
    if err != nil { return err }
    request.Header.Set("Authorization", "Bearer " + env("CRYPTANIO_API_KEY"))
    request.Header.Set("Accept", "application/json")
    request.Header.Set("Content-Type", "application/json")
    request.Header.Set("Idempotency-Key", env("CRYPTANIO_OPERATION_ID"))
    client := &http.Client{Timeout: 30 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
    response, err := client.Do(request)
    if err != nil { return err }
    defer response.Body.Close()
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        body, err := io.ReadAll(response.Body)
        if err != nil { return err }
        return fmt.Errorf("HTTP %d: %s", response.StatusCode, body)
    }
    _, err = io.Copy(os.Stdout, response.Body)
    return err
}
func main() {
    if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
}
Download example
Rust
// Cargo.toml dependencies:
// reqwest = { version = "=0.12.24", features = ["blocking", "json"] }
// serde_json = "=1.0.145"
use std::{error::Error, io, time::Duration};
use reqwest::{blocking::Client, redirect::Policy, Method, Url};

fn env(name: &str) -> Result<String, Box<dyn Error>> {
    let value = std::env::var(name)?;
    if value.is_empty() { return Err(format!("Set {name}").into()); }
    Ok(value)
}
fn run() -> Result<(), Box<dyn Error>> {
    let mut url = Url::parse("https://api.cryptanio.com")?;
    {
        let mut segments = url.path_segments_mut().map_err(|_| "Invalid base URL")?;
        segments.clear();
        segments.push("v1");
        segments.push("invoices");
    }
    let client = Client::builder().timeout(Duration::from_secs(30)).redirect(Policy::none()).build()?;
    let request = client.request(Method::POST, url)
        .header("Authorization", format!("Bearer {}", env("CRYPTANIO_API_KEY")?))
        .header("Accept", "application/json".to_owned())
        .header("Content-Type", "application/json".to_owned())
        .header("Idempotency-Key", env("CRYPTANIO_OPERATION_ID")?);
    let mut body: serde_json::Value = serde_json::from_str("{\"project\":\"2002\",\"invoice_currency\":\"USD\",\"invoice_amount\":\"12.50\",\"reference\":\"order-42\"}")?;
    body["project"] = serde_json::Value::String(env("CRYPTANIO_PROJECT_ID")?);
    let request = request.json(&body);
    let mut response = request.send()?;
    let status = response.status();
    if !status.is_success() { return Err(format!("HTTP {}: {}", status.as_u16(), response.text()?).into()); }
    io::copy(&mut response, &mut io::stdout())?;
    Ok(())
}
fn main() {
    if let Err(error) = run() { eprintln!("{error}"); std::process::exit(1); }
}
Download example
PHP
<?php
// PHP 8 with the cURL extension. Run this on your server.
function env(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '') throw new RuntimeException('Set ' . $name);
    return $value;
}
try {
    $path = '/v1/invoices';

    $query = [

    ];
    $url = 'https://api.cryptanio.com' . $path . ($query ? '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986) : '');
    $headers = [
        'Authorization: ' . 'Bearer ' . env('CRYPTANIO_API_KEY'),
        'Accept: ' . 'application/json',
        'Content-Type: ' . 'application/json',
        'Idempotency-Key: ' . env('CRYPTANIO_OPERATION_ID'),
    ];
    $body = json_decode('{"project":"2002","invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}', true, 512, JSON_THROW_ON_ERROR);
    $body['project'] = env('CRYPTANIO_PROJECT_ID');
    $curl = curl_init($url);
    curl_setopt_array($curl, [
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
    ]);
    $response = curl_exec($curl);
    if ($response === false) throw new RuntimeException(curl_error($curl));
    $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status . ': ' . $response);
    echo $response;
} catch (Throwable $error) {
    fwrite(STDERR, $error->getMessage() . PHP_EOL);
    exit(1);
}
Download example
Python
# Python 3; standard library only.
import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import HTTPRedirectHandler, Request, build_opener

def env(name):
    result = os.environ.get(name)
    if not result:
        raise ValueError("Set " + name)
    return result

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

try:
    path = "/v1/invoices"

    query = {

    }
    url = "https://api.cryptanio.com" + path + ("?" + urlencode(query) if query else "")
    headers = {
        "Authorization": "Bearer " + env("CRYPTANIO_API_KEY"),
        "Accept": "application/json",
        "Content-Type": "application/json",
        "Idempotency-Key": env("CRYPTANIO_OPERATION_ID"),
    }
    body = json.loads("{\"project\":\"2002\",\"invoice_currency\":\"USD\",\"invoice_amount\":\"12.50\",\"reference\":\"order-42\"}")
    body["project"] = env("CRYPTANIO_PROJECT_ID")
    data = json.dumps(body).encode("utf-8")
    request = Request(url, data=data, headers=headers, method="POST")
    with build_opener(NoRedirect()).open(request, timeout=30) as response:
        if not 200 <= response.status < 300:
            raise ValueError("HTTP " + str(response.status))
        sys.stdout.buffer.write(response.read())
except HTTPError as error:
    print("HTTP " + str(error.code) + ": " + error.read().decode("utf-8", errors="replace"), file=sys.stderr)
    sys.exit(1)
except (ValueError, URLError, OSError) as error:
    print(str(error), file=sys.stderr)
    sys.exit(1)
Download example
Node.js
// Node.js 18+. Run on your server, never in a browser.
const env = name => {
  const result = process.env[name];
  if (!result) throw new Error(`Set ${name}`);
  return result;
};
try {
  let path = "/v1/invoices";

  const url = new URL(path, "https://api.cryptanio.com");

  const headers = {
    "Authorization": "Bearer " + env("CRYPTANIO_API_KEY"),
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Idempotency-Key": env("CRYPTANIO_OPERATION_ID"),
  };
  const body = {
    "project": "2002",
    "invoice_currency": "USD",
    "invoice_amount": "12.50",
    "reference": "order-42"
  };
  body["project"] = env("CRYPTANIO_PROJECT_ID");
  const response = await fetch(url, {
    method: "POST", headers, redirect: "manual",
    signal: AbortSignal.timeout(30_000), body: JSON.stringify(body),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  if (response.body) {
    for await (const chunk of response.body) process.stdout.write(chunk);
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}
Download example

Responses and errors

201 Successful response.
FieldTypeRequiredDescription
checkout_urlstringNoHosted Cryptanio payment page. Creation/retry of a payer-selected invoice returns an interactive capability that permits method selection until payment is detected. Fixed-method creation and every GET/list return a separate read-only link. Share either link only with the intended payer; neither grants account or API-key access. Confirm payment using the invoice API or a verified webhook, not a browser redirect.
uri
idstringYesOpaque identifier; keep it as a string.
minLength: 1
statusstringYespending awaits payment; seen detected; partial partially paid; paid settled; underpaid/overpaid/late need a decision; expired deadline passed; canceled voided.
pending, seen, paid, underpaid, partial, overpaid, expired, late, canceled
project_idstringYesOpaque identifier; keep it as a string.
minLength: 1
referencestringNoOrder reference.
customerobjectNo
productobjectNo
payerobjectNo
metadataobjectNoUp to 20 pairs, at most 8192 JSON bytes. Do not include secrets.
invoice_currencystringNoOrder quote currency.
invoice_amountstringNoExact decimal amount as a string; do not use floating point.
pattern: ^[0-9]+(?:\.[0-9]+)?$
chainstringYesNetwork code.
assetstringYesPayment asset.
addressstringYesPayment address.
payment_routeobjectNoRequired on-chain discriminator when an automatic source issues a tag, memo, TON comment or Solana Pay reference. Keep it with the receiving address; the merchant order reference is a separate field.
payment_uristringNoSolana Pay transfer request with the recipient, exact amount, supported token mint and mandatory reference. Encode this URI in the payment QR instead of the plain receiving address.
uri
amountstring | stringYesActual amount the buyer must send; may differ from asked to distinguish invoices. Empty until a method is selected.
amount_rawstring | stringYesInteger smallest-unit amount as a string. Empty until a method is selected.
askedstring | stringYesOriginally requested crypto amount. Empty until a method is selected.
decimalsintegerYesAsset decimal places.
minimum: 0
tolerancestringNoTolerance in smallest units; unlike request.tolerance.
pattern: ^[0-9]+$
sandboxbooleanYesA test invoice does not prove a real blockchain payment.
needs_decisionbooleanNoA decision is required for underpaid, overpaid, or late.
paidstringNoReceived amount in smallest units.
pattern: ^[0-9]+$
tx_idstringNoPayment transaction identifier.
paid_atstringNoUTC date and time in RFC 3339.
date-time
expires_atstringYesUTC date and time in RFC 3339.
date-time
created_atstringYesUTC date and time in RFC 3339.
date-time
201 application/json
{
  "id": "2100",
  "status": "pending",
  "project_id": "2002",
  "reference": "order-42",
  "chain": "ethereum",
  "asset": "USDC",
  "address": "0x52908400098527886E0F7030069857D2E4169EE7",
  "amount": "12.5",
  "amount_raw": "12500000",
  "asked": "12.5",
  "decimals": 6,
  "sandbox": true,
  "expires_at": "2026-10-06T11:00:00Z",
  "created_at": "2026-10-06T10:00:00Z"
}
400 Invalid request (bad_request, unsupported_asset).
FieldTypeRequiredDescription
errorobjectYes
400 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
401 No valid active API key (unauthorized).
FieldTypeRequiredDescription
errorobjectYes
401 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
403 Key disallows the operation, project, service or IP; email unconfirmed.
FieldTypeRequiredDescription
errorobjectYes
403 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
409 State conflict, conflicting retry parameters, or payment resource limit (conflict, limit_reached, service_inactive).
FieldTypeRequiredDescription
errorobjectYes
409 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
429 Too many requests (rate_limited); retry after Retry-After.
FieldTypeRequiredDescription
errorobjectYes
429 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
500 Internal error (internal).
FieldTypeRequiredDescription
errorobjectYes
500 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}
503 Temporary unavailability (unavailable); retry with backoff.
FieldTypeRequiredDescription
errorobjectYes
503 application/json
{
  "error": {
    "code": "forbidden",
    "message": "this API key is read-only"
  }
}

Handling errors and retrying safely

Nested objects

Invoice

FieldTypeRequiredDescription
checkout_urlstringNoHosted Cryptanio payment page. Creation/retry of a payer-selected invoice returns an interactive capability that permits method selection until payment is detected. Fixed-method creation and every GET/list return a separate read-only link. Share either link only with the intended payer; neither grants account or API-key access. Confirm payment using the invoice API or a verified webhook, not a browser redirect.
uri
idstringYesOpaque identifier; keep it as a string.
minLength: 1
statusstringYespending awaits payment; seen detected; partial partially paid; paid settled; underpaid/overpaid/late need a decision; expired deadline passed; canceled voided.
pending, seen, paid, underpaid, partial, overpaid, expired, late, canceled
project_idstringYesOpaque identifier; keep it as a string.
minLength: 1
referencestringNoOrder reference.
customerobjectNo
productobjectNo
payerobjectNo
metadataobjectNoUp to 20 pairs, at most 8192 JSON bytes. Do not include secrets.
invoice_currencystringNoOrder quote currency.
invoice_amountstringNoExact decimal amount as a string; do not use floating point.
pattern: ^[0-9]+(?:\.[0-9]+)?$
chainstringYesNetwork code.
assetstringYesPayment asset.
addressstringYesPayment address.
payment_routeobjectNoRequired on-chain discriminator when an automatic source issues a tag, memo, TON comment or Solana Pay reference. Keep it with the receiving address; the merchant order reference is a separate field.
payment_uristringNoSolana Pay transfer request with the recipient, exact amount, supported token mint and mandatory reference. Encode this URI in the payment QR instead of the plain receiving address.
uri
amountstring | stringYesActual amount the buyer must send; may differ from asked to distinguish invoices. Empty until a method is selected.
amount_rawstring | stringYesInteger smallest-unit amount as a string. Empty until a method is selected.
askedstring | stringYesOriginally requested crypto amount. Empty until a method is selected.
decimalsintegerYesAsset decimal places.
minimum: 0
tolerancestringNoTolerance in smallest units; unlike request.tolerance.
pattern: ^[0-9]+$
sandboxbooleanYesA test invoice does not prove a real blockchain payment.
needs_decisionbooleanNoA decision is required for underpaid, overpaid, or late.
paidstringNoReceived amount in smallest units.
pattern: ^[0-9]+$
tx_idstringNoPayment transaction identifier.
paid_atstringNoUTC date and time in RFC 3339.
date-time
expires_atstringYesUTC date and time in RFC 3339.
date-time
created_atstringYesUTC date and time in RFC 3339.
date-time
ID

Opaque identifier. Its format is not guaranteed; do not convert to a number.

string Opaque identifier. Its format is not guaranteed; do not convert to a number.

InvoiceStatus

pending awaits payment; seen detected; partial partially paid; paid settled; underpaid/overpaid/late need a decision; expired deadline passed; canceled voided.

string pending awaits payment; seen detected; partial partially paid; paid settled; underpaid/overpaid/late need a decision; expired deadline passed; canceled voided.

Customer

FieldTypeRequiredDescription
idstringYesCustomer identifier in your system.
maxLength: 120
Product

FieldTypeRequiredDescription
idstringNoProduct identifier in your system.
maxLength: 120
namestringNoProduct name.
maxLength: 200
quantityintegerNoQuantity.
minimum: 1; maximum: 1000000
Payer

FieldTypeRequiredDescription
commentstringYesPayer comment.
maxLength: 500
InvoiceMetadata

Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets.

object Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets.

Decimal

string

RawAmount

string

InvoiceRequest

FieldTypeRequiredDescription
projectstringNoPayments project ID; explicit selection is recommended. A project-scoped key uses its project.
chainstringNoNetwork code.
minLength: 1
assetstringNoSupported payment asset: native, symbol, or known identifier. Accepted assets depend on the network; omission selects native.
Default: native
amountstringNoPositive crypto amount in asset units; precision must not exceed the asset's decimals.
pattern: ^[0-9]+(?:\.[0-9]+)?$
referencestringNoYour order reference; not a substitute for Idempotency-Key.
maxLength: 120
addressstringNoOptional address from the project's payment pool. Otherwise the service selects one.
tolerancestringNoAllowed underpayment in asset units, strictly less than amount. Omit for zero tolerance.
pattern: ^[0-9]+(?:\.[0-9]+)?$
customerobjectNo
productobjectNo
payerobjectNo
metadataobjectNoUp to 20 pairs, at most 8192 JSON bytes. Do not include secrets.
invoice_currencystringNoThree-letter order quote currency; only together with invoice_amount.
pattern: ^[A-Za-z]{3}$
invoice_amountstringNoPositive order quote in invoice_currency. With chain and amount this is the merchant snapshot. Without payment details it is the fixed invoice value used to quote the payer's selected asset.
maxLength: 80; pattern: ^[0-9]+(?:\.[0-9]+)?$
expires_inintegerNoPayment window in minutes, starting at successful issuance of details. Omission or 0 uses the project policy (60 by default); maximum 10080. Fixed-method legacy requests also treat negative values as omitted; payer-selected checkout rejects negative values.
Default: 60; maximum: 10080
Error

FieldTypeRequiredDescription
errorobjectYes