---
title: "Registrar un webhook"
method: POST
path: "/webhooks"
tags: ["webhooks"]
---

# Registrar un webhook

`POST /webhooks`

## Request

- Base URL: `https://api.enviadores.com.mx/api/v1`
- URL: `https://api.enviadores.com.mx/api/v1/webhooks`
- Auth: HTTP bearer

Registra un endpoint `https://` público que recibirá un `POST` firmado por cada evento suscrito. **El `secret` se devuelve UNA sola vez** en esta respuesta: no se almacena (se deriva bajo una llave del servidor) y no vuelve a mostrarse; guárdalo al recibirlo. Máximo 5 webhooks activos por cuenta y modo (`409 WEBHOOK_LIMIT_REACHED`). La URL se valida contra SSRF: solo `https://`, hostname público (se rechazan `localhost`, `.local`, `.internal`, literales IP privadas/loopback/link-local y hosts que resuelvan a ellas) y sin credenciales embebidas; en cada entrega se vuelve a resolver el host y la conexión se fija a la IP pública verificada. Con llave `ek_test_` el webhook es de sandbox (solo `shipment.created`/`shipment.cancelled` del carril de pruebas + ping). Requiere scope `webhooks:write`.

## Request body

- WebhookCreateRequest
  - `url` string, uri, required: Endpoint `https://` público en el puerto 443 u 8443 (cualquier otro puerto se rechaza). Se rechazan http, credenciales embebidas, fragmentos, `localhost`/`.local`/`.internal`, literales IP privadas, hosts que resuelvan a rangos privados y hosts que apunten al propio API de Enviadores.
  - `events` WebhookEvent[], required: Eventos a recibir (duplicados se descartan).

## Response `201`

Webhook registrado. `secret` aparece SOLO aquí.

- object
  - `success` unknown, required
  - `data` object, required
    - `webhook` Webhook, required: Recurso webhook. NUNCA incluye el secreto: solo existe en la respuesta de `POST /webhooks`.
      - `id` string, required: `wh_` + 24 hex.
      - `url` string, required
      - `events` WebhookEvent[], required
      - `mode` 'live' | 'test', required: Lane de la llave que lo registró. `live` recibe eventos reales; `test` solo `shipment.created`/`shipment.cancelled` del sandbox y el ping.
      - `status` 'active' | 'disabled', required: `disabled` = desactivado automáticamente: tras 20 fallos consecutivos, o porque la llave API que lo registró fue revocada (`disabled_reason: key_revoked` — un webhook muere con su llave). Al desactivarse, sus eventos pendientes se descartan. No cuenta para el tope; elimínalo y regístralo de nuevo.
      - `failure_count` integer, required: Fallos CONSECUTIVOS de entrega (cualquier respuesta no 2xx o error de transporte). Vuelve a 0 con cada 2xx. Los pings de prueba no lo afectan.
      - `last_delivery_at` string, nullable: ISO-8601 UTC (Z). Último intento (éxito o fallo), pings incluidos.
      - `last_delivery_status` integer, nullable: Status HTTP del último intento; `0` = fallo de transporte (timeout/TLS/DNS).
      - `created_at` string, nullable: ISO-8601 UTC (Z).
      - `disabled_at` string, nullable: ISO-8601 UTC (Z).
      - `disabled_reason` string, nullable: `key_revoked` cuando la llave que lo registró fue revocada; texto explicativo cuando fue por fallos consecutivos.
    - `secret` string, required: Secreto de firma — se muestra SOLO aquí. Guárdalo: no se almacena y no se puede recuperar; para obtener otro hay que eliminar el webhook y registrarlo de nuevo.
  - `requestId` string, required: Correlación de logs; también en el header `X-Request-Id`.

## Other responses

- `401`: `UNAUTHORIZED` — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos).
- `403`: `INSUFFICIENT_SCOPE` — a la llave le falta el scope requerido · `PDV_NOT_ALLOWED` — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde `429 RATE_LIMITED` (el 429 tiene precedencia sobre este 403).
- `404`: `NOT_FOUND` — los webhooks no están habilitados en este despliegue (respuesta idéntica a una ruta inexistente).
- `409`: `WEBHOOK_LIMIT_REACHED` — ya hay 5 webhooks activos en este modo (`details {max_active, mode}`). Elimina uno con `DELETE /webhooks/{id}`.
- `422`: `VALIDATION_ERROR` — `details.fields` nombra el problema: URL no `https://`, puerto distinto de 443/8443, host privado/no resoluble/propio del API, credenciales embebidas, `events` vacío o con un evento desconocido, campo desconocido.
- `429`: `RATE_LIMITED` — bucket de la llave agotado. Espera `Retry-After` s (`details.retry_after_ms` para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 `INSUFFICIENT_SCOPE`.
- `500`: `SERVER_ERROR` — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff.

---

[API](https://skmtc.dev/enviadores/apis/enviadores-public-api.md) · [All operations](https://skmtc.dev/enviadores/apis/enviadores-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/enviadores/apis/enviadores-public-api/revisions/3633f08c889c?raw)
