Ir al contenido

Autenticación y firma

En la configuración estándar, los webhooks de IDPOS no usan usuario, contraseña ni token en la petición. IDPOS firma cada envío con el secreto de su suscripción y usted vuelve a calcular esa firma con el mismo secreto. Si ambas coinciden, el envío es auténtico y no fue alterado.

Una suscripción puede incluir cabeceras adicionales acordadas con IDBI. La firma sigue siendo la forma de comprobar que el envío viene de IDPOS.

CabeceraDescripción
Content-Typeapplication/json
User-AgentID-Webhook/1.0
X-Webhook-IdIdentificador único del evento. Es igual al event_id del cuerpo y no cambia entre reintentos. No forma parte del mensaje firmado.
X-Webhook-TimestampMarca de tiempo Unix, en segundos. Cambia en cada intento de envío.
X-Webhook-SignatureFirma HMAC-SHA256 en hexadecimal. Cambia en cada intento de envío.
  1. Tenga a mano el secreto.

    Es la cadena que IDBI le entregó una sola vez al registrar su suscripción. Cada suscripción tiene el suyo. Guárdelo fuera del código fuente, por ejemplo en la variable de entorno IDPOS_WEBHOOK_SECRET.

  2. Lea las cabeceras y el cuerpo crudo.

    Tome X-Webhook-Timestamp y X-Webhook-Signature, y los bytes del cuerpo tal como llegaron, sin interpretarlos. Si falta alguna cabecera, rechace el envío.

  3. Arme el texto a firmar: {timestamp}.{cuerpo}.

    Es el valor de X-Webhook-Timestamp, un punto (.) y el cuerpo. Con el vector de prueba, el texto es:

    1791815400.{"event_id":"01J9ZK3Q7V4N8X2M5T6R1B0C9D","event_type":"order.accepted","timestamp":"2026-10-12T14:30:00Z","api_version":"v1","data":{"id":"9b6f0c3e-1a2d-4c55-8f01-3d2a7e6b9a10","reference":"OV-001234"}}
  4. Calcule el HMAC-SHA256.

    Use el secreto como clave y el texto del paso anterior como mensaje. El resultado debe ir en hexadecimal y en minúsculas: es la firma esperada.

  5. Compare en tiempo constante.

    Compare la firma esperada con X-Webhook-Signature con una función de comparación en tiempo constante. No use == ni ===: la comparación normal puede filtrar información por el tiempo que tarda.

  6. Compruebe la marca de tiempo.

    Rechace el envío si X-Webhook-Timestamp difiere en más de 300 segundos (5 minutos) de la hora actual de su servidor. Esto limita la ventana en la que un envío capturado puede reenviarse, pero no la elimina: dentro de esos 300 segundos el mismo envío sigue pareciendo válido. Por eso debe descartar duplicados con el event_id del cuerpo ya verificado, no con la cabecera X-Webhook-Id, que no está firmada. Mantenga el reloj de su servidor sincronizado.

X-Webhook-Signature = HMAC-SHA256(secreto, "{X-Webhook-Timestamp}.{cuerpo}") en hexadecimal
ElementoValor
AlgoritmoHMAC con SHA-256
ClaveEl secreto de la suscripción, entregado por IDBI
MensajeEl valor de X-Webhook-Timestamp, un punto (.) y el cuerpo de la petición tal como llegó
SalidaHexadecimal en minúsculas

Cada ejemplo tiene dos partes: la función que verifica la firma y el endpoint que la usa. Ambos operan sobre el cuerpo crudo, comparan en tiempo constante y aplican la tolerancia de 300 segundos. El secreto se lee de la variable de entorno IDPOS_WEBHOOK_SECRET. En los endpoints, processLater (process_later en Python) representa la cola o el proceso en segundo plano de su sistema. Reciben el event_id del cuerpo ya verificado: úselo para descartar duplicados antes de procesar.

Requiere Node.js 18 o superior y Express 4 o superior. La ruta usa express.raw para recibir el cuerpo como Buffer, sin analizar el JSON.

verify-signature.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function isValidSignature({ rawBody, timestamp, signature, secret, now = Date.now() / 1000 }) {
if (!Buffer.isBuffer(rawBody) || !/^\d{1,15}$/.test(timestamp ?? '')) {
return false;
}
if (Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) {
return false;
}
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
const expectedBytes = Buffer.from(expected);
const receivedBytes = Buffer.from(signature ?? '');
return (
expectedBytes.length === receivedBytes.length &&
timingSafeEqual(expectedBytes, receivedBytes)
);
}
receiver.mjs
import express from 'express';
import { isValidSignature } from './verify-signature.mjs';
const app = express();
app.post(
'/webhooks/idpos',
express.raw({ type: 'application/json', limit: '5mb' }),
(req, res) => {
const valid = isValidSignature({
rawBody: req.body,
timestamp: req.get('X-Webhook-Timestamp'),
signature: req.get('X-Webhook-Signature'),
secret: process.env.IDPOS_WEBHOOK_SECRET,
});
if (!valid) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
processLater(event.event_id, event);
res.sendStatus(200);
},
);
app.listen(3000);

Use estos valores para comprobar su implementación antes de recibir envíos reales.

ElementoValor
Secretosecreto-de-prueba
X-Webhook-Timestamp1791815400
X-Webhook-Signature esperadab9452f6af0e368501a8b3fc3c059e5ac9723a6753eb0e2006280abc524ccf7b8

Cuerpo, en una sola línea y sin salto de línea final (202 bytes):

{"event_id":"01J9ZK3Q7V4N8X2M5T6R1B0C9D","event_type":"order.accepted","timestamp":"2026-10-12T14:30:00Z","api_version":"v1","data":{"id":"9b6f0c3e-1a2d-4c55-8f01-3d2a7e6b9a10","reference":"OV-001234"}}

Mensaje firmado: 1791815400. seguido del cuerpo anterior. Para reproducir la firma desde una terminal:

Ventana de terminal
printf '%s.%s' '1791815400' '{"event_id":"01J9ZK3Q7V4N8X2M5T6R1B0C9D","event_type":"order.accepted","timestamp":"2026-10-12T14:30:00Z","api_version":"v1","data":{"id":"9b6f0c3e-1a2d-4c55-8f01-3d2a7e6b9a10","reference":"OV-001234"}}' \
| openssl dgst -sha256 -hmac 'secreto-de-prueba' -hex
SíntomaCausa probable
La firma nunca coincideSe está verificando sobre el JSON reserializado y no sobre el cuerpo crudo.
La firma nunca coincideEl secreto es de otra suscripción o tiene espacios o saltos de línea al inicio o al final.
La firma nunca coincideEl mensaje no usa el formato {timestamp}.{cuerpo}, por ejemplo falta el punto.
Se rechazan envíos válidosEl reloj de su servidor está desfasado más de 300 segundos.
Los reintentos fallan, pero el primer envío pasóSe almacenó la firma o la marca de tiempo del primer intento. Ambas cambian en cada intento: verifique siempre con las cabeceras de la petición actual.

Revise Entrega y reintentos para saber cómo debe responder su endpoint.