Documentación de la API
Conecta Cryptanio a tu aplicación.
Contrato v1 / 1.0.0 62 / 2026-10-10
Contenido
Comenzar
La API es para tu servidor. Configura primero proyectos, servicios y pools de direcciones en Account; la API realiza dentro de ellos las operaciones autorizadas por la clave.
- Verifica tu cuenta y activa el servicio necesario en Mis servicios.
- Crea una clave API con los permisos mínimos y guárdala en tu servidor.
- Elige tu tarea: vigilar una dirección, crear una factura o recibir eventos.
Los ejemplos realizan operaciones reales sobre datos activos. Crear o eliminar objetos puede consumir cuotas de servicio. Las claves nunca se incluyen en los ejemplos; se leen de variables de entorno.
Instrucciones para tu agente
Pega esta instrucción en el chat de tu agente o en las reglas del proyecto. Ayuda al agente a conocer los servicios, precios y referencia, y después a escribir una integración de la aplicación con la API REST o el SDK. El agente puede responder en tu idioma.
Learn how to integrate a server-side application with Cryptanio's public REST API before proposing or writing integration code.
Start here:
- Website: https://cryptanio.com/
- Documentation index: https://cryptanio.com/llms.txt
- Product, services and pricing: https://cryptanio.com/cryptanio.md
- API integration index: https://cryptanio.com/docs/llms.txt
- API guide: https://cryptanio.com/docs.md
- Exact API v1 contract: https://cryptanio.com/openapi.json
- FAQ: https://cryptanio.com/faq/index.md
- SDK setup and examples: https://cryptanio.com/docs.html#sdk
Read the relevant method reference before using it. Use the public API at https://api.cryptanio.com/v1. Do not assume a public sandbox exists.
Use HTTP requests or the documented SDK for the application's language. Match request fields, response schemas and errors to OpenAPI; do not invent endpoints or SDK methods.
Check the actual enabled networks, service, project and API key permissions. Product documentation is not evidence of a live payment, balance or deployment.
Keep API keys in the user's server environment or secret store. Never ask the user to paste a key into chat or put it in a URL, browser bundle or log.
Default to reading. Create, change or delete objects only when the user authorizes the specific task. Explain quota or billing consequences before a write.
Follow the documented confirmation, webhook verification, pagination and retry rules. Reuse an invoice's original Idempotency-Key and request when retrying.
Show the returned chain, asset, address, amount and expiry. Preserve payment_route.kind and its exact string value: destination_tag, memo, comment or reference is mandatory when present. The merchant order reference is a separate field.
For a payment QR use the returned payment_uri when present. A Solana Pay reference is not a wallet memo; do not rebuild its URI or replace it with a plain address. Offer the exact returned checkout_url only to the payer; never construct a checkout URL from an invoice ID. Neither a checkout redirect nor a QR scan confirms payment.
Distinguish pending, confirmed, rolled-back and simulated data. If a fact is unavailable, say so instead of guessing.
Answer the user's questions in their language. If a necessary product decision is not documented, ask instead of inventing it.Este bloque no contiene claves ni datos de cuenta. Utiliza una clave API con los permisos mínimos para tu integración y guárdala en el servidor de tu aplicación. No compartas la clave en el chat del agente.
Referencia de la API
Métodos: 16
Vigilancias de Monitoring
Eventos
Facturas
Webhook
Estado de la API
Autenticación
Envía Authorization: Bearer <API_KEY> desde tu servidor. Crea una clave para datos reales en Account → Claves API, limitada a los proyectos y operaciones necesarios. Actualmente no se ofrece un entorno de pruebas público.
Una clave puede permitir acceso de solo lectura, operaciones concretas, un proyecto o servicio y determinados rangos IP/CIDR. La caducidad, la pausa y la revocación también afectan al acceso. Para rotar una clave, crea su sustituta con las restricciones adecuadas, actualiza tu integración y revoca la anterior. Nunca incluyas claves en código del navegador, aplicaciones móviles ni URL.
| Ámbito | Finalidad |
|---|---|
watches | Monitorización |
events | Eventos y pagos |
invoices | Facturas de Payments |
webhooks | Endpoints de webhook |
stream | SSE |
Las restricciones se aplican conjuntamente, no se sustituyen entre sí. 401 significa que la clave no fue aceptada; 403 puede indicar permisos insuficientes o una IP no permitida. Un recurso inaccesible puede devolver 404. Consulta errores y reintentos.
SDK: clientes listos para usar
Descarga el archivo de tu lenguaje, guárdalo en el servidor de tu aplicación y pasa tu clave a su constructor. Cada cliente cubre los 16 métodos de esta referencia. No hay ningún paquete de npm, PyPI, Composer ni crates.io que instalar: los archivos descargados son el SDK.
| Idioma | Requisitos | Archivo |
|---|---|---|
| Node.js | Node.js 18+ | cryptanio.mjs |
| Python | Python 3.10+ | cryptanio.py |
| PHP | PHP 8.1+ / cURL | Cryptanio.php |
| Go | Go 1.22+ | cryptanio.go |
| Rust | Rust 2021 / reqwest 0.12 / serde_json 1 | cryptanio.rs |
Los archivos comparten la versión 1.0.0 62. El manifiesto contiene la versión, el contrato y el SHA-256 de cada archivo. Configura primero CRYPTANIO_API_KEY y CRYPTANIO_PROJECT_ID en tu entorno. Estos ejemplos solo leen eventos, pero acceden a datos reales de la cuenta.
Node.js
Guarda el ejemplo como archivo .mjs junto a cryptanio.mjs.
import { Cryptanio } from './cryptanio.mjs';
const key = process.env.CRYPTANIO_API_KEY;
const project = process.env.CRYPTANIO_PROJECT_ID;
if (!key || !project) throw new Error('Set API key and project ID');
const api = new Cryptanio(key);
const page = await api.listEvents({ project, limit: 25 });
console.log(page);Para leer las instrucciones de pago de una factura existente, indica su ID en CRYPTANIO_RESOURCE_ID. El ejemplo conserva el payment_route obligatorio, el payment_uri devuelto para el QR y el checkout_url exacto. Entrega el resultado al pagador mediante tu interfaz; no lo publiques en registros públicos. Los importes siguen siendo cadenas y se omiten los campos ausentes.
import { Cryptanio } from './cryptanio.mjs';
const key = process.env.CRYPTANIO_API_KEY;
const invoiceID = process.env.CRYPTANIO_RESOURCE_ID;
if (!key || !invoiceID) throw new Error('Set API key and invoice ID');
const api = new Cryptanio(key);
const invoice = await api.getInvoice(invoiceID);
const payment = {
chain: invoice.chain, asset: invoice.asset, address: invoice.address,
amount: invoice.amount, expires_at: invoice.expires_at,
payment_route: invoice.payment_route,
payment_uri: invoice.payment_uri, checkout_url: invoice.checkout_url,
};
console.log(JSON.stringify(payment, null, 2));Python
Guarda el ejemplo como archivo .py junto a cryptanio.py.
import os
from cryptanio import Cryptanio
api = Cryptanio(os.environ['CRYPTANIO_API_KEY'])
page = api.list_events({
'project': os.environ['CRYPTANIO_PROJECT_ID'], 'limit': 25
})
print(page)Para leer las instrucciones de pago de una factura existente, indica su ID en CRYPTANIO_RESOURCE_ID. El ejemplo conserva el payment_route obligatorio, el payment_uri devuelto para el QR y el checkout_url exacto. Entrega el resultado al pagador mediante tu interfaz; no lo publiques en registros públicos. Los importes siguen siendo cadenas y se omiten los campos ausentes.
import json
import os
from cryptanio import Cryptanio
api = Cryptanio(os.environ['CRYPTANIO_API_KEY'])
invoice = api.get_invoice(os.environ['CRYPTANIO_RESOURCE_ID'])
payment = {field: invoice[field] for field in (
'chain', 'asset', 'address', 'amount', 'expires_at',
'payment_route', 'payment_uri', 'checkout_url'
) if field in invoice}
print(json.dumps(payment, ensure_ascii=False))PHP
Guarda el ejemplo como archivo .php junto a Cryptanio.php; activa la extensión cURL.
<?php
require __DIR__ . '/Cryptanio.php';
$key = getenv('CRYPTANIO_API_KEY');
$project = getenv('CRYPTANIO_PROJECT_ID');
if (!$key || !$project) throw new RuntimeException('Set API key and project ID');
$api = new Cryptanio($key);
$page = $api->listEvents(['project' => $project, 'limit' => 25]);
echo json_encode($page, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);Para leer las instrucciones de pago de una factura existente, indica su ID en CRYPTANIO_RESOURCE_ID. El ejemplo conserva el payment_route obligatorio, el payment_uri devuelto para el QR y el checkout_url exacto. Entrega el resultado al pagador mediante tu interfaz; no lo publiques en registros públicos. Los importes siguen siendo cadenas y se omiten los campos ausentes.
<?php
require __DIR__ . '/Cryptanio.php';
$key = getenv('CRYPTANIO_API_KEY');
$invoiceID = getenv('CRYPTANIO_RESOURCE_ID');
if (!$key || !$invoiceID) throw new RuntimeException('Set API key and invoice ID');
$api = new Cryptanio($key);
$invoice = $api->getInvoice($invoiceID);
$payment = array_intersect_key($invoice, array_fill_keys([
'chain', 'asset', 'address', 'amount', 'expires_at',
'payment_route', 'payment_uri', 'checkout_url'
], true));
echo json_encode($payment, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);Go
Coloca el archivo en un directorio cryptanio/ dentro de tu módulo Go. Este ejemplo utiliza el módulo example.local/myapp; sustituye la ruta de importación por el nombre de tu módulo. No se necesitan dependencias externas.
package main
import (
"context"
"fmt"
"os"
"example.local/myapp/cryptanio"
)
func main() {
key, project := os.Getenv("CRYPTANIO_API_KEY"), os.Getenv("CRYPTANIO_PROJECT_ID")
if key == "" || project == "" { panic("Set API key and project ID") }
api, err := cryptanio.New(key, "", 0)
if err != nil { panic(err) }
page, err := api.ListEvents(context.Background(), map[string]any{
"project": project, "limit": 25,
})
if err != nil { panic(err) }
fmt.Println(page)
}Para leer las instrucciones de pago de una factura existente, indica su ID en CRYPTANIO_RESOURCE_ID. El ejemplo conserva el payment_route obligatorio, el payment_uri devuelto para el QR y el checkout_url exacto. Entrega el resultado al pagador mediante tu interfaz; no lo publiques en registros públicos. Los importes siguen siendo cadenas y se omiten los campos ausentes.
package main
import (
"context"
"encoding/json"
"os"
"example.local/myapp/cryptanio"
)
func main() {
key, invoiceID := os.Getenv("CRYPTANIO_API_KEY"), os.Getenv("CRYPTANIO_RESOURCE_ID")
if key == "" || invoiceID == "" { panic("Set API key and invoice ID") }
api, err := cryptanio.New(key, "", 0)
if err != nil { panic(err) }
invoice, err := api.GetInvoice(context.Background(), invoiceID)
if err != nil { panic(err) }
payment := make(map[string]any)
for _, field := range []string{
"chain", "asset", "address", "amount", "expires_at",
"payment_route", "payment_uri", "checkout_url",
} {
if value, ok := invoice[field]; ok { payment[field] = value }
}
if err := json.NewEncoder(os.Stdout).Encode(payment); err != nil { panic(err) }
}Rust
Guarda el archivo como src/cryptanio.rs y el ejemplo como src/main.rs. Añade las dependencias siguientes a Cargo.toml. Es un cliente síncrono bloqueante: no lo ejecutes directamente en un worker asíncrono.
[dependencies]
reqwest = { version = "=0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde_json = "=1.0.145"mod cryptanio;
use cryptanio::Cryptanio;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = Cryptanio::new(&std::env::var("CRYPTANIO_API_KEY")?)?;
let project = std::env::var("CRYPTANIO_PROJECT_ID")?;
let page = api.list_events(&[("project", &project), ("limit", "25")])?;
println!("{page}");
Ok(())
}Para leer las instrucciones de pago de una factura existente, indica su ID en CRYPTANIO_RESOURCE_ID. El ejemplo conserva el payment_route obligatorio, el payment_uri devuelto para el QR y el checkout_url exacto. Entrega el resultado al pagador mediante tu interfaz; no lo publiques en registros públicos. Los importes siguen siendo cadenas y se omiten los campos ausentes.
mod cryptanio;
use cryptanio::Cryptanio;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = Cryptanio::new(&std::env::var("CRYPTANIO_API_KEY")?)?;
let invoice = api.get_invoice(&std::env::var("CRYPTANIO_RESOURCE_ID")?)?;
let mut payment = serde_json::Map::new();
for field in ["chain", "asset", "address", "amount", "expires_at",
"payment_route", "payment_uri", "checkout_url"] {
if let Some(value) = invoice.get(field) {
payment.insert(field.to_owned(), value.clone());
}
}
println!("{}", serde_json::Value::Object(payment));
Ok(())
}Llamada a métodos
Node.js y PHP usan listEvents, Python y Rust usan list_events, Go usa ListEvents. Cada método corresponde a una solicitud de la referencia. Los filtros son un objeto/mapa, o una lista de pares en Rust. Los métodos de Go siempre reciben context.Context primero. Los filtros desconocidos se rechazan antes de enviar una solicitud.
| Método Node.js / PHP | HTTP |
|---|---|
createWatch | POST /v1/watches |
listWatches | GET /v1/watches |
getWatch | GET /v1/watches/{id} |
deleteWatch | DELETE /v1/watches/{id} |
listEvents | GET /v1/events |
listPayments | GET /v1/payments |
createInvoice | POST /v1/invoices |
listInvoices | GET /v1/invoices |
getInvoice | GET /v1/invoices/{id} |
cancelInvoice | POST /v1/invoices/{id}/cancel |
createEndpoint | POST /v1/endpoints |
listEndpoints | GET /v1/endpoints |
deleteEndpoint | DELETE /v1/endpoints/{id} |
streamEvents | GET /v1/stream |
getHealth | GET /v1/health |
listChains | GET /v1/chains |
La creación de vigilancias y webhooks recibe un cuerpo de solicitud. Los métodos de recurso único reciben su ID como cadena. createInvoice(body, idempotencyKey) requiere una clave de 16 a 80 caracteres con letras ASCII, dígitos, guiones o guiones bajos. Reutiliza la misma clave y cuerpo al reintentar; el SDK no genera una nueva clave ni reintenta automáticamente. Consulta la página de cada método para ver cuerpos, errores y permisos.
Respuestas, errores y streaming
Las respuestas JSON conservan los nombres de sus campos; los ID, importes y cursores siguen siendo cadenas. La paginación es manual, como se documenta para cada método. Los errores exponen el estado HTTP, code, message y Retry-After. Sin respuesta HTTP, status es 0 (Rust utiliza None). No se siguen redirecciones. Las respuestas JSON normales se limitan a 8 MiB. El tiempo de espera predeterminado es de 30 segundos, configurable hasta 300 segundos. En Python se aplica a una operación de red; en los demás clientes cubre toda la solicitud, incluido el streaming. Un tiempo de espera agotado no demuestra que una escritura no se haya ejecutado.
| Lenguaje / error | Campos |
|---|---|
| Node.js / Python: ApiError | status, code, message, retry_after |
| PHP: CryptanioError | status, apiCode, getMessage(), retryAfter |
| Go: *APIError | Status, Code, Message, RetryAfter |
| Rust: ApiError | status: Option<u16>, code, message, retry_after |
streamEvents expone bytes SSE sin procesar, sin reconexión ni análisis de eventos: un ReadableStream de Node.js, una respuesta HTTP de Python que se puede cerrar, Go io.ReadCloser o Rust blocking::Response. Libera el flujo cuando ya no sea necesario. PHP pasa fragmentos a una función de retorno; devuelve false para detenerlo. El streaming no sustituye los webhooks ni garantiza reproducir el historial; consulta las reglas SSE.
Tu primera vigilancia
En Account, crea un proyecto con el servicio Monitoring activo y una clave con permisos de vigilancias y eventos. Completa la verificación de la cuenta cuando se requiera. Copia el ID del proyecto desde su configuración; añade solo una dirección que realmente quieras vigilar.
Configura CRYPTANIO_API_KEY y CRYPTANIO_PROJECT_ID en el entorno de tu servidor. Sustituye la dirección de ejemplo por la tuya. Esta solicitud vincula una vigilancia real; no simula una transacción.
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${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' \
--data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"chain":"ethereum","address":"0x52908400098527886E0F7030069857D2E4169EE7","asset":"native","label":"Treasury"}' \
"https://api.cryptanio.com/v1/watches" \
--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
Una respuesta correcta contiene id: guárdalo. GET /v1/watches/{id} devuelve la vigilancia y su configuración, no los pagos. Para ver la actividad detectada, solicita GET /v1/events filtrado por watch_id. Es normal que la lista esté vacía antes de detectar una transacción. Respuesta, errores y ejemplos en otros idiomas.
Facturas de Payments
Activa Payments para tu proyecto y añade direcciones compatibles a su pool en Account. Envía project explícitamente en vez de depender de la selección del proyecto predeterminado. amount es una cadena en unidades del criptoactivo; invoice_currency/invoice_amount describen por separado la cotización del pedido.
Configura CRYPTANIO_PROJECT_ID con tu proyecto Payments y CRYPTANIO_API_KEY con una clave con permiso de facturas. CRYPTANIO_OPERATION_ID debe ser único para un nuevo pedido; reutilízalo al reintentar la misma operación. La factura se crea en datos reales.
#!/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
Tu clave debe permitir invoices y Payments; la clave del ejemplo de Monitoring puede estar restringida a otro servicio. Idempotency-Key contiene 16-80 letras ASCII, dígitos, - y _. Repetir la misma solicitud normalizada con la misma clave devuelve la factura existente con 201; cambiar el contenido devuelve 409 conflict. Las claves de operación abarcan toda la cuenta. reference no las sustituye.
Muestra al comprador la red, el activo, la dirección, amount y el vencimiento devueltos. Si aparece payment_route, transmite su kind y su valor value exacto como cadena: destination_tag, memo, comment o reference es obligatorio para atribuir el pago a esta factura. El campo reference del pedido es independiente.
Usa el payment_uri devuelto para el QR de pago cuando esté presente. Una referencia Solana Pay no es un memo de la cartera: no sustituyas el URI por una dirección simple ni lo reconstruyas. Si la cartera no puede incluir el identificador obligatorio, ofrece al pagador checkout_url o una cartera compatible.
checkout_url es la página de pago alojada para las facturas nuevas. Comparte con el pagador únicamente la URL exacta devuelta; no la construyas a partir del ID. El enlace revela los detalles y el estado del pago, por lo que no debes publicarlo en registros públicos. Si falta, muestra las instrucciones completas. Una redirección del navegador no confirma el pago: comprueba la API o un webhook verificado.
asked puede diferir de amount para distinguir pedidos. Concilia usando amount_raw como entero de precisión arbitraria. El TTL predeterminado es de 60 minutos y el máximo de 10080 minutos. Las listas de facturas aceptan before, pero no devuelven next_before: continúa con el ID del último elemento y detente al recibir una página vacía.
Customer
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Identificador del cliente en tu sistema. maxLength: 120 |
Product
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | No | Identificador del producto en tu sistema. maxLength: 120 |
name | string | No | Nombre del producto. maxLength: 200 |
quantity | integer | No | Cantidad. minimum: 1; maximum: 1000000 |
Payer
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
comment | string | Sí | Comentario del pagador. maxLength: 500 |
InvoiceMetadata
Hasta 20 pares, máximo 8192 bytes JSON. No incluyas secretos.
object Hasta 20 pares, máximo 8192 bytes JSON. No incluyas secretos.
Vigilancias de Monitoring
Una vigilancia pertenece a un proyecto Monitoring. La misma dirección puede utilizarse en varios proyectos con reglas independientes de dirección y activos. Conecta listas de la libreta de direcciones y fuentes de direcciones en Account; esta API REST no expone sus comandos de gestión.
La lista se ordena por ID descendente. Si aparece next_before, pásalo como before en la siguiente solicitud y conserva el proyecto y los filtros. limit tiene como valor predeterminado 50 y se limita a 200. Guarda los ID como cadenas. Las vigilancias eliminadas dejan la lista activa; eliminarlas aquí no borra la dirección de otros proyectos ni de la libreta de direcciones.
Eventos e historial
/v1/events devuelve eventos disponibles para la clave en todos los servicios; /v1/payments se limita a Payments. Filtra por project, address, chain, type, watch_id y tx_id. before es un cursor de ID, no una fecha; no existe filtro from/to. next_before permite obtener la página siguiente.
El historial de eventos detectados está disponible durante un máximo de 30 días. No es un archivo de todos los mensajes entregados: no se garantiza recuperar mensajes notice y webhook.test mediante él. REST Event tiene su propio esquema, independiente de los webhooks; pueden añadirse campos dentro de v1.
Event
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
event_id | string | Sí | Identificador opaco; consérvalo como cadena. minLength: 1 |
event_type | string | Sí | Tipo de evento; pueden añadirse nuevos tipos. |
chain | string | Sí | Código de red. |
service | string | No | Servicio, si se conoce. |
watch_id | string | No | ID de vigilancia del proyecto, si está vinculado a una vigilancia. minLength: 1 |
occurred_at | string | Sí | Fecha y hora UTC en RFC 3339.date-time |
payment | object | null | Sí | Documento de pago, o null para un evento sin pago. GET /events no devuelve contenidos contract/notice. |
sandbox | boolean | No | Evento de prueba; la omisión significa false. |
EventPayment
Documento de pago REST/SSE. Los campos dependen del evento y la red; no es la estructura normalizada de webhook v1.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tx_id | string | No | Identificador de transacción. |
index | integer | No | Índice de transferencia dentro de la transacción. minimum: 0 |
height | integer | No | Altura del bloque, cuando se conoce. minimum: 0 |
address | string | No | Dirección observada. |
asset | string | No | Activo REST/SSE: identificador de cadena, no el objeto asset del webhook. |
amount_raw | string | No | Importe entero en la unidad mínima como cadena. pattern: ^[0-9]+$ |
amount | string | No | Importe decimal exacto como cadena; no uses coma flotante. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
decimals | integer | No | Decimales. minimum: 0 |
confirmations | integer | No | Confirmaciones. minimum: 0 |
revision | integer | No | Revisión del estado de transferencia. minimum: 0 |
direction | string | No | Dirección respecto a la dirección observada.in, out |
simulated | boolean | No | Evento simulado. |
late | boolean | No | Transferencia detectada tarde. |
block_hash | string | No | Hash del bloque. |
match_key | string | No | Identificador de coincidencia, cuando lo utiliza la red. |
meta | object | No | |
invoice | object | No | |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No |
Eventos de webhook
La entrega es al menos una vez: un evento puede llegar varias veces. Tras verificar la firma, elimina duplicados por event_id en tu integración. Guarda el procesamiento y el cambio de negocio en una única transacción de base de datos. Una clave tx_id/address/asset no sustituye a event_id: fusiona transiciones, direcciones y proyectos distintos.
Lee event_type y event_id del cuerpo firmado. Las cabeceras X-Event-Type/X-Event-Id no se incluyen en el HMAC; compáralas con el cuerpo si las utilizas. Ten en cuenta project_id, direction y payment.revision al actualizar un pago. No supongas un orden de entrega; ignora de forma segura los tipos desconocidos y consérvalos para diagnóstico.
| Familia | Cuerpo |
|---|---|
| payment.pending / confirmed / rolled_back | Pago entrante |
| debit.pending / confirmed / rolled_back | Pago saliente |
| contract.approval / owner_added / owner_removed / threshold_changed / permissions_changed | contract |
| asset.unexpected | contract; el activo no se acredita como pago aceptado |
| account.notice / account.notification / treasury.alarm / subscription.notice | notice.subject, notice.text |
| webhook.test | Evento de control sin pago |
schema_version: v1 contiene payment, contract o notice según el tipo de evento; webhook.test no contiene ninguno. Los ID e importes son cadenas. amount_raw contiene unidades mínimas; amount es una representación decimal. Comprueba redes y activos mediante chains. Los esquemas completos de los objetos están disponibles en OpenAPI.
| Cabecera | Significado |
|---|---|
| X-Timestamp | Segundos Unix en el momento de la firma |
| X-Signature | hex HMAC-SHA256(timestamp + '.' + raw body) |
| X-Event-Id / X-Event-Type | Comparar con el cuerpo firmado |
| X-Attempt | Número de intento a partir de 1 |
Devuelve 2xx tras aceptar el evento de forma duradera. El tiempo de espera por intento es de 10 segundos. Los fallos de red, 408, 425, 429 y 5xx se reintentan durante un máximo de 24 horas, con esperas de 2 segundos a 15 minutos y variación aleatoria; se respeta Retry-After. No se siguen redirecciones. Otros 3xx/4xx finalizan la entrega; 410 retira el destino y su cola pendiente.
{
"schema_version": "v1",
"event_id": "2300",
"event_type": "payment.confirmed",
"occurred_at": "2026-10-06T10:00:00Z",
"project_id": "1002",
"service": "monitoring",
"mode": "test",
"sandbox": true,
"payment": {
"chain": "ethereum",
"direction": "in",
"asset": {
"symbol": "ETH"
},
"address": "0x52908400098527886E0F7030069857D2E4169EE7",
"tx_id": "simulated_2300",
"index": 0,
"amount": "1.000000000000000000",
"amount_raw": "1000000000000000000",
"decimals": 18,
"confirmations": 0,
"revision": 1,
"simulated": true
}
}Webhook
Cuerpo público de webhook firmado v1. payment.* y debit.* contienen payment; contract.* y asset.unexpected contienen contract; webhook.test no tiene contenido de negocio; account.notice, account.notification, treasury.alarm y subscription.notice contienen notice. Elimina duplicados de reintentos de entrega por event_id.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
schema_version | string | Sí | Versión del contenido de webhook.v1 |
event_id | string | Sí | ID del evento: no cambia en los reintentos de entrega. Los distintos estados pueden tener valores event_id distintos. minLength: 1 |
event_type | string | Sí | Tipo en el cuerpo firmado; úsalo después de verificar la firma. Pueden añadirse nuevos tipos. |
occurred_at | string | Sí | Fecha y hora UTC en RFC 3339.date-time |
project_id | string | Sí | Identificador opaco; consérvalo como cadena. minLength: 1 |
service | string | No | Servicio del proyecto, cuando corresponda. |
mode | string | Sí | Modo del evento.live, test |
sandbox | boolean | Sí | Indicador explícito de simulación. |
payment | object | No | |
contract | object | No | |
invoice | object | No | |
customer | object | No | |
product | object | No | |
payer | object | No | |
metadata | object | No | |
notice | object | No |
WebhookPayment
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
chain | string | Sí | Código de red. |
direction | string | Sí | Pago entrante: in; débito saliente: out.in, out |
asset | object | Sí | |
address | string | Sí | Dirección observada. |
tx_id | string | Sí | ID de transacción. |
index | integer | Sí | Índice de transferencia; no supongas 0 para ninguna red. minimum: 0 |
height | integer | No | Altura del bloque, si se conoce. minimum: 0 |
block_hash | string | No | Hash del bloque, si se conoce. |
match_key | string | No | Identificador de coincidencia de la red, cuando corresponda. |
amount | string | Sí | Importe decimal exacto como cadena; no uses coma flotante. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
amount_raw | string | Sí | Importe entero en la unidad mínima como cadena. pattern: ^[0-9]+$ |
decimals | integer | Sí | Decimales. minimum: 0 |
confirmations | integer | Sí | Confirmaciones. minimum: 0 |
revision | integer | Sí | Revisión del estado de transferencia. No agrupes valores event_id distintos por identidad de transacción. minimum: 0 |
late | boolean | No | Transferencia detectada tarde; no equivale al estado late de una factura. |
simulated | boolean | No | Transferencia simulada. |
valuation | object | No |
WebhookAsset
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
symbol | string | No | Símbolo, si se conoce; no basta para establecer la identidad del token. |
standard | string | No | Estándar del token, si se conoce. |
contract | string | No | Contrato del token en su red, cuando corresponda. |
identifier | string | No | Otro identificador de token específico de la red. |
WebhookContract
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
chain | string | Sí | Código de red. |
address | string | Sí | Dirección o contrato observado. |
tx_id | string | Sí | ID de transacción. |
index | integer | Sí | Índice del evento. minimum: 0 |
height | integer | No | Altura del bloque. minimum: 0 |
block_hash | string | No | Hash del bloque. |
kind | string | No | Tipo de cambio. |
asset | string | No | Identificador del activo afectado, si se conoce. |
detail | object | No | |
simulated | boolean | No | Evento simulado. |
WebhookInvoice
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Identificador opaco; consérvalo como cadena. minLength: 1 |
reference | string | No | Referencia del pedido. |
status | string | No | pending espera el pago; seen detectado; partial parcialmente pagado; paid liquidado; underpaid/overpaid/late requieren una decisión; expired plazo vencido; canceled anulado.pending, seen, paid, underpaid, partial, overpaid, expired, late, canceled |
currency | string | No | Moneda de la cotización del pedido. |
amount | string | No | Importe decimal exacto como cadena; no uses coma flotante. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
Notice
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
subject | string | Sí | Asunto del aviso. |
text | string | Sí | Texto del aviso. |
Valuation
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
currency | string | Sí | Moneda de valoración, como USD. |
amount | string | Sí | Importe decimal exacto como cadena; no uses coma flotante. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
rate | string | Sí | Tipo de cambio del activo como cadena decimal. pattern: ^[0-9]+(?:\.[0-9]+)?$ |
rate_at | string | Sí | Fecha y hora UTC en RFC 3339.date-time |
provider | string | No | Nombre de la fuente de cotización, si se conoce. |
Verificación de firmas
Verifica el HMAC sobre los bytes originales del cuerpo antes de analizar el JSON, incluidos espacios y saltos de línea. Comprobar la antigüedad limita la repetición, pero no sustituye la deduplicación persistente por event_id. Los ejemplos solo aceptan firmas hexadecimales de 64 caracteres y marcas de tiempo a menos de 300 segundos de la hora del servidor. Usa el secreto del destino de webhook concreto.
import { createHmac, timingSafeEqual } from "node:crypto";
import { TextDecoder } from "node:util";
// rawBody is the original HTTP request Buffer, before JSON.parse or serialization.
export function verifyWebhook(secret, timestamp, rawBody, signature, nowSeconds = Math.floor(Date.now() / 1000)) {
if (typeof secret !== "string" || secret.length === 0 || !Buffer.isBuffer(rawBody) || rawBody.length === 0 ||
typeof timestamp !== "string" || !/^(0|[1-9][0-9]*)$/.test(timestamp) ||
typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature) ||
!Number.isSafeInteger(nowSeconds) || nowSeconds < 0) return false;
const signedAt = Number(timestamp);
if (!Number.isSafeInteger(signedAt) || Math.abs(nowSeconds - signedAt) > 300) return false;
const expected = createHmac("sha256", secret).update(timestamp).update(".").update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
// headers is Node.js request.headers (lowercase names). Optional X-Event-Id/Type
// headers are not signed separately; compare them with the verified body.
export function readSignedWebhook(secret, timestamp, rawBody, signature, headers = {}, nowSeconds) {
if (!verifyWebhook(secret, timestamp, rawBody, signature, nowSeconds) ||
headers === null || typeof headers !== "object" || Array.isArray(headers)) return null;
let event;
try {
event = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(rawBody));
} catch {
return null;
}
if (event === null || typeof event !== "object" || Array.isArray(event) || event.schema_version !== "v1" ||
typeof event.event_id !== "string" || event.event_id.length === 0 ||
typeof event.event_type !== "string" || event.event_type.length === 0) return null;
for (const [header, field] of [["x-event-id", "event_id"], ["x-event-type", "event_type"]]) {
if (headers[header] !== undefined && headers[header] !== event[field]) return null;
}
return event;
}
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strconv"
"time"
"unicode/utf8"
)
// VerifyWebhook checks the original HTTP body bytes before parsing JSON.
// Optional nowSeconds is used only for deterministic tests.
func VerifyWebhook(secret, timestamp string, rawBody []byte, signature string, nowSeconds ...int64) bool {
if secret == "" || len(rawBody) == 0 || len(signature) != 64 || len(nowSeconds) > 1 {
return false
}
signedAt, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || signedAt < 0 || strconv.FormatInt(signedAt, 10) != timestamp {
return false
}
now := time.Now().Unix()
if len(nowSeconds) == 1 {
now = nowSeconds[0]
}
if now < 0 || now-signedAt < -300 || now-signedAt > 300 {
return false
}
provided, err := hex.DecodeString(signature)
if err != nil || len(provided) != sha256.Size {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(rawBody)
return hmac.Equal(mac.Sum(nil), provided)
}
// WebhookEvent contains only routing fields. Parse other fields from rawBody
// after ReadSignedWebhook succeeds; unknown fields are allowed.
type WebhookEvent struct {
SchemaVersion string `json:"schema_version"`
EventID string `json:"event_id"`
EventType string `json:"event_type"`
}
// ReadSignedWebhook extracts event_id/type only from the verified body.
// X-Event-Id/Type are not signed separately; nonempty values must match the body.
func ReadSignedWebhook(secret, timestamp string, rawBody []byte, signature, eventIDHeader, eventTypeHeader string,
nowSeconds ...int64) (*WebhookEvent, error) {
if !VerifyWebhook(secret, timestamp, rawBody, signature, nowSeconds...) {
return nil, errors.New("invalid webhook signature or timestamp")
}
var event WebhookEvent
if !utf8.Valid(rawBody) || json.Unmarshal(rawBody, &event) != nil || event.SchemaVersion != "v1" ||
event.EventID == "" || event.EventType == "" {
return nil, errors.New("invalid webhook body")
}
if eventIDHeader != "" && eventIDHeader != event.EventID || eventTypeHeader != "" && eventTypeHeader != event.EventType {
return nil, errors.New("webhook headers do not match the signed body")
}
return &event, nil
}
Endpoints de webhook
Añade una URL HTTPS pública en el puerto 443 para un proyecto concreto. No se permiten credenciales en la URL, fragmentos, redirecciones ni destinos no públicos. Se permiten hasta 20 endpoints por cuenta. Cada uno tiene un secreto independiente, que se devuelve solo al crearlo; guárdalo en tu servidor.
Crear otra vez la misma URL para el mismo proyecto y modo devuelve 200 y el endpoint existente sin su secreto. Un endpoint nuevo devuelve 201. DELETE devuelve 200 con id y deleted: true, incluida una repetición segura. Una clave solo ve su modo y los proyectos permitidos; la entrega sigue la configuración del proyecto.
Flujo SSE
SSE sirve para integrar desde el servidor y mostrar datos en directo; no garantiza la entrega ni reproduce eventos perdidos. Usa un cliente HTTP de servidor con la cabecera Bearer. EventSource estándar del navegador no puede enviarla; no incluyas tu clave API en código del navegador ni en parámetros de URL.
Los mensajes usan event: payment y su propia estructura StreamEvent, no webhook v1 ni REST Event. Los comentarios empiezan con dos puntos; las señales de mantenimiento de conexión llegan aproximadamente cada 20 segundos. retry: 5000 es una indicación de reconexión. Tras desconectar, recupera el historial disponible de eventos de blockchain mediante events.
StreamEvent
JSON en los datos SSE; difiere de webhook v1. Los eventos sin pago omiten payment y pueden incluir contract.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
event_id | string | Sí | Identificador opaco; consérvalo como cadena. minLength: 1 |
event_type | string | Sí | Tipo de evento; pueden añadirse nuevos tipos. |
chain | string | Sí | Código de red. |
service | string | No | Servicio, si se conoce. |
watch_id | string | No | ID de vigilancia del proyecto, si está vinculado a una vigilancia. minLength: 1 |
occurred_at | string | Sí | Fecha y hora UTC en RFC 3339.date-time |
payment | object | null | No | Documento de pago, o null para un evento sin pago. GET /events no devuelve contenidos contract/notice. |
sandbox | boolean | No | Evento de prueba; la omisión significa false. |
project_id | string | No | Identificador opaco; consérvalo como cadena. minLength: 1 |
project_name | string | No | Nombre del proyecto. |
url | string | No | Enlace al evento de Account.uri |
contract | object | No | |
mode | string | No | live o test.live, test |
Estados de pagos y facturas
El estado de un pago en blockchain y el de un pedido son diferentes. payment.pending indica la detección antes de tener suficientes confirmaciones; payment.confirmed significa que se alcanzó la regla de confirmación de la red/proyecto; payment.rolled_back informa de una reversión. No entregues un pedido por eventos pending, débitos salientes ni eventos simulados.
| Invoice | Significado |
|---|---|
| pending | Esperando el pago |
| partial | Parte recibida; se rige por la política de pagos parciales del proyecto |
| paid | Pago aceptado |
| underpaid | Un pago insuficiente requiere una decisión |
| overpaid | Un pago excesivo requiere una decisión conforme a la política aplicable |
| late | Un pago tras la caducidad requiere una decisión |
| expired | Plazo de pago vencido |
| canceled | Factura cancelada; el registro sigue siendo consultable |
needs_decision aparece para underpaid, overpaid y late. Resuelve estos estados en Account. Una reorganización y reinclusión pueden producir nuevas transiciones y un nuevo payment.revision; confirmed no es una garantía incondicional contra futuros cambios.
Errores y reintentos seguros
{"error":{"code":"forbidden","message":"this API key is read-only"}}Usa el estado HTTP y error.code para la lógica y message para diagnóstico. Las listas correctas tienen data; un objeto único se devuelve directamente. Health utiliza su propio esquema status/version, incluso en 503. No dependas del texto exacto de message.
| HTTP | Acción |
|---|---|
| 400 | Corrige el JSON, los formatos y los parámetros; no reintentes sin cambios |
| 401 | Comprueba la clave, su caducidad, pausa y revocación |
| 403 | Comprueba los ámbitos, lectura/escritura, proyecto, servicio, IP y verificación de la cuenta |
| 404 | Comprueba el ID, el modo de la clave y el acceso; las restricciones pueden ocultar el recurso |
| 402 | Límite de vigilancias de Monitoring; comprueba tu plan |
| 409 | Conflicto, servicio inactivo, pool ocupado o límite; revisa el código |
| 429 | Respeta Retry-After; reduce la tasa conjunta de solicitudes de la cuenta |
| 500 / 503 | Reintenta con espera progresiva limitada; crea facturas solo con la Idempotency-Key original |
Un tiempo de espera agotado no significa que la operación no se haya ejecutado. No cambies la clave de operación de una factura al reintentar. En los métodos sin protección contra duplicados, consulta primero el estado; no supongas que todos los POST son idempotentes.
Redes y límites
Utiliza chains en el host API seleccionado para su catálogo de redes, activos, capacidades y confirmaciones. Figurar en el catálogo general no garantiza que una red esté habilitada en ese entorno ni admita todas las operaciones. Comprueba las cuotas de servicio y el presupuesto en Account; los precios están en la página Precios.
La cuota REST autenticada de la cuenta es de 10 solicitudes/s sin servicio de pago y 50 solicitudes/s con un servicio de pago; la capacidad de ráfaga breve es 20/100. Todas las claves y modos de la cuenta comparten esta cuota. Superarla devuelve 429 con Retry-After. Las cuotas de servicio y los límites de tasa de la API son restricciones independientes.
Versiones de la API
Esta referencia describe v1 en la versión 1.0.0 62; actualizada el 2026-10-10. Antes de utilizar nuevas funciones, comprueba la versión desplegada mediante GET /v1/health. La versión de la documentación no confirma el despliegue.
| API pública | Contrato |
|---|---|
https://api.cryptanio.com | /v1 |
Utiliza /v1 para integraciones estables. Una ruta sin versión selecciona el contrato estable actual del entorno; un /vN desconocido devuelve 404. X-API-Version identifica el contrato; X-Server-Version y health.version identifican la versión del producto, no un contrato API distinto.
Cambios de la API
La versión 1.0.0 62 incluye SDK descargables para Node.js, Python, PHP, Go y Rust y MCP local para agentes de IA. La referencia, Markdown y los ejemplos HTTP sin procesar siguen disponibles por separado. No se ofrece un entorno de pruebas público. El comportamiento de la API no cambia.
Dentro de v1, los clientes deben tolerar campos de respuesta adicionales. Aceptar un tipo de evento desconocido no significa entregar un pedido ni acreditar un pago. Comprueba la disponibilidad del cambio en la versión del entorno seleccionado antes de usarlo.