Webhooks: eventos, firmas y entrega
Registre un endpoint HTTPS público, demuestre control y reciba eventos firmados de factura y pago. La entrega se pone en cola de forma duradera; procesar el mismo evento nuevamente debe ser seguro para su pedido.
En esta página
De dónde provienen los eventos#
invoice.created no significa pagado. Los eventos de pago provienen de una operación de servidor verificada después de una lectura actual de la pasarela; un nombre de evento entrante por sí solo es insuficiente.
La entrada Pasarela → Kevmi y la entrega Kevmi → comerciante son tramos diferentes con secretos separados. El navegador, la navegación de retorno y los mensajes del widget no establecen el estado financiero.
| Evento | Fuente |
|---|---|
| invoice.created | Factura persistida |
| invoice.updated | Disponibilidad del enlace cambiada |
| payment.succeeded | Pago exacto verificado por el servidor |
| payment.failed | Operación fallida verificada |
| payment.refunded | Reembolso completo verificado |
| payment.partially_refunded | Reembolso parcial verificado |
| payment.disputed | Disputa verificada |
| payment.review_required | La operación requiere revisión adicional |
Registrar y verificar un endpoint#
Las URL no pueden apuntar a direcciones IP, localhost, redes privadas, puertos no estándar ni contener credenciales. El DNS se verifica antes de la entrega; las solicitudes POST salientes no siguen redirecciones. Una cuenta puede registrar hasta cinco endpoints.
El evento de verificación tiene livemode=false. Un código de estado HTTP exitoso por sí solo es insuficiente: el desafío debe coincidir exactamente. Rotar el secreto requiere verificación nuevamente.
- 1
En /account#webhooks en su cuenta de comerciante, ingrese una URL HTTPS pública, una descripción y los tipos de evento que necesite.
- 2
Guarde el secreto emitido en el almacenamiento del lado del servidor. Su valor completo se muestra una sola vez.
- 3
Verifique las firmas sobre los bytes originales de la solicitud.
- 4
Ejecute la verificación del endpoint. Para endpoint.verification, devuelva JSON que repita data.challenge: {"challenge":"el valor recibido"}.
- 5
Habilite el endpoint verificado e inspeccione el historial de entrega.
Sobre del evento#
id permanece igual en cada intento de entrega de un evento. createdAt es una marca de tiempo Unix en milisegundos, y apiVersion identifica la versión del contrato. data contiene el objeto persistido en el servidor; el ejemplo está abreviado.
livemode=true distingue un evento real del servidor de la verificación del endpoint. No significa que una factura esté pagada: verifica el tipo y los datos de pago. Maneja los tipos de evento conocidos y valida el esquema.
{
"id": "evt_example",
"type": "invoice.created",
"apiVersion": "2026-10-21",
"createdAt": 1792540800000,
"livemode": true,
"data": {
"invoice": {
"id": "lnk_example",
"orderId": "order-1042",
"currency": "USD",
"amountMinor": 500,
"vatCustomerPercent": 100,
"feeCustomerPercent": 0
}
}
}Verificar HMAC-SHA256#
Kevmi-Signature contiene t=segundos,v1=hex. La firma es HMAC-SHA256 sobre la cadena de la marca de tiempo, un punto y el cuerpo original: timestamp + "." + rawBody. Usa el secreto específico del endpoint y una comparación de tiempo constante.
Rechaza firmas cuya marca de tiempo difiera de la hora del servidor en más de 300 segundos en cualquier dirección. Sincroniza los relojes. No analices ni vuelvas a serializar JSON antes de la verificación: los espacios en blanco y el orden de las propiedades afectan los bytes firmados.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyKevmi(rawBody, header, secret, nowSeconds) {
const match = /^t=(\d{10}),v1=([a-f0-9]{64})$/.exec(header || '');
if (!match || Math.abs(nowSeconds - Number(match[1])) > 300) {
return false;
}
const expected = createHmac('sha256', secret)
.update(match[1] + '.')
.update(rawBody)
.digest();
const received = Buffer.from(match[2], 'hex');
return expected.length === received.length
&& timingSafeEqual(expected, received);
}
// Read bytes before JSON parsing; cap the request body.
// secret comes from your server secret store.
const valid = verifyKevmi(rawBody, signature, secret,
Math.floor(Date.now() / 1000));
if (!valid) throw new Error('Invalid webhook signature');
const event = JSON.parse(rawBody.toString('utf8'));
// Validate the event schema and persist its ID together
// with your order update in one database transaction.
// Return a 2xx response after durable acceptance.Reintentos y confirmación#
Cualquier respuesta 2xx confirma un evento ordinario. Persiste el evento de forma duradera antes de responder; el trabajo costoso puede continuar a través de tu propia cola. El tiempo de espera de respuesta es de ocho segundos. Un error de red o una respuesta que no sea 2xx programa un reintento.
Hay como máximo seis intentos: la entrega inicial más demoras de un minuto, cinco minutos, 30 minutos, dos horas y 12 horas después de fallos sucesivos. Estos son programas de cola, no garantías de hora exacta de entrega. Agotar los intentos marca la entrega como fallida.
La entrega es al menos una vez y no promete un orden estricto. Persiste el ID único del evento en la misma transacción que la actualización de tu pedido. Un duplicado ya aplicado debe recibir 2xx sin cumplir el pedido nuevamente.
Un reintento manual reinicia los intentos de entrega preservando el ID original del evento, el ID de entrega y el cuerpo exacto. Cada intento recibe una marca de tiempo de firma nueva. Pausar un endpoint conserva su cola; eliminarlo cancela las entregas pendientes y fallidas.
Historial de entrega y conciliación#
La interfaz muestra el estado del endpoint, entregas recientes, conteos de intentos, última respuesta HTTP, error y próxima hora de intento. Estos diagnósticos complementan tus propios registros de pedidos.
GET /api/v1/events con events:read devuelve hasta 50 eventos y nextCursor. Pasa el nextCursor opaco sin cambios como cursor; null marca el final. El ordenamiento usa tanto createdAt como id. El límite anterior de milisegundos Unix permanece disponible por separado y no puede combinarse con cursor. Los endpoints verificados reciben eventos suscritos no antes de su registro; los endpoints pausados siguen acumulando eventos en cola para entrega después de reanudar. Esto no promete historial anterior a la creación del endpoint.
Verificaciones de recibos entrantes de gateway#
Gateway v3 publica en /api/gateway/webhook con X-Gateway-Event-Id, X-Gateway-Timestamp y X-Gateway-Signature: v1=<hex>. HMAC-SHA256 firma timestamp + "." + rawBody con GATEWAY_WEBHOOK_SECRET; la ventana de reloj es de 300 segundos. Se verifican la igualdad de ID entre cabecera y cuerpo, la versión de API, el modo en vivo y el hash del cuerpo.
Un ID de evento reutilizado con bytes de cuerpo diferentes es rechazado. Una notificación firmada activa un GET autenticado de la operación exacta. Se verifican pedido, método, moneda, base, total aprobado y versión. El éxito requiere fulfillment_ready=true, verification=provider_confirmed, el monto pagado exacto y sin disputa; cripto también necesita evidencia de liquidación confirmada.
El manejo de ingreso, recibos y eventos de comerciante están protegidos contra duplicados. Los formatos antiguos de notificación solo para navegador no pueden establecer un pago exitoso. Los webhooks de comerciante nunca contienen campos de tarjeta, códigos bancarios, datos QR, credenciales del proveedor ni el secreto del gateway.
Telegram tiene un contrato separado#
POST /api/telegram/webhook verifica X-Telegram-Bot-Api-Secret-Token, la estructura de la actualización y el remitente. update_id protege contra procesamiento duplicado. Esta ruta maneja inicio de sesión, comandos, soporte y acciones de moderación; no acepta recibos financieros.
El propietario del despliegue configura el token del bot, el secreto del webhook, el registro setWebhook y la dirección HTTPS alcanzable. Son separados del secreto de firma del endpoint de comerciante.
Múltiples tiendas y un precio base en USD#
Elija Todas las tiendas para análisis y operaciones agregados, o seleccione una tienda para sus propios registros. Cada tienda tiene un storeId, nombre, IVA predeterminado, facturas, claves de API, endpoints de webhook, historial de eventos y aplicación web separados. Las notificaciones y tickets de soporte pertenecen a la cuenta. Una cuenta puede crear hasta 100 tiendas. Los registros existentes permanecen en su primera tienda.
Las facturas del nuevo espacio de trabajo usan pricingMode="universal" y un precio base en USD. Any incluye tarjeta, alipay, sbp y cripto; un comerciante puede seleccionar un subconjunto. Tarjetas y Alipay cobran en USD. SBP cobra en RUB usando el tipo de referencia USD/RUB del Banco de Rusia. Cripto cobra USDT bajo la regla explícita de factura 1 USD = 1 USDT. Esta es una regla de conversión de factura, no una cotización de mercado de USDT. La pasarela proporciona la tasa de la moneda seleccionada, el monto exacto de transferencia y las instrucciones antes de la transferencia.
El tipo de referencia y ambos montos se congelan al crear. Las facturas que implican conversión de RUB vencen dentro de 24 horas. Si no se puede obtener el tipo de referencia, la creación falla con FX_UNAVAILABLE; no se usa ningún tipo de respaldo inventado o desactualizado. El monto en minor del propietario y las cotizaciones usan la moneda contable USD. quotes[method].settlement contiene la cotización de cargo nativa. El checkout público expone la moneda de cargo seleccionada en currency, el precio original en baseAmountMinor/baseCurrency y el tipo de referencia en fx. Una vez que un pago ha sido despachado, su método no se puede cambiar; una factura tiene una operación de pasarela.
Cree una clave de API dentro de la tienda prevista y envíe Authorization: Bearer <key> desde su servidor. Su vinculación a tienda es inmutable; X-Kevmi-Store no puede otorgarle acceso a otra tienda. Las sesiones del navegador seleccionan X-Kevmi-Store: <storeId>; omitirlo selecciona la primera tienda. El valor all solo es aceptado por GET /api/account/dashboard y GET /api/payment-links. Las mutaciones requieren una tienda seleccionada, Origin y X-CSRF-Token. Los IDs de cuenta y los IDs de tienda son identificadores diferentes.
Por compatibilidad, las solicitudes de API sin pricingMode="universal" conservan el precio nativo y las restricciones de moneda originales. Las facturas existentes nunca se recalculan. Los recibos confirmados en análisis reflejan pagos verificados menos gastos de factura guardados; no son un saldo retirable. Las monedas de factura heredadas permanecen separadas y los pagos se gestionan con soporte. Cumpla un pedido solo después de verificar payment.fulfillmentReady=true y payment.status="succeeded" a través de la API del servidor, el historial de eventos o un webhook firmado.