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

# Webhooks de integración

> Suscribí tu ERP a los eventos de Tero POS y verificá la firma HMAC-SHA256 de cada entrega.

## Qué es

Los webhooks de integración envían cada evento del POS a una URL de tu ERP en cuanto ocurre. Es la vía principal para mantener el ERP sincronizado: ventas, cierres de caja, facturación, stock, fiados y productos nuevos.

Cada entrega es un `POST` con el [envelope del evento](https://docs.pos.terominds.com/api/catalogo-eventos.md) en el cuerpo JSON, firmado con HMAC-SHA256 usando el secreto del webhook.

Si preferís leer por tu cuenta, el mismo feed está disponible con [pull de eventos](https://docs.pos.terominds.com/api/eventos.md).

## Dónde está

En el back office: **Configuración → Integraciones**. Ahí registrás la URL, elegís sucursales opcionales y ves el estado de la última entrega.

También por API, con una clave de alcance **escritura**:

| Método | Ruta | Qué hace |
| - | - | - |
| `GET` | `/api/integration/v1/webhooks` | Lista los webhooks de la empresa (nunca devuelve el secreto). |
| `POST` | `/api/integration/v1/webhooks` | Crea un webhook y devuelve el secreto **una sola vez**. |
| `DELETE` | `/api/integration/v1/webhooks/:webhookId` | Elimina un webhook y sus entregas pendientes. |

```bash theme={null}
export TERO_API='https://pos-api.terominds.com/api'
export TERO_API_KEY='tero_live_aabbccdd_xxxxxxxx'
export TERO_TENANT_ID='00000000-0000-0000-0000-000000000001'

curl -sS -X POST "$TERO_API/integration/v1/webhooks?tenantId=$TERO_TENANT_ID" \
  -H "Authorization: Bearer $TERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://erp.miempresa.com/hooks/tero",
    "location_ids": [],
    "event_types": []
  }'
```

Respuesta:

```json theme={null}
{
  "ok": true,
  "webhook": {
    "id": "b6b0e0d0-0000-4000-8000-000000000000",
    "url": "https://erp.miempresa.com/hooks/tero",
    "secret_prefix": "whsec_ab12cd",
    "enabled": true,
    "location_ids": [],
    "event_types": [],
    "created_at": "2026-10-21T19:00:00.000Z",
    "last_delivery": null
  },
  "secret": "whsec_ab12cd..."
}
```

`location_ids` vacío recibe todas las sucursales. `event_types` vacío recibe todos los eventos.

> [!WARNING]
>
> La URL debe ser `https://`. Solo se acepta `http://` en `127.0.0.1` o `localhost` para pruebas locales.

> [!WARNING]
>
> El campo `secret` se devuelve **una sola vez**. Guardalo en tu ERP; después solo vas a ver el prefijo (`secret_prefix`).

## Encabezados de la entrega

| Encabezado | Valor |
| - | - |
| `Content-Type` | `application/json` |
| `X-Tero-Event-Id` | `event_id` del envelope (idempotente) |
| `X-Tero-Timestamp` | Unix en segundos, momento del envío |
| `X-Tero-Signature` | `v1=<hex HMAC-SHA256(secret, "<timestamp>.<body>")>` |

El `body` es el JSON exacto del envelope. Firmá y verificá **los bytes tal cual llegan**, sin reformatear.

## Verificar la firma

Compará la firma con la que calculás vos y rechazá si difiere o si el timestamp se aleja más de 5 minutos.

### Node.js

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

const SECRET = process.env.TERO_WEBHOOK_SECRET

export function verifyTeroSignature({ headers, rawBody }) {
  const signature = headers['x-tero-signature']
  const timestamp = headers['x-tero-timestamp']
  if (!signature || !timestamp) return false

  // Tolerancia de reloj: 5 minutos.
  const now = Math.floor(Date.now() / 1000)
  if (Math.abs(now - Number(timestamp)) > 300) return false

  const expected =
    'v1=' + createHmac('sha256', SECRET).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(signature)
  const b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}
```

### Python

```python theme={null}
import hashlib
import hmac
import time

SECRET = os.environ["TERO_WEBHOOK_SECRET"].encode()

def verify_tero_signature(headers, raw_body: bytes) -> bool:
    signature = headers.get("X-Tero-Signature")
    timestamp = headers.get("X-Tero-Timestamp")
    if not signature or not timestamp:
        return False

    if abs(int(time.time()) - int(timestamp)) > 300:
        return False

    expected = "v1=" + hmac.new(
        SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
```

## Reintentos

Respondé `2xx` para confirmar la entrega. Cualquier otro resultado (o un timeout) cuenta como fallo y Tero reintenta con backoff exponencial: **1m, 2m, 4m, 8m…** hasta un máximo de **24 horas**, con un tope de **\~15 intentos**. Después la entrega queda en estado `dead` y se ve en **Configuración → Integraciones**.

* Tolerá duplicados: reintentá siempre con el mismo `event_id`. Usalo para deduplicar.
* Tolerá huecos: el orden es por `seq` por empresa, pero puede haber entregas fuera de orden.
* Respondé rápido. Si tu ERP tarda, contestá `2xx` y procesá en segundo plano.

## Errores de creación

| Código | `error` | Qué significa |
| - | - | - |
| 400 | `https_required` | La URL no es `https://` (ni loopback http). |
| 400 | `invalid_url` | URL inválida. |
| 400 | `invalid_location_ids` | Alguna sucursal no pertenece a la empresa. |
| 400 | `invalid_event_types` | Algún tipo de evento no existe. |
| 403 | `insufficient_scope` | La clave no es de escritura. |
| 404 | `not_found` | El webhook no existe o es de otra empresa. |


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