Core

Webhooks & signing

Subscribe to events with HMAC-SHA256 signed deliveries. Pick managed inbox (we host) or your own backend URL.

Subscribe to event types from your dashboard or via the API. Each delivery includes a signed header so you can verify the payload was issued by Key2Pay. Hay dos formas de recibir los eventos — usá la que mejor encaje con tu stack.

Two delivery modes

Managed inbox
Recomendado para empezar
Te asignamos una URL en NUESTRO dominio (merchant.key2pays.com/api/webhooks/inbox/<tu-shop>). Los eventos llegan ahí y los ves en /dashboard/webhooks sin levantar infraestructura. Es la opción default cuando creás un webhook sin URL en el dashboard.
Tu propia URL
Vos hosteás el endpoint (ej. https://acme.com/webhooks/key2pay) y nosotros le hacemos POST con el evento firmado. Más control, ideal para producción cuando tu backend ya está listo para procesar eventos automáticamente.
Path de adopción típico: sandbox arranca con managed inbox para inspeccionar las cargas y ajustar el handler; cuando todo se ve bien, agregás una segunda subscripción con tu URL real y deshabilitás la managed. Las dos pueden coexistir, así que también podés tener producción usando ambas (la managed como audit log de backup, la externa como el handler real).
text
X-Key2Pay-Signature: t=1714672890,v1=2c8a8…b7
X-Key2Pay-Event: payment.completed
X-Key2Pay-Delivery: dlv_1zP9e…

Verifying a delivery

javascript
import crypto from "node:crypto";

export function verify(payload, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=")),
  );
  const t = Number(parts.t);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${payload}`)
    .digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
    throw new Error("invalid_signature");
  }
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) {
    throw new Error("stale_timestamp");
  }
}
Always verify with a constant-time comparison. Reject any delivery whose timestamp is more than 5 minutes old to defend against replay.

Event types

EventFires when…
payment.completedCapture succeeded — funds landed in the merchant balance.
payment.failedCharge attempt failed or the customer abandoned a pending voucher/PIX.
payment.refundedA previously-completed transaction was refunded in full or in part.
chargeback.createdA dispute was opened against a completed transaction. Funds are frozen.
claim.openedLegacy alias for chargeback.created — kept for back-compat. Prefer chargeback.created on new code.
claim.resolvedInternal claim (refund or chargeback) reached a final resolution.
settlement.closedA settlement batch was closed and the crypto payout was initiated.
settlement.pdf_readyThe PDF receipt for a closed settlement is ready to download.
withdrawal.completedA merchant withdrawal finished — funds left the platform.
withdrawal.failedA merchant withdrawal failed and the funds were returned to balance.
payment.capturedDeprecated alias for payment.completed. Subscribe to payment.completed instead.

Register an endpoint

Vía dashboard/dashboard/webhooks tiene un wizard con dos opciones (Managed inbox / URL propia). El secret HMAC se muestra una sola vez después de crear; guardalo.

Vía APIPOST /api/v1/webhooks:

bash
# Tu propia URL (modo externo):
curl https://sandbox.key2pays.com/api/v1/webhooks \
  -H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://example.com/webhooks/key2pay",
        "events": ["payment.completed", "payment.failed", "payment.refunded"]
      }'

# Managed inbox (modo gestionado — omití el campo url):
curl https://sandbox.key2pays.com/api/v1/webhooks \
  -H "Authorization: Bearer sk_test_51N8mP...exampleK3Y" \
  -H "Content-Type: application/json" \
  -d '{
        "events": ["*"]
      }'
# Respuesta: { "url": "https://sandbox.key2pays.com/api/webhooks/inbox/<shopSlug>",
#              "managed": true, "secret": "whsec_…", … }
La respuesta incluye managed: true cuando se autogeneró la URL. El secret se devuelve EXACTAMENTE UNA VEZ — guardalo en tu vault aunque uses managed inbox (el día que migres a tu propia URL podés reutilizarlo).