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
| Parameter | In | Required | Description |
|---|---|---|---|
Idempotency-Key | header | No | 16–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
| Field | Type | Required | Description |
|---|---|---|---|
project | string | No | Payments project ID; explicit selection is recommended. A project-scoped key uses its project. |
chain | string | No | Network code. minLength: 1 |
asset | string | No | Supported payment asset: native, symbol, or known identifier. Accepted assets depend on the network; omission selects native. Default: native |
amount | string | No | Positive crypto amount in asset units; precision must not exceed the asset's decimals. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
reference | string | No | Your order reference; not a substitute for Idempotency-Key. maxLength: 120 |
address | string | No | Optional address from the project's payment pool. Otherwise the service selects one. |
tolerance | string | No | Allowed underpayment in asset units, strictly less than amount. Omit for zero tolerance. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No | Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets. |
invoice_currency | string | No | Three-letter order quote currency; only together with invoice_amount. pattern: ^[A-Za-z]{3}$ |
invoice_amount | string | No | Positive 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_in | integer | No | Payment 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 |
{
"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.
#!/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
// 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) }
}
// 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); }
}
<?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);
}
# 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)
// 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;
}
Responses and errors
201 Successful response.
| Field | Type | Required | Description |
|---|---|---|---|
checkout_url | string | No | Hosted 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 |
id | string | Yes | Opaque identifier; keep it as a string. minLength: 1 |
status | string | Yes | pending 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_id | string | Yes | Opaque identifier; keep it as a string. minLength: 1 |
reference | string | No | Order reference. |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No | Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets. |
invoice_currency | string | No | Order quote currency. |
invoice_amount | string | No | Exact decimal amount as a string; do not use floating point. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
chain | string | Yes | Network code. |
asset | string | Yes | Payment asset. |
address | string | Yes | Payment address. |
payment_route | object | No | Required 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_uri | string | No | Solana 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 |
amount | string | string | Yes | Actual amount the buyer must send; may differ from asked to distinguish invoices. Empty until a method is selected. |
amount_raw | string | string | Yes | Integer smallest-unit amount as a string. Empty until a method is selected. |
asked | string | string | Yes | Originally requested crypto amount. Empty until a method is selected. |
decimals | integer | Yes | Asset decimal places. minimum: 0 |
tolerance | string | No | Tolerance in smallest units; unlike request.tolerance. pattern: ^[0-9]+$ |
sandbox | boolean | Yes | A test invoice does not prove a real blockchain payment. |
needs_decision | boolean | No | A decision is required for underpaid, overpaid, or late. |
paid | string | No | Received amount in smallest units. pattern: ^[0-9]+$ |
tx_id | string | No | Payment transaction identifier. |
paid_at | string | No | UTC date and time in RFC 3339.date-time |
expires_at | string | Yes | UTC date and time in RFC 3339.date-time |
created_at | string | Yes | UTC date and time in RFC 3339.date-time |
{
"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).
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}401 No valid active API key (unauthorized).
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}403 Key disallows the operation, project, service or IP; email unconfirmed.
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"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).
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}429 Too many requests (rate_limited); retry after Retry-After.
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}500 Internal error (internal).
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}503 Temporary unavailability (unavailable); retry with backoff.
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
{
"error": {
"code": "forbidden",
"message": "this API key is read-only"
}
}Nested objects
Invoice
| Field | Type | Required | Description |
|---|---|---|---|
checkout_url | string | No | Hosted 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 |
id | string | Yes | Opaque identifier; keep it as a string. minLength: 1 |
status | string | Yes | pending 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_id | string | Yes | Opaque identifier; keep it as a string. minLength: 1 |
reference | string | No | Order reference. |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No | Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets. |
invoice_currency | string | No | Order quote currency. |
invoice_amount | string | No | Exact decimal amount as a string; do not use floating point. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
chain | string | Yes | Network code. |
asset | string | Yes | Payment asset. |
address | string | Yes | Payment address. |
payment_route | object | No | Required 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_uri | string | No | Solana 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 |
amount | string | string | Yes | Actual amount the buyer must send; may differ from asked to distinguish invoices. Empty until a method is selected. |
amount_raw | string | string | Yes | Integer smallest-unit amount as a string. Empty until a method is selected. |
asked | string | string | Yes | Originally requested crypto amount. Empty until a method is selected. |
decimals | integer | Yes | Asset decimal places. minimum: 0 |
tolerance | string | No | Tolerance in smallest units; unlike request.tolerance. pattern: ^[0-9]+$ |
sandbox | boolean | Yes | A test invoice does not prove a real blockchain payment. |
needs_decision | boolean | No | A decision is required for underpaid, overpaid, or late. |
paid | string | No | Received amount in smallest units. pattern: ^[0-9]+$ |
tx_id | string | No | Payment transaction identifier. |
paid_at | string | No | UTC date and time in RFC 3339.date-time |
expires_at | string | Yes | UTC date and time in RFC 3339.date-time |
created_at | string | Yes | UTC 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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Customer identifier in your system. maxLength: 120 |
Product
| Field | Type | Required | Description |
|---|---|---|---|
id | string | No | Product identifier in your system. maxLength: 120 |
name | string | No | Product name. maxLength: 200 |
quantity | integer | No | Quantity. minimum: 1; maximum: 1000000 |
Payer
| Field | Type | Required | Description |
|---|---|---|---|
comment | string | Yes | Payer 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
| Field | Type | Required | Description |
|---|---|---|---|
project | string | No | Payments project ID; explicit selection is recommended. A project-scoped key uses its project. |
chain | string | No | Network code. minLength: 1 |
asset | string | No | Supported payment asset: native, symbol, or known identifier. Accepted assets depend on the network; omission selects native. Default: native |
amount | string | No | Positive crypto amount in asset units; precision must not exceed the asset's decimals. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
reference | string | No | Your order reference; not a substitute for Idempotency-Key. maxLength: 120 |
address | string | No | Optional address from the project's payment pool. Otherwise the service selects one. |
tolerance | string | No | Allowed underpayment in asset units, strictly less than amount. Omit for zero tolerance. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No | Up to 20 pairs, at most 8192 JSON bytes. Do not include secrets. |
invoice_currency | string | No | Three-letter order quote currency; only together with invoice_amount. pattern: ^[A-Za-z]{3}$ |
invoice_amount | string | No | Positive 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_in | integer | No | Payment 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
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |