Cómo confirmar automáticamente el pago de un pedido mediante webhook
Vincula una factura con un pedido, verifica la firma del webhook y protege la entrega frente a duplicados, pagos insuficientes y reversiones.
Actualizado:
Para que tu sitio marque un pedido como pagado automáticamente, crea una factura en Cryptanio y configura un webhook: una notificación que el servicio envía a tu servidor cuando cambia el estado del pago. El servidor verifica la autenticidad del mensaje, localiza el pedido y comprueba el estado de la factura. Después puede conceder el acceso comprado o enviar el pedido para su preparación.
Usaremos el ejemplo de una tienda que crea una factura con un precio en dólares como unidad de cuenta, mientras el comprador elige una criptomoneda y una red disponibles en la página de pago. Los fondos llegan a las direcciones de la tienda. Los pedidos y la entrega del producto siguen en su aplicación; Cryptanio relaciona la factura con la transferencia e informa de su estado. Esta integración requiere un desarrollador o la posibilidad de modificar la parte de servidor del sitio.
Prepara el proyecto y las direcciones de pago
En tu cuenta de Cryptanio selecciona un proyecto y añade direcciones de recepción que te pertenezcan. Conéctalas al servicio Payments: el seguimiento normal de una dirección no basta para emitir facturas. Comprueba que las direcciones estén activas y correspondan a la red elegida. Los pagos reales requieren direcciones del modo de funcionamiento real.
Prepara varias direcciones si los clientes pueden pagar pedidos a la vez. Una dirección puede estar ocupada por una factura abierta o permanecer temporalmente indisponible tras su cierre. La integración debe mostrar un error claro si no hay ninguna libre y permitir intentarlo de nuevo más tarde.
Crea una clave API y guárdala en el servidor de la tienda. La utilizarás para llamar a https://api.cryptanio.com/v1. No la incluyas en el JavaScript de la página, una aplicación móvil ni un repositorio público.
Configura la dirección que recibe las notificaciones
Abre las «Notificaciones» del proyecto elegido. Si «Utilice los ajustes de notificación de cuenta» está activado, configura Webhook desde «Configuración de la cuenta», o desactiva la herencia y establece la dirección propia del proyecto.
En la tarjeta Webhook, introduce la dirección HTTPS de tu gestor en «URL del punto final» y pulsa «Guardar». El gestor debe aceptar directamente las solicitudes POST: no se utilizan redirecciones a otra URL. Obtén el «Secreto de firma» en la misma tarjeta y guárdalo en el servidor, separado de la clave API.
Comprueba que Webhook no esté pausado. Procesa payment.confirmed y payment.rolled_back en tu gestor, y utiliza payment.pending para un estado intermedio si hace falta. Antes de completar el pedido, verifica el modo real y las demás condiciones indicadas abajo. No es necesario activar un canal Webhook independiente para la dirección de la factura.
Crea una factura para un pedido concreto
Primero guarda el pedido en la base de datos de la tienda. Después el servidor crea la factura mediante POST /v1/invoices. En reference envía tu identificador del pedido. En este ejemplo, el precio del pedido es de 12.50 USD: invoice_currency e invoice_amount definen el precio, mientras el comprador elige una red y un activo disponibles en la página de pago. Por eso, no envíes chain, asset, amount, address ni tolerance en esta solicitud. Indicar USD no significa aceptar dinero fiduciario ni cambiar por dólares la criptomoneda recibida.
#!/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
Antes de ejecutar el ejemplo, define CRYPTANIO_API_KEY, CRYPTANIO_PROJECT_ID y CRYPTANIO_OPERATION_ID en tu servidor. Usa una clave API de modo real con permiso para crear facturas; CRYPTANIO_PROJECT_ID debe identificar el proyecto Payments preparado. CRYPTANIO_OPERATION_ID es el identificador guardado de la operación de creación de este pedido: de 16 a 80 letras ASCII, dígitos, guiones o guiones bajos. El ejemplo lo envía en la cabecera Idempotency-Key, no en un campo JSON. Genera un valor diferente para un pedido nuevo; conserva el mismo valor al reintentar la misma solicitud.
Si la creación tiene éxito, la API devuelve HTTP 201. Guarda el id de la factura y el checkout_url recibido junto al pedido, y dirige al comprador a esa URL. Hasta que se elija un método de pago, todavía no se han asignado la red, la dirección ni el importe en criptomoneda. En la página de pago, el comprador elige un método disponible y recibe los datos vigentes y el plazo de pago. Si tu propia interfaz muestra los datos de pago, utiliza los de ese intento concreto, el amount exacto y su plazo expires_at; no redondees el importe ni lo sustituyas por asked. Los enlaces devueltos por las solicitudes GET posteriores y por la lista de facturas son de solo lectura; guarda por separado el enlace interactivo recibido al crear la factura y compártelo únicamente con el comprador correspondiente.
Si se interrumpe la conexión después de enviar la solicitud, repite el mismo cuerpo con el mismo Idempotency-Key. Un reintento correcto devuelve HTTP 201 y el mismo id. Cambiar los parámetros con una clave ya utilizada provoca HTTP 409. reference relaciona la factura con el pedido, pero no sustituye la clave de idempotencia. No cambies la clave solo porque se haya agotado el tiempo de espera.
Verifica la autenticidad del webhook entrante
Cryptanio envía X-Timestamp, X-Signature, X-Event-Id, X-Event-Type y X-Attempt. La firma se calcula así:
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
raw_body contiene los bytes originales de la solicitud. Obténlos antes de analizar el JSON: volver a serializarlo puede cambiar los espacios o el orden de los campos e invalidar la comprobación. Limita el tamaño aceptado del cuerpo y rechaza las solicitudes sin una firma válida.
El ejemplo siguiente utiliza una ventana de validez de cinco minutos. Es una configuración de tu gestor; para que funcione correctamente, el reloj del servidor debe estar sincronizado.
import hashlib
import hmac
import re
import time
def verify_signature(secret, timestamp, raw_body, signature):
if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
return False
if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
La función comprueba la firma y la hora de la solicitud. Una vez verificada, analiza el JSON y comprueba schema_version="v1", el project_id esperado, mode="live" y sandbox=false.
Para tomar decisiones utiliza event_id y event_type del cuerpo firmado. Puedes comparar con ellos los encabezados correspondientes; esos encabezados de evento no están protegidos por la firma por sí solos. Ejecuta la lógica de pago únicamente para el objeto payment esperado y con dirección in.
Distingue la confirmación de la transferencia del pago del pedido
Los principales eventos para la tienda son:
payment.pending: la transferencia se ha detectado y espera confirmación;payment.confirmed: la transferencia ha alcanzado las confirmaciones necesarias;payment.rolled_back: una transferencia detectada anteriormente se ha revertido tras un cambio en la cadena de bloques.
Un evento payment.confirmed todavía no significa que un pedido concreto esté completamente pagado. Comprueba el objeto invoice y relaciona invoice.id e invoice.reference con el pedido guardado. Un mensaje sin factura asociada no debe asignarse automáticamente a una compra solo porque el importe sea parecido.
Antes de entregar el producto, consulta el estado actual con GET /v1/invoices/{id}. Verifica id, project_id, reference, el precio del pedido en invoice_currency e invoice_amount, y sandbox=false. Comprueba la red, el activo, la dirección y el intento de pago elegido; si hay varios intentos, ten en cuenta settled_attempt_id y settled_attempt. El estado de factura que corresponde a un pago correcto es paid; pending, seen y partial no autorizan la entrega. Que el comprador vuelva a tu sitio o abra el enlace de pago no confirma el pago.
Un pago insuficiente puede tener el estado underpaid, un pago parcial a la espera de un complemento tiene partial y una entrada tardía tiene late. Si se paga de más, la política del proyecto puede dejar la factura en paid o establecer overpaid con needs_decision=true. Los estados underpaid, overpaid y late requieren una decisión aparte antes de entregar el pedido. La API envía amount_raw y paid como cadenas. Para compararlos, interprétalos como enteros exactos en las unidades mínimas del activo; decimals define la escala. No uses cálculos con coma flotante binaria. Define antes cómo tratar la diferencia; esto no implica una devolución automática.
Gestiona los reintentos y las reversiones
La entrega puede incluir mensajes repetidos. Guarda el evento verificado en un almacenamiento persistente y usa event_id como clave única. La repetición de un evento ya aceptado debe recibir una respuesta de éxito sin volver a entregar el producto.
El identificador de la transacción no basta para descartar todos los mensajes posteriores: una transferencia pasa por distintos estados. Para la combinación de chain, tx_id, index, address y la identidad del activo, guarda la última payment.revision aplicada. Aplica una transición solo si su revision es mayor que la ya procesada.
Registra de forma coherente el cambio del pedido y la tarea de entrega, con protección frente al procesamiento simultáneo. La tarea debe tener una clave única del pedido. Justo antes de ejecutarla, comprueba que la entrega sigue autorizada. Si llega una reversión, actualiza el estado y detén las entregas pendientes; para el acceso ya concedido, prevé una revisión o suspensión.
Comprueba todo el recorrido
Antes de un pago real, comprueba el análisis del cuerpo, la firma y la protección contra duplicados con datos preparados. Un evento de servicio webhook.test no confirma una compra ni sustituye la comprobación del proceso completo.
Después utiliza tu propio pedido de control para comprobar el proceso completo: creación de factura, transferencia, confirmación y actualización del pedido. Prueba por separado los eventos repetidos, el pago insuficiente y la reversión con datos preparados. Devuelve HTTP 2xx después de guardar de forma fiable el mensaje aceptado; si temporalmente no puedes guardarlo, devuelve un error del servidor para permitir un nuevo intento de entrega.
En la cuenta, compara el evento y el registro de entrega con la entrada de tu aplicación. La comprobación termina cuando un pedido confirmado se completa una sola vez y un evento intermedio, ajeno o repetido no provoca una entrega incorrecta.
Referencias: facturas de la API v1, verificación de la firma del webhook.