> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maple.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Suscríbase a tipos de evento, verifique entregas firmadas con HMAC, elimine duplicados de la entrega "al menos una vez" y reenvíe desde el registro de eventos.

Los webhooks notifican a su integración sobre los eventos de negocio compatibles. Registre un endpoint HTTPS público y los tipos de evento que procesa; Maple firma las entregas y registra sus resultados. La entrega duradera de eventos de negocio reintenta los fallos. Las pruebas firmadas y los reenvíos manuales son acciones de un solo intento y no programan reintentos automáticos propios.

## Administre las entregas en la consola

Los miembros invitados de la organización pueden abrir **Developers → app → Webhooks** sin ingresar una clave de API. La app necesita `webhooks:read` para diagnósticos y `webhooks:write` para modificaciones. Las acciones salientes también requieren una app activa.

* **Endpoints:** cree o edite un endpoint, guarde su secreto de firma de un solo uso, envíe una prueba de forma explícita o deshabilítelo y vuelva a habilitarlo. Es preferible deshabilitar para conservar el historial de entregas. Las apps suspendidas siguen siendo consultables y sus endpoints todavía se pueden deshabilitar.
* **Deliveries:** filtre las entregas ordinarias y las pruebas firmadas por endpoint, tipo de evento, resultado y rango de tiempo. Consulte el estado agregado, el número de intentos, el último estado de respuesta, el error seguro y las marcas de tiempo. Esto no es una línea de tiempo por intento. Las URL y el estado del endpoint que se muestran son la configuración actual, no copias históricas del destino.
* **Events:** inspeccione los metadatos de eventos de negocio que se pueden reenviar. **Review event** muestra una vista previa de un reintento a nivel de evento; **Confirm retry** intenta los destinos elegibles y omite los pares ya exitosos, los endpoints deshabilitados y los endpoints que ya no están suscritos. Si los destinos cambian, revise una vista previa nueva antes de confirmar.

El historial muestra 24 horas de forma predeterminada y admite hasta 30 días y 100 filas por página; los cursores vencen a los 15 minutos. Esta ventana de lectura no significa que los registros más antiguos se hayan eliminado. Un reenvío confirmado desde la consola admite como máximo diez endpoints elegibles y muestra una vista previa de hasta 100 destinos; si la distribución es mayor, se rechaza antes de enviar.

Las pruebas y los reenvíos de la consola tienen presupuestos independientes en el servidor: tres acciones por persona/app/entorno y diez por app/entorno por minuto. Respete el tiempo de espera que se le indique; la interfaz no reenvía automáticamente. Si el resultado es desconocido, revise su receptor y actualice el historial antes de reintentar.

Una prueba entregada significa que su endpoint respondió con un `2xx`, no que se haya verificado su firma. Las entregas de prueba sin un evento de negocio reenviable ofrecen **Send another test**. Ni una fila pendiente en el registro ni el fallo de una acción manual demuestran que haya otro intento programado.

La consola expone metadatos, no los payloads completos de los eventos, los cuerpos de respuesta del receptor, los secretos ni los rastreos internos. La API pública de eventos autenticada que se describe más abajo es independiente. La guía **Webhook format and signature verification** contiene únicamente ejemplos sintéticos que puede copiar; abrirla no envía ninguna solicitud ni genera una firma con su secreto.

### Pruebe el flujo completo de pedidos

Para integraciones de pedidos, abra **Developers → app → Test orders** en el panel del sandbox. Seleccione un local de prueba autorizado y confirme **Create test order**. La app necesita `orders:write`, debe estar activa y debe ser el receptor activo de pedidos en modo de prueba del local. Maple debe poder preparar un artículo del menú con existencias y sin modificadores obligatorios.

Esto crea un pedido real para recoger con pago en el local, a través del flujo normal, incluidas las integraciones de webhook e impresora configuradas. La acción no cobra ninguna tarjeta. Use un local de sandbox aislado; no es lo mismo que **Send test** en un endpoint de webhook. La creación tiene un límite por local (tres por minuto de forma predeterminada) y no está disponible si no se puede consultar el almacén de presupuestos.

Con `orders:read`, el rastreador de resultados muestra la validación, la entrega de notificaciones y la decisión del socio para los últimos diez pedidos asociados con esta app, entorno y local. Los pedidos nuevos aparecen después de que el flujo registra la validación o un evento de webhook. Actualice antes de reenviar tras un resultado incierto; una lista vacía no demuestra que la creación haya fallado. Esta herramienta no reemplaza la verificación de solicitudes a la API con su propia clave.

**Location settings → Integrations → Connected apps** del comercio conserva los controles de conexión y acceso. Las herramientas de prueba para desarrolladores ahora están en la consola a nivel de organización.

## El sobre del evento

Todas las entregas tienen el mismo sobre al estilo de Stripe. El payload `data` es específico de cada evento:

```json theme={null}
{
  "object": "event",
  "id": "evt_...",
  "type": "order.notification",
  "created": 1765432100,
  "data": { "order_id": "ord_...", "location_id": "str_..." }
}
```

<ResponseField name="id" type="string">
  Id único del evento. **Elimine duplicados con este valor**: la entrega es al menos una vez.
</ResponseField>

<ResponseField name="type" type="string">
  El tipo de evento. Úselo para enrutar el evento.
</ResponseField>

<ResponseField name="created" type="number">
  Segundos Unix en que se creó el evento.
</ResponseField>

<ResponseField name="data" type="object">
  Payload específico del evento. `order.notification` incluye el contenido completo del pedido; los eventos de ciclo de vida (`order.created`, `order.paid`, `order.cancelled`) incluyen campos de resumen. Consulte [Eventos de webhook](/es/developer-api/webhook-events) para ver cada payload y use `GET /v1/orders/{orderId}` para obtener el estado actual autorizado.
</ResponseField>

## Tipos de evento

Suscríbase solo a lo que procesa. El catálogo vigente siempre está en `GET /v1/webhook_event_types`:

| Tipo | Cuándo se dispara |
| - | - |
| `order.validation_requested` | Maple le pide validar un pedido antes de notificarle |
| `order.notification` | Se le entrega un pedido para que lo prepare — **el evento sobre el que usted actúa** |
| `order.created` | Se creó un pedido — una señal de ciclo de vida para analítica, más breve que la notificación |
| `order.paid` | Se liquidó el pago (puede ser después de la entrega) |
| `order.cancelled` | Se canceló el pedido — deje de prepararlo |
| `booking.created` | Se creó una reserva o se observó por primera vez desde su proveedor |
| `booking.confirmed` | Se confirmó una reserva |
| `booking.updated` | Se modificó una reserva |
| `booking.cancelled` | Se canceló una reserva |
| `booking.declined` | El establecimiento rechazó una reserva |
| `booking.seated` | Se sentó a un grupo |
| `booking.completed` | Se completó una reserva |
| `booking.no_show` | Una reserva se marcó como no presentada |
| `waitlist_entry.created` | Un grupo se unió a la lista de espera |
| `waitlist_entry.notified` | Se avisó a un grupo en espera que su mesa está lista |
| `waitlist_entry.seated` | Un grupo de la lista de espera se sentó en una reserva de cliente sin reserva |
| `waitlist_entry.removed` | Un grupo salió de la lista de espera o el establecimiento lo retiró |
| `waitlist_entry.expired` | Un grupo notificado no respondió a tiempo |
| `store.provisioned` | Se aprovisionó un local para su app |
| `store.deprovisioned` | Se desaprovisionó un local de su app |
| `store.status.changed` | Cambió el estado de un local conectado |
| `menu.sync.completed` | Se completó la sincronización del menú de un local |
| `menu.sync.failed` | Falló la sincronización del menú de un local |

El endpoint de prueba también entrega `webhook.test` a solicitud, para que pueda verificar un controlador antes de recibir tráfico real.

<Note>
  Estos son eventos discretos, no un flujo de estados. Maple no envía un webhook por cada cambio de estado de un pedido:
  las transiciones que usted mismo realiza (`accept`, `ready`, `complete`) no se devuelven y no existe un evento por
  transición. Para conocer el estado actual de un pedido, lea `GET /v1/orders/{orderId}`. Consulte el [Ciclo de vida del
  pedido](/es/developer-api/concepts/order-lifecycle).
</Note>

Para saber qué contiene cada evento de pedido y a cuáles suscribirse, consulte [Eventos de webhook](/es/developer-api/webhook-events). Los eventos de reserva requieren `bookings:read` y se entregan para los locales autorizados; consulte [Eventos de reserva](/es/developer-api/webhook-events).

## Suscripción

```bash theme={null}
curl -X POST $MAPLE_BASE/webhook_subscriptions \
  -H "Authorization: Bearer $MAPLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notification_url": "https://your-app.example.com/maple/webhooks",
    "event_types": ["order.notification", "order.cancelled"]
  }'
```

```json Response theme={null}
{
  "object": "webhook_subscription.created",
  "subscription": { "object": "webhook_subscription", "id": "dws_...", "environment": "test", "status": "enabled", "...": "..." },
  "signing_secret": "mwhsec_..."
}
```

<Warning>
  El `signing_secret` (`mwhsec_…`) se devuelve **una sola vez, al crear la suscripción, y no se vuelve a mostrar.**
  Guárdelo de forma segura ahora. Si lo pierde, rótelo creando una nueva suscripción.
</Warning>

La `notification_url` debe ser HTTPS pública; las direcciones privadas e internas se rechazan. Administre las suscripciones con `GET`, `PATCH` (actualiza la URL, los tipos de evento o el estado `enabled`/`disabled`) y `DELETE` en `/v1/webhook_subscriptions/{id}`. Actualizar una suscripción no cambia su secreto de firma.

### Varias suscripciones

Puede registrar más de una suscripción. Cada suscripción **habilitada** cuyos `event_types` incluyan un evento disparado recibe su propia entrega firmada, cada una con su propio secreto de firma, por lo que se permite que los `event_types` de distintas suscripciones se solapen. Esto le permite distribuir por destino: por ejemplo, dirigir los eventos de pedido a su servicio de preparación y los eventos `menu.sync.*` a un endpoint de administración interna distinto. El [reenvío](#registro-de-eventos) sigue el mismo enrutamiento y omite cualquier suscripción que ya haya recibido el evento.

## Verificación de la firma

Cada entrega incluye dos encabezados:

* `maple-webhook-id` — el id del evento.
* `maple-webhook-signature` — `t=<unix_seconds>,v1=<hex_hmac>`.

La firma HMAC-SHA256 se calcula sobre una cadena firmada que vincula la marca de tiempo, la suscripción y la URL de destino con el cuerpo, de modo que una firma capturada no se puede reutilizar contra otra suscripción u otra URL:

```
{timestamp}.{subscription_id}.{notification_url}.{raw_body}
```

Recalcúlela con su secreto de firma y compárela en tiempo constante. Use siempre el **cuerpo de la solicitud sin procesar ni analizar**: volver a serializar el JSON cambia los bytes e invalida la firma.

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyMapleWebhook(params: {
  signatureHeader: string; // maple-webhook-signature
  subscriptionId: string; // your subscription id (dws_…)
  notificationUrl: string; // the exact URL you registered
  rawBody: string; // the unparsed request body
  signingSecret: string; // mwhsec_…
  toleranceSeconds?: number; // default 300
}): boolean {
  const parts = Object.fromEntries(params.signatureHeader.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  const provided = parts.v1 ?? '';

  // Reject stale deliveries to prevent replay.
  const skew = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
  if (!Number.isFinite(timestamp) || skew > (params.toleranceSeconds ?? 300)) return false;

  const signed = `${timestamp}.${params.subscriptionId}.${params.notificationUrl}.${params.rawBody}`;
  const expected = createHmac('sha256', params.signingSecret).update(signed).digest('hex');
  return expected.length === provided.length && timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}
```

## Garantías de entrega

* **Al menos una vez.** El mismo evento puede llegar más de una vez. Elimine duplicados con el `id` del sobre y haga que su controlador sea idempotente.
* **Responda rápido.** Devuelva un `2xx` de inmediato y realice el trabajo más lento de forma asíncrona. Cualquier respuesta que no sea `2xx` (o un tiempo de espera agotado) cuenta como una entrega fallida.
* **No se garantiza el orden.** No suponga que los eventos llegan en el orden en que ocurrieron; concilie con el recurso del pedido cuando la secuencia importe.

### Reintentos y deshabilitación automática

La entrega duradera de eventos de negocio usa hasta **9 intentos** de forma predeterminada: el primero de inmediato y los demás con un retraso creciente. Los primeros intentos caben dentro de la ventana de decisión de seis minutos del POS, mientras que la cola larga permite conciliar después de una interrupción real:

| Intento | Retraso tras el anterior |
| - | - |
| 1 | inmediato |
| 2 | 10 segundos |
| 3 | 30 segundos |
| 4 | 2 minutos |
| 5 | 5 minutos |
| 6 | 15 minutos |
| 7 | 1 hora |
| 8 | 3 horas |
| 9 | 6 horas |

Una respuesta que no sea `2xx` (o un tiempo de espera agotado; el límite por intento es de 15 segundos) hace fallar ese intento. Un evento que falla **los 9** intentos cuenta como una entrega totalmente fallida. Después de **5 entregas totalmente fallidas consecutivas** (cinco eventos distintos que agotaron todos los intentos), la suscripción se **deshabilita** automáticamente; una sola entrega exitosa reinicia el contador a cero. Vuelva a habilitarla con un `PATCH` que establezca `status` de nuevo en `enabled` cuando su endpoint esté en buen estado y luego [reenvíe](#registro-de-eventos) lo que se haya perdido.

## Registro de eventos

Los eventos de negocio publicados se registran para conciliación y reenvío. Las entregas de prueba firmadas sintéticas tienen registros en el registro de entregas, pero no una fila reenviable en el registro de eventos; envíe otra prueba explícita en su lugar.

* `GET /v1/webhook_events` — sus eventos recientes, del más reciente al más antiguo (hasta 50). Útil para conciliación y depuración.
* `GET /v1/webhook_events/{eventId}` — un solo evento, con su payload `data` completo.
* `POST /v1/webhook_events/{eventId}/replay` — intenta la entrega a las suscripciones habilitadas que coinciden actualmente, omitiendo los pares con un éxito registrado. Reutiliza el id del evento y no fuerza la reentrega de lo ya exitoso. Los intentos concurrentes o un resultado incierto de red o de persistencia aún pueden producir duplicados; el receptor debe eliminarlos. Los intentos manuales fallidos son definitivos y no programan reintentos automáticos.

Si su servicio estuvo caído el tiempo suficiente para perder un pedido, use primero el reenvío del registro cuando quiera conservar la identidad original del evento. Para una puesta al día directa, use `since` como su último punto de control exitoso y avance en orden: solicite `GET /v1/orders?since=<checkpoint>&limit=100` y repita con `starting_after=<last-order-id>` mientras `has_more` sea true. Con `since`, los resultados van del más antiguo al más reciente, de modo que se recorre todo el atraso sin omitir pedidos que comparten una marca de tiempo. Obtenga el recurso autorizado del pedido y luego llame a `POST /v1/orders/{orderId}/resend` solo para un pedido activo que no recibió su webhook. El reenvío crea un `order.notification` firmado nuevo con una identidad de evento nueva y nunca cambia el estado del pedido. Los pedidos en estado terminal (`REJECTED`, `CUSTOMER_CANCELLED` o `STORE_CANCELLED`) no se pueden reenviar ni reactivar; concílielos con `GET /v1/orders?since=...` y `GET /v1/orders/{orderId}`.

## Prueba de su controlador

```bash theme={null}
curl -X POST $MAPLE_BASE/webhook_subscriptions/{id}/test \
  -H "Authorization: Bearer $MAPLE_KEY"
```

Esto entrega un evento `webhook.test` firmado y devuelve si se entregó y el estado HTTP que devolvió su endpoint:

```json theme={null}
{ "object": "webhook_test_result", "event_id": "evt_...", "delivered": true, "response_status": 200 }
```

Úselo para comprobar la accesibilidad y una respuesta `2xx`, y verifique la firma en su propio receptor. La respuesta no certifica el manejo de la firma. Es un solo intento; un valor `delivered` en false o un estado de respuesta nulo no debe interpretarse como un reintento automático pendiente.

## Lista de verificación

<Check>Verifique la firma HMAC de cada entrega contra el cuerpo sin procesar y rechace las marcas de tiempo vencidas.</Check>
<Check>Elimine duplicados con el `id` del sobre; haga que los controladores sean idempotentes.</Check>
<Check>Devuelva `2xx` rápido; procese de forma asíncrona.</Check>
<Check>Suscríbase solo a los tipos de evento que procesa.</Check>
<Check>Monitoree las suscripciones deshabilitadas automáticamente y reenvíe desde el registro después de una interrupción.</Check>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.