Ir al contenido
Ir al contenido

DESARROLLADORES

Documentación de la API de cobros

Crea un pago, envía a tu cliente a la página de pago y recibe el resultado por webhook o consulta.

  • URL basehttps://www.cryptoavans.com/api/v3
  • Versión de la API3.0.0

Primeros pasos

Entorno

Prueba sin mover dinero real.

Pagos reales. Se requiere una clave de producción.

Primeros pasos

De una cuenta nueva al primer pago en producción en cuatro pasos.

  1. Obtén tus claves

    Crea una cuenta con “Registro” o inicia sesión, abre “API Key e Integración” y genera una clave de sandbox. Antes, añade la IP de tu servidor y una dirección de webhook. Detalles: Obtener una clave.

  2. Crea un pago

    Envía un POST /api/v3/payments firmado (hay código de ejemplo) y lleva al cliente a la payment_url devuelta. Los campos y las reglas están en Pagos.

  3. Prueba y recibe el resultado

    Completa un pago de prueba con una tarjeta de prueba y revísalo en “Pedidos API de Sandbox”. Tu webhook recibe un payment.paid firmado (Webhooks); también puedes consultar GET /api/v3/payments/{code}. Completa el pedido solo cuando el estado sea paid.

  4. Pasa a producción

    Tras tu primer pedido de sandbox, genera la clave de producción, añade la lista de IP y la dirección de webhook de producción y completa la lista de verificación.

Convenciones

URL base
https://www.cryptoavans.com/api/v3
Formato
application/json · UTF-8
Importes
Cadenas decimales: los importes en moneda fiduciaria tienen 2 decimales (por ejemplo "250.00"). usdt_amount tiene 6.
Monedas
USD EUR AED TRY
Marcas de tiempo
UTC · ISO 8601
Idioma de los errores
Se elige con la cabecera Accept-Language. Por defecto es el turco.

El saldo, los reembolsos, los retiros, las disputas y las entregas de webhook se describen en la página Operaciones de la cuenta.

Si prefieres recoger la tarjeta en tu propio formulario de pago, consulta Direct API (v4).

Obtener una clave

Sandbox y producción tienen claves, listas de IP y webhooks independientes. Ambas claves pueden estar activas a la vez.

  1. Abre “API Key e Integración”.

  2. Copia el número de cuenta de 6 dígitos de la parte superior. Envíalo como X-CAV-Account-No en las solicitudes firmadas.

  3. En “Pruebas”, añade la IP pública del servidor a “Lista de IP permitidas” con “Añadir IP”. Introduce las direcciones IPv4 o IPv6 de una en una. No se admiten rangos CIDR.

  4. Introduce una dirección HTTPS pública en “URL de webhook HTTPS”. Es obligatoria y se guarda al generar la clave. Después puedes cambiarla con “Guardar webhook” sin renovar la clave.

  5. Pulsa “Generar clave”. Guarda la clave API y la clave secreta, marca “He guardado la clave secreta de forma segura.” y pulsa “Guardada, cerrar”.

La sección de producción se habilita cuando has creado al menos un pedido de sandbox. No hace falta completar el pago.

  1. Crea al menos un pedido con tu clave de sandbox (POST /api/v3/payments). En el panel se desbloquea la sección de producción.

  2. En la sección de producción, añade las IP de tus servidores de producción a “Lista de IP permitidas”. La lista es independiente de la de sandbox. Mientras esté vacía, se rechaza toda solicitud de producción.

  3. Escribe tu URL de webhook HTTPS de producción en “URL de webhook HTTPS”. Se guarda junto con la clave en el paso siguiente.

  4. Pulsa “Generar clave” y guarda la clave secreta de producción en tu servidor.

  5. Configura la clave API de producción y su clave secreta en tu integración. La URL base y el formato de las solicitudes se mantienen.

Lista de IP permitidas

Las solicitudes solo se aceptan desde direcciones IP incluidas en tu lista de IP permitidas. Una solicitud desde cualquier otra dirección se rechaza con 403 (9000012) y no se procesa nada.

  • La lista es obligatoria. Sandbox y producción tienen listas separadas, y no se puede generar una clave mientras la lista de ese entorno esté vacía.
  • Introduce solo direcciones IPv4 o IPv6 individuales; no hay rangos (CIDR). Si tu servidor sale tanto por IPv4 como por IPv6, añade ambas.
  • Lo que cuenta es la dirección pública desde la que tu servidor sale a internet. Las direcciones privadas (10.x, 192.168.x) no sirven; si estás detrás de un NAT o un proxy, añade su dirección de salida.
  • Pulsa “Quitar IP” junto a la dirección y confirma. Para quitar la última IP con una clave activa, añade antes otra dirección o revoca la clave.
  • Los cambios se aplican de inmediato. No hace falta renovar la clave.

Añade todas las direcciones desde las que pruebes, como un servidor de desarrollo o la dirección de salida de tu oficina.

Añade las direcciones de los servidores de producción a su lista. La lista de sandbox no autoriza solicitudes de producción.

Renovar y revocar claves

Si pierdes la clave secreta, usa “Renovar clave”. La clave anterior de ese entorno se revoca de inmediato. El otro entorno conserva su clave.

  • Cada entorno tiene una clave activa. “Renovar clave” genera un par nuevo y revoca el anterior de inmediato. Las solicitudes firmadas con la clave anterior fallan con 9000003.
  • “Revocar clave” desactiva la clave sin emitir una nueva. Las solicitudes se rechazan de inmediato.
  • La clave secreta de una clave revocada sigue firmando los webhooks de los pagos creados con ella. Consérvala de forma segura para esos pagos. Si se pierde o se filtra, verifica su estado mediante solicitudes GET autenticadas con tu clave actual; no confíes en webhooks firmados con una clave secreta comprometida.

Autenticación

Firma las solicitudes autenticadas con HMAC-SHA256 y tu clave secreta. La firma cubre la marca de tiempo, el método, la ruta y el hash del cuerpo. GET /api/v3/service-status es público y no necesita firma.

Cabeceras obligatorias

CabeceraDescripción
X-CAV-Account-NoTu número de cuenta de 6 dígitos.
X-CAV-Api-KeyLa clave API del entorno al que llamas (sandbox o producción).
X-CAV-TimestampHora Unix actual en segundos, 10 dígitos. Se rechazan las solicitudes con más de 300 segundos de desviación.
X-CAV-SignatureHMAC-SHA256 de la cadena canónica, 64 caracteres hex en minúsculas.
Idempotency-KeyObligatoria en solicitudes de escritura. Usa entre 8 y 128 caracteres de A-Z a-z 0-9 _ -. Un UUID sirve.
Content-Typeapplication/json en solicitudes con cuerpo.

Guarda la clave secreta según Obtener una clave.

Cómo firmar una solicitud

  1. Serializa el cuerpo JSON una sola vez y calcula sha256(body) en hex minúsculas. Para solicitudes sin cuerpo usa sha256("").

  2. Construye la cadena canónica con timestamp, METHOD, path y el hash del cuerpo, unidos por saltos de línea (\n, sin salto final). La ruta no incluye la cadena de consulta.

  3. Calcula HMAC-SHA256(secret, canonical) y codifícalo como 64 caracteres hex en minúsculas.

  4. Envía las cuatro cabeceras X-CAV-* (número de cuenta, clave API, marca de tiempo, firma); añade Idempotency-Key en las solicitudes de escritura.

Cadena de firma canónica

timestamp + "\n" + METHOD + "\n" + path + "\n" + hex(sha256(body))
signature = hmac_sha256(secret, canonical)  // 64 lowercase hex characters

path es la ruta completa sin la cadena de consulta (por ejemplo, /api/v3/payments). Una solicitud sin cuerpo usa el hash de la cadena vacía (sha256("")).

Firma exactamente los bytes que envías: serializa el JSON una sola vez y usa la misma cadena para el hash y para el cuerpo de la solicitud.

Ejemplo (con datos ficticios)

Todos los valores siguientes son ficticios: número de cuenta 123456, clave secreta sk_test_EXAMPLE. Usa los valores de tu propio panel.

Cuerpo de la solicitud

{"amount":"250.00","currency_code":"USD","merchant_reference":"ORDER-2026-0001"}

sha256(body)

280db1ad2f6a457444b4838e9da76682edbd328a1b62f7b9b762852d9c4354f7

Cadena de firma canónica

1790000000
POST
/api/v3/payments
280db1ad2f6a457444b4838e9da76682edbd328a1b62f7b9b762852d9c4354f7

X-CAV-Signature

3d3a95fcbda890bb424184c914ef3e2371c43602988cf3a698dc357b8074c5e9

Enviar una solicitud firmada

Cada ejemplo crea un pago. Las credenciales se leen de variables de entorno. No escribas la clave secreta en el código. Cada ejecución usa una merchant_reference nueva.

#!/usr/bin/env bash
# export MERCHANT_ACCOUNT_NO=... MERCHANT_API_KEY=... MERCHANT_API_SECRET=...   (sandbox key)
# GET requests: METHOD=GET, REQ_PATH=/api/v3/payments/<code>, BODY='' (sha256 of the empty string), no Content-Type / --data-binary
set -euo pipefail

BASE_URL="https://www.cryptoavans.com"
METHOD="POST"
REQ_PATH="/api/v3/payments"
REFERENCE="ORDER-$(date +%s)-$(openssl rand -hex 4)"   # a new merchant_reference for every payment
BODY="{\"amount\":\"250.00\",\"currency_code\":\"USD\",\"merchant_reference\":\"$REFERENCE\",\"return_url\":\"http://localhost/return\"}"

TIMESTAMP="$(date +%s)"
BODY_SHA256="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
CANONICAL="$(printf '%s\n%s\n%s\n%s' "$TIMESTAMP" "$METHOD" "${REQ_PATH%%\?*}" "$BODY_SHA256")"
# the secret travels in the environment, not on the command line
SIGNATURE="$(printf '%s' "$CANONICAL" | python3 -c 'import hashlib,hmac,os,sys; print(hmac.new(os.environ["MERCHANT_API_SECRET"].encode(), sys.stdin.buffer.read(), hashlib.sha256).hexdigest())')"

curl --silent --show-error --request "$METHOD" "$BASE_URL$REQ_PATH" \
  --header "X-CAV-Account-No: $MERCHANT_ACCOUNT_NO" \
  --header "X-CAV-Api-Key: $MERCHANT_API_KEY" \
  --header "X-CAV-Timestamp: $TIMESTAMP" \
  --header "X-CAV-Signature: $SIGNATURE" \
  --header "Idempotency-Key: $(openssl rand -hex 16)" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json" \
  --data-binary "$BODY"
<?php
$config = [
    'base'    => 'https://www.cryptoavans.com',
    'account' => getenv('MERCHANT_ACCOUNT_NO'),
    'key'     => getenv('MERCHANT_API_KEY'),
    'secret'  => getenv('MERCHANT_API_SECRET'),
];

function signedRequest(array $config, string $method, string $path, ?array $payload = null, ?string $idempotencyKey = null): array
{
    $method = strtoupper($method);
    $signingPath = explode('?', $path, 2)[0];
    // serialise once: the same bytes are hashed and sent
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $timestamp = (string) time();
    $canonical = implode("\n", [$timestamp, $method, $signingPath, hash('sha256', $body)]);
    $signature = hash_hmac('sha256', $canonical, $config['secret']);

    $headers = [
        'X-CAV-Account-No: ' . $config['account'],
        'X-CAV-Api-Key: ' . $config['key'],
        'X-CAV-Timestamp: ' . $timestamp,
        'X-CAV-Signature: ' . $signature,
        'Content-Type: application/json',
        'Accept: application/json',
    ];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }

    $ch = curl_init($config['base'] . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($response === false) {
        throw new RuntimeException('No response; retry the same operation and Idempotency-Key.');
    }

    return [$status, json_decode((string) $response, true)];
}

// a new merchant_reference for every payment: seconds plus a random suffix
[$status, $payment] = signedRequest(
    $config,
    'POST',
    '/api/v3/payments',
    ['amount' => '250.00', 'currency_code' => 'USD', 'return_url' => 'http://localhost/return', 'merchant_reference' => 'ORDER-' . time() . '-' . bin2hex(random_bytes(4))],
    bin2hex(random_bytes(16))
);
echo $status, ' ', json_encode($payment, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;

if (!in_array($status, [200, 201], true)) {
    exit(1); // the error code is not a payment code
}

// GET: no body, so the hash is sha256("")
[$status, $fetched] = signedRequest($config, 'GET', '/api/v3/payments/' . $payment['code']);
echo $status, ' ', $fetched['status'] ?? '', PHP_EOL;

Se rechazan las solicitudes con una marca de tiempo antigua, así que mantén el reloj de tu servidor sincronizado con NTP. Las firmas incorrectas repetidas desde una misma IP provocan un bloqueo temporal (429).

Reglas de las solicitudes

Idempotencia

Envía la cabecera Idempotency-Key en cada solicitud de escritura. Hace segura la repetición: por muchas veces que envíes la misma solicitud, la operación se ejecuta una sola vez.

  • Repetir una solicitud con la misma clave y el mismo cuerpo devuelve el resultado original y nunca crea una segunda operación. Usa GET cuando necesites el estado actual.
  • La misma clave con un cuerpo distinto devuelve 409 con el código 9000010; una clave no válida devuelve 422 con el código 1000009.
  • Genera una clave aleatoria nueva (vale un UUID) para cada operación nueva y guárdala con tu pedido para que un reintento pueda reutilizarla.

Límites de tasa

Las solicitudes están limitadas por clave API y por dirección IP. Si recibes 429, espera y reintenta más tarde.

  • Ante un 429, espera lo indicado en Retry-After si existe y reintenta con retroceso exponencial y jitter.
  • Ante un 503, reintenta más tarde con la misma Idempotency-Key y el mismo cuerpo. Resuelve los demás errores 4xx antes de reintentar; usa una clave nueva solo para una operación nueva.

Si se rechaza una solicitud, busca su code en Códigos de error. Las causas más habituales: firma incorrecta (9000001), marca de tiempo antigua (9000002), credenciales erróneas (9000003) e IP que no está en la lista permitida (9000012).

Pagos

Crea un pago y lee su estado.

Estados del pago

CampoDescripción
createdEl pago está abierto. Su plazo está en expires_at; la página de pago es válida durante 24 horas.
paidEl pago se ha cobrado. Se envía una notificación payment.paid.
expiredLa página de pago caducó sin un pago confirmado.

Consulta el estado actual con GET /api/v3/payments/{code}. Tras un rechazo, el cliente puede volver a intentarlo en la misma payment_url mientras el pago esté created. Si está expired, crea un pago nuevo con otra merchant_reference y otra Idempotency-Key.

Reglas

  • El importe debe estar dentro de los límites de pago de tu cuenta. El límite se comprueba en dólares estadounidenses tras la conversión; por defecto es de 3.00 a 1100.00 USD. Fuera de ese rango, la API devuelve 422 con código 9000005. Si necesitas otro límite, consúltalo con CryptoAvans.
  • merchant_reference tiene como máximo 64 caracteres y es única por cuenta y entorno. No se puede reutilizar ni siquiera cuando un pago ha caducado (409, 9000007), así que envía una nueva para cada pago nuevo.
  • Lleva al cliente a payment_url como una página normal (navegación de nivel superior). La página de pago no se puede mostrar dentro de un iframe.
  • return_url es opcional. Cuando termina el pago, la página de resultado muestra un botón que lleva al cliente allí, con code, status y, si lo enviaste, merchant_reference añadidos a la consulta. No hay redirección automática.
  • Los campos desconocidos se ignoran. Confirma el pago con un webhook firmado o un GET desde tu servidor; el regreso del cliente a tu sitio es solo informativo.

En sandbox, payment_url abre la misma página de pago; paga con una tarjeta de prueba del Entorno de pruebas.

En producción, el pago se cobra de una tarjeta real y se abona en tu saldo tras su fecha valor.

POST/api/v3/paymentsRequiere Idempotency-Key

Crear un pago

La respuesta 201 incluye payment_url y expires_at. Lleva al cliente a payment_url; la página de pago es válida durante 24 horas. Si se devuelve 409 / 9000039, repite la misma solicitud con la misma Idempotency-Key.

Cuerpo de la solicitud

CampoTipoObligatorioDescripción
amountstringObligatorioImporte positivo, como cadena decimal de escala fija con 2 decimales (p. ej. "250.00"), en currency_code. Debe estar dentro de los límites de pago de tu cuenta (comprobados en USD tras convertir al tipo actual; consultad los límites en API Key e Integración del panel); de lo contrario, la solicitud devuelve 422 9000005.
currency_codestringObligatorioMoneda de origen.
USDEURAEDTRY
merchant_referencestring | nullOpcionalTu propia referencia de pedido (máximo 64 caracteres). Única por cuenta y entorno: una referencia no se puede reutilizar, ni siquiera para un pago caducado (409 9000007). Opcional.
maxLength 64
webhook_urlstring | nullOpcionalURL https:// que recibe las notificaciones firmadas (payment.paid, payment.refunded, payment.failed). El host debe resolverse por DNS a una dirección pública (sin IP privada ni reservada, sin localhost); la URL tiene como máximo 500 caracteres y no debe contener nombre de usuario ni contraseña. Opcional: si se omite, se usa la URL de webhook guardada en el panel para el entorno de tu clave de firma; si no hay ninguna guardada, no se envía ninguna notificación.
maxLength 500
merchant_namestring | nullOpcionalNombre que se muestra en la pantalla de pago. Opcional; por defecto es Hesap <account number> (tu número de cuenta de 6 dígitos). Un valor de más de 120 caracteres se trunca, no se rechaza.
return_urlstring · uri | nullOpcionalreturn_url opcional (hasta 2048 caracteres). Producción exige HTTPS; sandbox también acepta HTTP en localhost, 127.0.0.1 o [::1]. No se permiten información de usuario, fragmentos, caracteres de control ni espacios. Los nombres de dominio internacionales se normalizan a ASCII. URL no válida: 422 / 9000038. Cuando termina el pago, la página de resultado muestra un botón que lleva al cliente a esta dirección, con code, status y, si lo enviaste, merchant_reference añadidos a la consulta; no hay redirección automática. No la cambies al reintentar con la misma Idempotency-Key.
maxLength 2048

Cuerpo de la solicitud · Ejemplo

{
  "amount": "10.00",
  "currency_code": "USD",
  "merchant_reference": "INV-104",
  "merchant_name": "Test Shop",
  "return_url": "https://shop.example.invalid/return?basket=104"
}

limits contiene currency_code, min_amount y max_amount (null: sin límite superior). Los importes de abajo son ejemplos, no tus límites.

HTTP 422 / 9000005

{
  "type": "https://www.cryptoavans.com/errors/9000005",
  "title": "Invalid amount",
  "status": 422,
  "code": "9000005",
  "correlation_id": "example",
  "detail": "Amount outside the account limits.",
  "limits": {
    "currency_code": "USD",
    "min_amount": "25.00",
    "max_amount": null
  }
}

Respuesta · 201

Pago creado. Si se repite con la misma Idempotency-Key, devuelve 200 en su lugar.

Payment

CampoTipoDescripción
codestringCódigo corto del pago (4–16 caracteres en minúsculas).
merchant_referencestring | nullTu referencia de pedido, devuelta tal cual.
statusstringEstado del ciclo de vida del pago: created hasta que el cliente paga y luego paid; expired cuando el enlace caduca sin pagarse.
createdpaidexpired
amountstringImporte bruto, como cadena decimal de escala fija con 2 decimales, en currency_code.
currency_codestringMoneda de origen de este pago; lee el valor devuelto en lugar de suponer la moneda del ejemplo.
USDEURAEDTRY
environmentstringEntorno al que pertenece este pago.
sandboxlive
payment_urlstringLleva al cliente a la payment_url devuelta como una página normal (navegación de nivel superior); no construyas ni cambies la dirección tú mismo. La página no se puede incrustar en un iframe. No se envían datos de tarjeta a esta API.
created_atstring · date-timeFecha y hora de creación (UTC, ISO-8601).
expires_atstring · date-timeUTC. La página de pago es válida durante 24 horas desde su creación. Crea un pago nuevo tras la caducidad; no hay un endpoint de renovación. Verifica el pago con GET /api/v3/payments/{code} o con un webhook firmado; el regreso del cliente a tu sitio es solo informativo.
paid_atstring · date-time | nullCuándo se pagó, o null.
payment_url_kindstringSiempre platform (nuestra página de pago). El campo se mantiene para las integraciones existentes; provider_hosted ya no se devuelve.
platformprovider_hosted
cardMerchantCardSummary | nullDatos de la tarjeta del intento aprobado; null hasta pagar o si no están disponibles.
failurePaymentFailureSummary | nullÚltimo intento fallido mientras no esté pagado; en otro caso, null. No es definitivo: puede pagarse hasta expires_at. attempt_count cuenta los intentos fallidos entre reintentos.

Respuesta · Ejemplo

{
  "code": "a1b2c3d4",
  "merchant_reference": "INV-104",
  "status": "created",
  "amount": "10.00",
  "currency_code": "USD",
  "environment": "live",
  "payment_url": "https://pay.example.com/l/a1b2c3d4",
  "payment_url_kind": "platform",
  "created_at": "2026-10-05T12:00:00Z",
  "expires_at": "2026-10-06T12:00:00Z",
  "paid_at": null,
  "card": null,
  "failure": null
}

Otras respuestas

  • 200Repetición idempotente: se devuelve el pago original; no se crea un segundo pago.
  • 422Un importe fuera de los límites de la cuenta (9000005, incluido menos de 0.01 tras convertir o superar la capacidad) incluye limits en la moneda base de la cuenta. Sandbox aplica la misma validación. Otros errores pueden omitir limits.

Errores posibles

GET/api/v3/payments/{code}

Obtener el estado del pago

Parámetros

CampoTipoObligatorioDescripción
codestring · pathObligatorioEl código del pago.
pattern ^[a-z0-9]{4,16}$

Respuesta · 200

El pago.

Objeto: Payment (campos en Crear un pago).

Respuesta · Ejemplo

{
  "code": "ab34cdef",
  "merchant_reference": "ORDER-2026-0001",
  "status": "created",
  "amount": "250.00",
  "currency_code": "USD",
  "environment": "live",
  "payment_url": "https://example.com/l/ab34cdef",
  "created_at": "2026-09-26T09:00:00.000Z",
  "expires_at": "2026-09-27T09:00:00.000Z",
  "paid_at": null,
  "payment_url_kind": "platform"
}

Errores posibles

Intentos fallidos

Payment.card contiene los datos del intento aprobado; failure.card, los del último intento fallido. Los valores no disponibles pueden ser null. El nombre del titular es un dato personal: tratadlo según vuestras obligaciones legales. Nunca se proporciona el número completo ni la fecha de caducidad.

payment.failed se envía una vez, con el primer intento fallido: rechazo de tarjeta, fallo de 3-D Secure o cancelación del pagador. El pago sigue created; se puede reintentar hasta expires_at y después puede llegar payment.paid. Otros fallos actualizan Payment.failure sin un nuevo evento. attempt_count cuenta los intentos fallidos; detail_code, si existe, es informativo. Usad los valores estables de failure.code siguientes.

PaymentFailure

CampoTipoObligatorioDescripción
detail_codestring | nullObligatorioCódigo de respuesta detallado de la red de pago, por ejemplo 058 o 511; los códigos de sandbox con punto siguen siendo compatibles. Sin transacción: timeout con detalle null. Las integraciones deben decidir según el campo estable code.
pattern ^[0-9]{3}(?:\.[0-9]{3})?$
cardMerchantCardSummary | nullObligatorioDatos opcionales de tarjeta del intento aprobado o fallido verificado. issuing_country usa ISO 3166-1 alpha-3. No se entrega el número completo, la caducidad, el código de seguridad ni identificadores exclusivos de la entidad.
codestringObligatorioMotivo estable y seguro de la última transacción de pago o, en su defecto, de la autenticación. 511/51: insufficient_funds; 058/004: authentication_failed; 022: timeout; 001/002: cancelled; 527/501: declined; 526: provider_error. Sin transacción, o autenticación correcta sin pago: timeout. Fallos desconocidos: declined. Sin mensaje del proveedor ni datos sensibles de la tarjeta.
declinedinsufficient_fundsauthentication_failedcard_invalidcancelledprovider_errorexpired_cardinvalid_cvvtimeout
occurred_atstring · date-timeObligatorioHora de finalización del intento fallido en UTC con milisegundos.

Webhooks

Las notificaciones JSON firmadas incluyen payment.paid, payment.failed y payment.refunded. Procesad solo tipos conocidos e ignorad los desconocidos; un intento fallido no es un resultado definitivo.

Verifica cada notificación

  1. Lee el cuerpo bruto antes de analizar JSON. Rechaza las firmas o marcas de tiempo ausentes o mal formadas. Los ejemplos rechazan marcas de tiempo que difieran más de 300 segundos del reloj de tu servidor; mantenlo sincronizado.

  2. Reconstruye la cadena canónica con X-CAV-Timestamp, POST, la ruta de tu URL de webhook (sin cadena de consulta) y el sha256 del cuerpo bruto.

  3. Calcula HMAC-SHA256 con tu clave secreta y compáralo con X-CAV-Signature en tiempo constante. Usa la clave secreta de la clave que creó el pago, aunque la hayas renovado desde entonces.

  4. Tras verificar la firma, contrasta environment, el código de pago, el importe y la moneda del cuerpo firmado con tu pedido. Usa event_type del cuerpo firmado para distinguir payment.paid / payment.failed de payment.refunded; un reembolso no debe activar la entrega del pedido.

  5. Deduplica de forma atómica con event_id del cuerpo verificado. Compáralo con X-CAV-Delivery sin distinguir mayúsculas y rechaza las discrepancias; la cabecera en sí no está firmada. No dependas del orden de entrega; comprueba los eventos inesperados mediante los endpoints de estado.

  6. Responde rápido con 2xx después de guardar de forma duradera el evento verificado o terminar de procesarlo. Si no puedes guardarlo, devuelve un error para permitir el reintento. Devuelve 2xx para un duplicado ya guardado. El cuerpo de la respuesta se ignora y no se siguen redirecciones.

Usa un certificado TLS válido emitido por una autoridad de certificación pública; la comprobación del certificado no se puede desactivar.

Ejemplos de verificación de firma

<?php
// $path is the path of YOUR webhook URL (no query string), e.g. /payment-webhook
function isValidWebhook(string $rawBody, array $headers, string $path, string $secret): bool
{
    $headers = array_change_key_case($headers, CASE_LOWER);
    $timestamp = $headers['x-cav-timestamp'] ?? '';
    $signature = $headers['x-cav-signature'] ?? '';
    if (!is_string($timestamp) || !is_string($signature)
        || preg_match('/^[0-9]{10}$/D', $timestamp) !== 1
        || preg_match('/^[0-9a-f]{64}$/D', $signature) !== 1
        || abs(time() - (int) $timestamp) > 300) {
        return false;
    }
    $canonical = implode("\n", [$timestamp, 'POST', $path, hash('sha256', $rawBody)]);

    return hash_equals(hash_hmac('sha256', $canonical, $secret), $signature);
}

$rawBody = file_get_contents('php://input');
$headers = [
    'X-CAV-Timestamp' => $_SERVER['HTTP_X_CAV_TIMESTAMP'] ?? '',
    'X-CAV-Signature' => $_SERVER['HTTP_X_CAV_SIGNATURE'] ?? '',
];
if (!isValidWebhook($rawBody, $headers, '/payment-webhook', getenv('MERCHANT_API_SECRET'))) {
    http_response_code(401);
    exit;
}

// After verification: compare event_id from the signed body with X-CAV-Delivery (case-insensitive),
// save the event once (deduplicate by event_id), then answer 2xx. On a save error answer 5xx so it is retried.
$event = json_decode($rawBody, true);
http_response_code(204);
const crypto = require("node:crypto");

// path is the path of YOUR webhook URL (no query string), e.g. /payment-webhook
function isValidWebhook(rawBody, headers, path, secret) {
  const timestamp = headers["x-cav-timestamp"] ?? "";
  const signature = headers["x-cav-signature"] ?? "";
  if (typeof timestamp !== "string" || typeof signature !== "string"
      || !/^[0-9]{10}$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)
      || Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const canonical = [timestamp, "POST", path, bodyHash].join("\n");
  const expected = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// rawBody must be the untouched request bytes (Buffer), not re-serialised JSON.
// After verification, deduplicate using event_id from the signed body; check X-CAV-Delivery matches.
// Check event_type and order fields, then durably save before 2xx (see the verification steps).
module.exports = { isValidWebhook };
import hashlib
import hmac
import re
import time


def is_valid_webhook(raw_body: bytes, headers: dict, path: str, secret: str) -> bool:
    """path is the path of YOUR webhook URL (no query string), e.g. /payment-webhook."""
    headers = {key.lower(): value for key, value in headers.items()}
    timestamp = headers.get("x-cav-timestamp", "")
    signature = headers.get("x-cav-signature", "")
    if (not isinstance(timestamp, str) or not isinstance(signature, str)
            or not re.fullmatch(r"[0-9]{10}", timestamp)
            or not re.fullmatch(r"[0-9a-f]{64}", signature)
            or abs(int(time.time()) - int(timestamp)) > 300):
        return False
    canonical = "\n".join([timestamp, "POST", path, hashlib.sha256(raw_body).hexdigest()])
    expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


# After verification, deduplicate by event_id from the signed body; check X-CAV-Delivery matches.
# Check event_type and order fields, then durably save before 2xx (see the verification steps).

Reintentos

Cualquier respuesta distinta de 2xx, un tiempo de espera agotado (unos 10 segundos) o un error de conexión cuenta como intento fallido. Se hacen hasta 6 intentos y entre ellos se espera, como mínimo, el tiempo indicado abajo. Tras el último intento fallido, la entrega se marca como failed; puedes volver a encolarla con POST /api/v3/webhooks/deliveries/{id}/retry.

El cuerpo y X-CAV-Delivery son iguales en cada intento; X-CAV-Timestamp y X-CAV-Signature se recalculan cada vez. El id de la entrega, la cabecera X-CAV-Delivery y event_id en el cuerpo son el mismo UUID en mayúsculas; compáralo sin distinguir entre mayúsculas y minúsculas.

IntentoEspera previa
1—
260 s (1 min)
3300 s (5 min)
4900 s (15 min)
53600 s (1 h)
621600 s (6 h)

Notificaciones enviadas a tu servidor

EventoCuándo se envía
payment.paidSe cobró un pago.
payment.failedEl primer intento fallido de un pago que sigue abierto. No es definitivo: el cliente puede volver a intentarlo.
payment.refundedSe completó un reembolso.
POSTpayment.paid

Notificación payment.paid firmada a la URL del comercio

Cabecera

CampoTipoObligatorioDescripción
X-CAV-Eventstring · headerObligatorio
payment.paid
X-CAV-Environmentstring · headerObligatorio
sandboxlive
X-CAV-Deliverystring · uuid · headerObligatorioId de la entrega. No cambia en los reintentos.
X-CAV-Timestampstring · headerObligatorio
pattern ^[0-9]{10}$
X-CAV-Signaturestring · headerObligatorio
pattern ^[0-9a-f]{64}$

Cuerpo de la solicitud · Ejemplo

{
  "event_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "event_type": "payment.paid",
  "environment": "sandbox",
  "occurred_at": "2026-10-05T12:01:00.000Z",
  "payment": {
    "code": "a1b2c3d4",
    "merchant_reference": "INV-104",
    "status": "paid",
    "amount": "10.00",
    "currency_code": "USD",
    "created_at": "2026-10-05T12:00:00.000Z",
    "paid_at": "2026-10-05T12:01:00.000Z",
    "card": {
      "brand": "visa",
      "bin": "400000",
      "last4": "0010",
      "holder_name": "TEST CARDHOLDER",
      "issuing_country": "TUR",
      "issuing_bank": "TEST BANK",
      "funding_type": "credit",
      "category": "STANDARD"
    }
  }
}
POSTpayment.failed

Notificación firmada de intento de pago fallido

Cuerpo de la solicitud · Ejemplo

{
  "event_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "event_type": "payment.failed",
  "environment": "sandbox",
  "occurred_at": "2026-10-05T12:01:00.000Z",
  "payment": {
    "code": "a1b2c3d4",
    "merchant_reference": "INV-104",
    "status": "created",
    "amount": "10.00",
    "currency_code": "USD",
    "created_at": "2026-10-05T12:00:00.000Z",
    "expires_at": "2026-10-06T12:00:00.000Z"
  },
  "failure": {
    "code": "declined",
    "occurred_at": "2026-10-05T12:01:00.000Z",
    "detail_code": null,
    "card": {
      "brand": "visa",
      "bin": "400000",
      "last4": "0010",
      "holder_name": "TEST CARDHOLDER",
      "issuing_country": "TUR",
      "issuing_bank": "TEST BANK",
      "funding_type": "credit",
      "category": "STANDARD"
    }
  }
}

El sobre es el mismo que en payment.paid. payment lleva expires_at en lugar de paid_at, y failure describe el intento (consulta Intentos fallidos).

Para consultar las entregas o volver a poner en cola una fallida, consulta Entregas de webhook.

Entorno de pruebas

Las solicitudes de sandbox usan datos virtuales y nunca mueven dinero real. Abre payment_url y paga con una de las tarjetas de prueba de abajo.

Cómo funciona

Toda solicitud firmada con una clave de sandbox se ejecuta sobre datos virtuales aislados. Las reglas, los campos y los códigos de error son los mismos que en producción. payment_url abre la misma página de pago que ven tus clientes en producción. Cuando se aprueba un pago de prueba, se envía payment.paid a la URL de webhook del pago con environment igual a sandbox.

  • La página muestra un aviso de prueba y solo acepta las tarjetas de prueba de abajo; los números de tarjeta reales se rechazan. Con las tarjetas 3-D Secure te pide que apruebes o rechaces la autenticación. La API no acepta números de tarjeta; introduce la tarjeta en la página de pago.
  • No interviene dinero real, ninguna tarjeta real ni ninguna transacción en la cadena, y nada llega a tu saldo de producción.

Tarjetas de prueba del sandbox

Usa estos números sintéticos solo en la página de pago de un pago de sandbox, nunca en un pago real. Introduce cualquier nombre de titular de al menos 2 letras, una fecha de caducidad del mes actual o posterior y un CVV de 3 o 4 dígitos.

Número de tarjetaCódigo de resultadoComportamiento
4000000000000010approvedAprobado sin 3-D Secure.
4000000000000051declinedRechazado sin 3-D Secure.
4000000000000002three_ds_approvedLa página de pago muestra un paso de 3-D Secure; si lo apruebas, el pago se aprueba y, si lo rechazas, el pago se rechaza.
4000000000000093three_ds_declinedLa página de pago muestra un paso de 3-D Secure; el pago se rechaza elijas el resultado que elijas.
4000000000000044timeoutEl resultado queda pendiente (sin resultado final); no lo trates como un rechazo definitivo. El intento se cierra más tarde como caducado.

Un pago rechazado permanece en created y envía payment.failed una vez, en su primer intento fallido; el cliente puede volver a intentarlo (10 intentos por hora y por dirección IP). La tarjeta de tiempo de espera agotado y los importes que terminan en .77 dejan el resultado pendiente: la página muestra un resultado pendiente, no se envía ningún webhook y el resultado nunca se introduce a mano. Pasados unos 30 minutos el intento se cierra como caducado; el pago permanece en created y el cliente puede empezar de nuevo. La regla de .77 se evalúa sobre el importe en dólares estadounidenses.

Registro de solicitudes

En el panel, “Registro de solicitudes de sandbox” muestra tus solicitudes de sandbox recientes: los últimos 90 días, con la IP de origen conservada durante 30 días. Las solicitudes que no superaron la comprobación de firma o de credenciales no se registran; localízalas con el correlation_id de la respuesta. Las solicitudes de producción no se registran.

Las pruebas de reembolsos, retiros y disputas se describen en la página Operaciones de la cuenta.

Códigos de error

Los errores se devuelven como application/problem+json. El campo code es uno de los códigos siguientes; incluye el correlation_id cuando contactes con soporte. title y detail siguen la cabecera Accept-Language; en tu código, apóyate solo en code.

Ejemplo de respuesta de error

application/problem+json

{
  "type": "https://www.cryptoavans.com/errors/9000011",
  "title": "Signature headers are missing",
  "status": 400,
  "detail": "X-CAV-Account-No, X-CAV-Api-Key, X-CAV-Timestamp and X-CAV-Signature are mandatory.",
  "code": "9000011",
  "correlation_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6"
}

Tabla de códigos

Si se pierde la respuesta o recibes 500 o 503, el resultado puede ser desconocido. Reintenta con la misma Idempotency-Key y el mismo cuerpo, firmando de nuevo con una marca de tiempo actual. Respeta Retry-After en 503. No crees otro pago solo porque se agotó el tiempo de espera.

CódigoHTTPTítulo
9000001401La firma no es válida
9000002401Marca de tiempo fuera de la ventana
9000003401Las credenciales no son válidas
9000004403La cuenta no puede realizar transacciones
9000005422El importe no es válido
9000006422La referencia del comerciante no es válida
9000007409La referencia del comerciante ya se ha utilizado
9000008422La dirección de notificación no es válida
9000009404Pago o reembolso no encontrado
9000010409Conflicto de idempotencia
9000011400Faltan las cabeceras de firma
9000012403IP no permitida
9000013409Sandbox no disponible
9000014409Ya existe una solicitud de reembolso o anulación
9000015404Disputa no encontrada
9000016409Respuesta a la disputa no aceptada
9000017422La dirección TRC20 no es válida
9000018422Se requiere confirmación de la dirección
9000019422El importe no es válido
9000020409Saldo insuficiente
9000021409Se superó el límite diario de retiros
9000022409Retiro en proceso
9000023404Retiro no encontrado
9000024404Entrega de webhook no encontrada
9000025409No se puede reintentar
9000026503El esquema de sandbox no está listo
9000027409El saldo de sandbox está ocupado
9000030409Abono de sandbox no válido
9000033409Saldo de sandbox insuficiente
9000038422URL de retorno no válida
9000039409Se está preparando el pago
9000040403El acceso a la API está desactivado
1000001422Error de validación compartido
1000002404No se encontró ningún registro compartido
1000005429Se superó el límite de solicitudes compartido
1000006500Error del servidor compartido
1000007405Método no admitido compartido
1000009422Clave de idempotencia no válida compartido
1000011503El servicio no está disponible en este momento compartido
1000013503La infraestructura de claves no está lista compartido
1000014503Servicio en mantenimiento compartido
1000020422Formato de importe no válido compartido
1000021422Moneda no admitida compartido
1000023422El importe supera la capacidad compartido
1000030422La dirección no se puede usar
3000001503El tipo de cambio no está disponible en este momento compartido

Lista de verificación para pasar a producción

Completa todos los puntos antes de pasar a producción.

  • Has probado de extremo a extremo en sandbox los flujos de pago, webhook, reembolso y retiro.

  • La clave de producción y la clave secreta se guardan solo en tu servidor y no se escriben en los registros.

  • Tu lista de IP permitidas y tu URL de webhook HTTPS de producción están configuradas.

  • La verificación de la firma del webhook está activa y rechaza las firmas no válidas; el manejador responde rápido con 2xx y tolera entregas repetidas.

  • Los pedidos solo se completan cuando el webhook o GET /payments/{code} muestra paid, nunca a partir del navegador del cliente.

  • Las solicitudes de escritura envían una Idempotency-Key y los reintentos la reutilizan; cada pago recibe una merchant_reference nueva.

  • El reloj de tu servidor está sincronizado con NTP.

  • Tu cuenta tiene asignado un terminal de producción y has hecho un pequeño pago real junto con CryptoAvans.

Habla con tu gestor de cuenta sobre el terminal de producción y la prueba final de extremo a extremo. Entrar al panel