Saltearse al contenido

La búsqueda solo está disponible en las versiones de producción. Intenta construir y previsualizar el sitio para probarlo localmente.

Webhooks

Cuando un depósito llega a un estado final, Cashela envía un POST firmado a la notification_url del depósito con el resultado. Verifica la firma, actualiza tu pedido y responde 2xx.

Cuándo se envían notificaciones

Evento (cabecera X-Cashela-Event)Cuándo
partner.payin.transaction.updatedEl depósito llega a su estado final: COMPLETED, DECLINED o CANCELLED (también un depósito vencido).
partner.payin.transaction.voidedSe anuló un depósito con tarjeta. Su estado pasa a REVERSED.
partner.payin.transaction.refundedSe reembolsó un depósito con tarjeta: REVERSED si el reembolso es total, COMPLETED tras un reembolso parcial.
partner.payin.dispute.opened, partner.payin.dispute.won, partner.payin.dispute.lostSe abrió o se resolvió una disputa de tarjeta sobre el depósito.

El evento viaja solo en la cabecera X-Cashela-Event, nunca en el cuerpo. Ignora los nombres de evento que no conozcas: se pueden añadir nuevos.

Entrega y reintentos

  • Cashela envía un POST con Content-Type: application/json a la notification_url del depósito (solo HTTPS).
  • Responde cualquier 2xx para confirmar la recepción. Cualquier otro estado, un timeout o un error de conexión cuentan como entrega fallida, y Cashela reintenta.
  • Responde rápido y haz el trabajo pesado de forma asíncrona.
  • La misma notificación puede llegar más de una vez. Haz tu handler idempotente: procesa cada ref + status (+ evento) una sola vez.
  • Las notificaciones nunca se envían sin firma.
  • Si perdiste una, pide que se reenvíe con Reenviar una notificación, o consulta el depósito con Consultar un depósito.

Cabeceras

CabeceraDescripción
X-Cashela-EventEl evento que disparó esta notificación (tabla de arriba).
X-Cashela-TimestampHora Unix (segundos) en que se calculó la firma.
X-Cashela-NonceValor único por entrega (UUID).
X-Cashela-SignatureHMAC-SHA256 de {timestamp}.{nonce}.{body} con tu api_secret, en hexadecimal en minúsculas (64 caracteres).
X-Cashela-Trace-IdIdentificador de correlación del depósito. Cítalo a soporte.

Verificar la firma

El secreto es el api_secret de la clave API que creó el depósito mientras esa clave siga activa; si ya no existe, el de la clave activa más reciente de tu cuenta. Nunca se envía en una notificación.

  1. Lee X-Cashela-Timestamp, X-Cashela-Nonce y X-Cashela-Signature. Rechaza la petición si falta alguna.
  2. Calcula HMAC-SHA256 sobre el texto {timestamp}.{nonce}.{body} con tu api_secret, donde {body} es el cuerpo en bruto exactamente como llegó. Parsear y volver a serializar el JSON cambia los bytes, y la firma no coincidirá.
  3. Compara tu resultado con X-Cashela-Signature con una comparación de tiempo constante.
  4. Rechaza timestamps a más de 300 segundos (5 minutos) de tu reloj, y nonces que ya hayas visto.
// Express: monta esta ruta con express.raw({ type: "application/json" })
// para que req.body sea el Buffer en bruto.
const crypto = require("crypto");
function isValidCashelaNotification(req, apiSecret) {
const timestamp = req.get("X-Cashela-Timestamp");
const nonce = req.get("X-Cashela-Nonce");
const signature = req.get("X-Cashela-Signature");
if (!timestamp || !nonce || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", apiSecret)
.update(`${timestamp}.${nonce}.${req.body.toString("utf8")}`)
.digest("hex");
return expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Estos ejemplos comprueban el timestamp; además, guarda cada nonce al menos durante la ventana de tolerancia y rechaza los repetidos.

Vectores de prueba

Comprueba tu implementación con estos casos antes de salir a producción. Descárgalos en JSON: webhook-signatures.json.

CasoSecretoTimestampResultado esperado
validwhsec_test_0123456789abcdef1759838400Válida. Firma b2e80494a3da3d33cfb2c38b1c6c334efb46ed098b4f29f24760da6831de0f6c
tampered-bodyigualigualNo válida: el cuerpo cambió (status es CANCELLED) pero la firma es la de valid.
reserialized-bodyigualigualNo válida: el mismo JSON con formato. Firma los bytes en bruto.
wrong-secretwhsec_test_wrongigualNo válida.
stale-timestampwhsec_test_0123456789abcdef100No válida: la firma coincide, pero el timestamp está fuera de la tolerancia de 300 segundos.

Todos los casos usan el nonce 8f14e45f-ceea-467d-9a0b-000000000001 y, salvo donde se indica, exactamente este cuerpo (sin espacios ni salto de línea final):

{"ref":"PI-TEST-0001","external_identifier":"partner-order-0001","status":"COMPLETED","date":"2025-10-07T12:00:00.000000Z","amount":4750.5,"currency":"CLP","settlement_currency":"USD","net_amount":5,"processing_fee_amount":0.25,"fx_spread_amount":0,"tax_amount":0,"card":null}

Cuerpo de la notificación

El cuerpo tiene las mismas claves, en el mismo orden, en todos los estados y eventos.

{
"ref": "210000000001",
"external_identifier": "order-2026-0002",
"status": "COMPLETED",
"date": "2026-10-20T15:04:05.000000Z",
"amount": 5,
"currency": "USD",
"settlement_currency": "USD",
"net_amount": 4.85,
"processing_fee_amount": 0.15,
"fx_spread_amount": 0,
"tax_amount": 0,
"card": { "network": "visa", "last4": "4242" }
}
CampoTipoDescripción
refstringLa reference del depósito.
external_identifierstringTu identificador, tal como lo enviaste al crear el depósito.
statusstringEl estado del depósito después del evento: COMPLETED, DECLINED, CANCELLED o REVERSED.
datestringFecha y hora ISO-8601 del evento.
amountnumberLo que se cobró a tu cliente, en currency.
currencystringMoneda de amount.
settlement_currencystringMoneda de los cuatro importes siguientes.
net_amountnumberLo que se te acredita, en settlement_currency.
processing_fee_amountnumberComisión de procesamiento, en settlement_currency.
fx_spread_amountnumberDiferencial cambiario, en settlement_currency. 0 cuando no hubo conversión de moneda.
tax_amountnumberImpuestos, en settlement_currency.
cardobject | nullnetwork (visa, mastercard, amex, discover, o null si se desconoce) y last4 de la tarjeta que pagó. null en métodos sin tarjeta y antes del cobro. Nunca el número completo, el CVV, el vencimiento ni un token.

Cómo leer los importes:

  • amount / currency es lo que se cobró a tu cliente. En opciones de tarjeta que cobran en USD, es lo cobrado en USD (por ejemplo 5.00 USD), no la factura en moneda local. La factura sigue disponible como details.invoice_* en Consultar un depósito, y details.charged_amount / details.charged_currency coinciden con amount / currency de aquí.
  • Cuando currency y settlement_currency son distintas (por ejemplo, un método en moneda local acreditado en USD), net_amount + processing_fee_amount + fx_spread_amount + tax_amount no suma amount: están en monedas distintas.
  • En DECLINED y CANCELLED, los importes son la cotización con la que se creó el depósito, no dinero que se movió. No se cobró ni se acredita nada. Concilia dinero solo en COMPLETED y REVERSED.
  • En REVERSED, el cuerpo conserva los importes originales del depósito. El detalle de la anulación o el reembolso está en reversals.refunds de Consultar un depósito.

Reenviar una notificación

Pide a Cashela que envíe otra vez, ahora, la notificación de un depósito a su notification_url.

POST/api/v1/pay-in/transactions/{reference}/callback
  • {reference}: la reference del depósito, tu external_identifier o el id del depósito.
  • Solo para un depósito en estado final. Si no, 409 (PARTNER_PAYIN_CALLBACK_NOT_FINAL).
  • Limitado a 6 peticiones por minuto.
  • La respuesta es 200 aunque tu endpoint no haya aceptado la entrega: revisa data.callback.
Ventana de terminal
curl -X POST https://sandbox-api.cashela.com/api/v1/pay-in/transactions/210000000001/callback \
-u "$CASHELA_API_KEY:$CASHELA_API_SECRET" \
-H "Accept: application/json"
{
"success": true,
"data": {
"transaction": { "reference": "210000000001", "status": "COMPLETED", "…": "…" },
"callback": {
"status": "…",
"success": true,
"reference": "210000000001",
"http_status": 200
}
},
"message": "…"
}
Campo de data.callbackDescripción
successtrue si tu endpoint respondió 2xx.
http_statusLo que respondió tu endpoint, cuando respondió.
reasonSi falló sin respuesta, un código estable como callback_url_not_configured o callback_url_blocked.

Lista de control

  • Tu notification_url es HTTPS, pública y responde 2xx en pocos segundos.
  • Verificas la firma sobre el cuerpo en bruto, el timestamp y el nonce.
  • Tu handler es idempotente.
  • Lees el evento de X-Cashela-Event, no del cuerpo.
  • Concilias dinero solo en COMPLETED y REVERSED.
  • Puedes recuperar una notificación perdida con el endpoint de reenvío o consultando el depósito.