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

# Webhooks

> Recibí eventos firmados cuando se crean o actualizan ventas, pedidos y preparaciones.

Los webhooks envían una solicitud `POST` a una URL HTTPS de tu sistema cuando ocurre un evento en La Pyme. Podés configurarlos desde la API pública o desde **Configuración > Integraciones > API > Webhooks**.

## Configurar por API

Usá `webhooks:read` para descubrir eventos y leer endpoints. Para crear o cambiar un endpoint necesitás `webhooks:write`, `Idempotency-Key` y los scopes de lectura de todos los eventos habilitados. Por ejemplo, `order.*` requiere `orders:read` y `sale.*` requiere `sales:read`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.lapyme.com.ar/api/v1/webhook-endpoints" \
    -H "Authorization: Bearer $LAPYME_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: endpoint-tienda-principal-v1" \
    -d '{
      "label": "Tienda principal",
      "url": "https://integracion.example.com/lapyme/webhooks",
      "enabled_events": ["order.created", "order.updated"],
      "api_version": "2026-06-29"
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch("https://api.lapyme.com.ar/api/v1/webhook-endpoints", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LAPYME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      label: "Tienda principal",
      url: "https://integracion.example.com/lapyme/webhooks",
      enabled_events: ["order.created", "order.updated"],
      api_version: "2026-06-29",
    }),
  });
  ```

  ```python Python theme={null}
  import os, uuid, requests

  response = requests.post(
      "https://api.lapyme.com.ar/api/v1/webhook-endpoints",
      headers={
          "Authorization": f"Bearer {os.environ['LAPYME_API_KEY']}",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={
          "label": "Tienda principal",
          "url": "https://integracion.example.com/lapyme/webhooks",
          "enabled_events": ["order.created", "order.updated"],
          "api_version": "2026-06-29",
      },
  )
  ```
</CodeGroup>

La creación devuelve `signing_secret` una vez. Guardalo en un secret manager; las lecturas, actualizaciones y acciones nunca lo incluyen. `GET /api/v1/webhook-event-types` devuelve el catálogo autoritativo de eventos, versiones y scopes.

Los endpoints creados con una API key pertenecen a la organización. Los creados con OAuth delegado pertenecen a ese grant: otra app no puede verlos y la revocación deshabilita el endpoint y cancela entregas todavía no enviadas.

## Ciclo de vida

* `PATCH /api/v1/webhook-endpoints/{id}` cambia `label`, `url`, `enabled_events` o `api_version`; `status` es de solo lectura.
* `POST .../{id}/pause` frena eventos normales sin descartar la cola.
* `POST .../{id}/resume` revalida los scopes de los eventos y reanuda la entrega.
* `POST .../{id}/disable` es terminal y cancela trabajo no enviado.
* `POST .../{id}/rotate-secret` invalida inmediatamente el secreto anterior.
* `POST .../{id}/test` crea una entrega sintética durable incluso si el endpoint está pausado.

Todas las mutaciones requieren `Idempotency-Key`. Repetir exactamente el request devuelve el mismo endpoint, secreto o test; reutilizar la clave con otro request devuelve `409 IDEMPOTENCY_CONFLICT`.

Para rotar sin perder eventos: pausá el endpoint, rotá el secreto, actualizá el receptor, enviá un test y reanudá.

## Eventos disponibles

V1 incluye estos eventos:

| Evento                      | Cuándo se envía                               |
| --------------------------- | --------------------------------------------- |
| `sale.created`              | Se crea una venta o nota de crédito.          |
| `sale.updated`              | Se edita una venta.                           |
| `order.created`             | Se crea un pedido de comercio.                |
| `order.updated`             | Cambia el agregado del pedido.                |
| `order.completed`           | El pedido pasa a completado.                  |
| `order.cancelled`           | El pedido pasa a cancelado.                   |
| `order.partially_fulfilled` | El pedido queda parcialmente preparado.       |
| `order.fulfilled`           | El pedido queda completamente preparado.      |
| `fulfillment.created`       | Se registra una preparación de pedido.        |
| `webhook_endpoint.disabled` | Se deshabilita permanentemente otro endpoint. |

<Info>
  `order.*` usa el modelo actual de pedidos de comercio.
</Info>

<Info>
  Las notas de crédito se envían como `sale.created` con `document_kind: "credit_note"` en el objeto.
</Info>

## Payload

Todos los eventos usan este envelope:

```json theme={null}
{
  "id": "0b5f2d7f-48cb-4f47-a8f7-df2d9c2e8f42",
  "type": "order.created",
  "api_version": "2026-06-29",
  "created": "2026-06-29T21:00:00.000Z",
  "organization_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "object": {
      "id": "7a6a7f42-7ef1-43e8-9878-628fc9e7a6f4",
      "object": "order",
      "status": "open",
      "fulfillment_status": "unfulfilled",
      "amounts": {
        "discount": 0,
        "subtotal": 100000,
        "tax": 21000,
        "total": 121000
      }
    }
  }
}
```

Usá `id` para deduplicar. La entrega es al menos una vez, por lo que tu endpoint puede recibir el mismo evento más de una vez.

## Headers

Cada request incluye:

| Header               | Descripción                                 |
| -------------------- | ------------------------------------------- |
| `La-Pyme-Event-Id`   | ID estable del evento.                      |
| `La-Pyme-Event-Type` | Tipo de evento, por ejemplo `sale.created`. |
| `La-Pyme-Signature`  | Firma HMAC para verificar origen.           |
| `Content-Type`       | Siempre `application/json`.                 |

## Verificar firma

La firma usa HMAC SHA-256 sobre:

```text theme={null}
{timestamp}.{raw_body}
```

El header tiene formato:

```text theme={null}
t=1782768000,v1=HEX_SIGNATURE
```

Ejemplo en TypeScript:

```typescript theme={null}
import crypto from "node:crypto";

function verifySignature(input: {
  rawBody: string;
  signatureHeader: string;
  secret: string;
}) {
  const parts = Object.fromEntries(
    input.signatureHeader.split(",").map((part) => {
      const [key, value] = part.split("=");
      return [key, value];
    })
  );

  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", input.secret)
    .update(`${timestamp}.${input.rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}
```

<Warning>
  Guardá el secreto cuando se muestra. Si lo perdés, rotalo desde la API o la pantalla de webhooks.
</Warning>

## Payload de prueba

`POST /api/v1/webhook-endpoints/{id}/test` usa el mismo sender, firma y política de reintentos que un evento normal, pero no contiene datos comerciales:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440401",
  "type": "webhook.test",
  "api_version": "2026-06-29",
  "created": "2026-07-20T15:15:00.000Z",
  "organization_id": "550e8400-e29b-41d4-a716-446655440000",
  "test": true,
  "data": {
    "object": {
      "object": "webhook_endpoint",
      "id": "550e8400-e29b-41d4-a716-446655440301"
    }
  }
}
```

## Respuestas y reintentos

La Pyme considera exitosa cualquier respuesta `2xx`.

Si tu endpoint responde fuera de `2xx`, corta por timeout o falla la conexión, La Pyme reintenta con backoff. Después de varios intentos fallidos, la entrega queda como fallo final y aparece en el historial del webhook.

Tu endpoint debería:

1. Responder rápido con `2xx` después de persistir el evento.
2. Procesar trabajos lentos en segundo plano.
3. Deduplicar por `id`.
4. Verificar la firma antes de confiar en el payload.
