API reference

Cancel an invoice

POST/v1/invoices/{id}/cancel

Contract v1 / 1.0.0 62 / 2026-10-10

On this page

Cancel an open invoice without deleting it. It remains available for reconciliation. An incompatible closed status returns 409.

Key permissions: invoices / write / payments.

Parameters

ParameterInRequiredDescription
idpathYesOpaque resource ID.
string minLength: 1

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_RESOURCE_ID:?Set CRYPTANIO_RESOURCE_ID}"
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
urlencode() {
  local LC_ALL=C text="$1" char index
  for ((index=0; index<${#text}; index++)); do
    char="${text:index:1}"
    case "$char" in [a-zA-Z0-9.~_-]) printf '%s' "$char" ;; *) printf '%%%02X' "'$char" ;; esac
  done
}
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' \
  "https://api.cryptanio.com/v1/invoices/$(urlencode "$CRYPTANIO_RESOURCE_ID")/cancel" \
  --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 (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strings"
    "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/{id}/cancel"
    path = strings.ReplaceAll(path, "{id}", url.PathEscape(env("CRYPTANIO_RESOURCE_ID")))
    endpoint, err := url.Parse("https://api.cryptanio.com" + path)
    if err != nil { return err }
    query := endpoint.Query()

    endpoint.RawQuery = query.Encode()

    request, err := http.NewRequest("POST", endpoint.String(), nil)
    if err != nil { return err }
    request.Header.Set("Authorization", "Bearer " + env("CRYPTANIO_API_KEY"))
    request.Header.Set("Accept", "application/json")
    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");
        segments.push(&env("CRYPTANIO_RESOURCE_ID")?);
        segments.push("cancel");
    }
    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());
    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/{id}/cancel';
    $path = str_replace('{id}', rawurlencode(env('CRYPTANIO_RESOURCE_ID')), $path);
    $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',
    ];
    $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,
    ]);
    $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/{id}/cancel"
    path = path.replace("{id}", quote(env("CRYPTANIO_RESOURCE_ID"), safe=""))
    query = {

    }
    url = "https://api.cryptanio.com" + path + ("?" + urlencode(query) if query else "")
    headers = {
        "Authorization": "Bearer " + env("CRYPTANIO_API_KEY"),
        "Accept": "application/json",
    }
    data = None
    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/{id}/cancel";
  path = path.replace("{id}", encodeURIComponent(env("CRYPTANIO_RESOURCE_ID")));
  const url = new URL(path, "https://api.cryptanio.com");

  const headers = {
    "Authorization": "Bearer " + env("CRYPTANIO_API_KEY"),
    "Accept": "application/json",
  };
  const response = await fetch(url, {
    method: "POST", headers, redirect: "manual",
    signal: AbortSignal.timeout(30_000),
  });
  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

200 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
200 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"
  }
}
404 Object not found within the key's scope (not_found).
FieldTypeRequiredDescription
errorobjectYes
404 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

Error

FieldTypeRequiredDescription
errorobjectYes