# Probar los avisos a tu servidor

Envía a tu servidor un aviso de prueba firmado y comprueba que rechaza los falsos, sin esperar a que ocurra un evento real.

UTILIA OS envía a la dirección de avisos de tu aplicación una **muestra** del evento que elijas, firmada exactamente como una entrega real, y comprueba que tu servidor rechaza los avisos con la firma falsa. Son las únicas pruebas que salen hacia fuera: llegan de verdad a tu servidor.

## Antes de empezar

- **Integraciones en el plan**, y el aplicativo del evento que vas a probar: si no, `403 MODULE_NOT_ENABLED`.
- **La dirección de avisos configurada** en la aplicación, que no apunte a una red privada, y los avisos activados.
- **Con la clave**, las dos pruebas comparten un cupo de 5 al minuto con el panel. Desde la pestaña **Pruebas**, hace falta el permiso `EXTERNAL_APPS_EDIT` o `INTEGRATIONS_WEBHOOKS_MANAGE`.
- **Una clave de servidor.** Con una clave publicada en una web, las dos responden `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY`: lánzalas desde la pestaña **Pruebas**.

## Enviar un aviso de prueba

**`POST /api/external/v1/diagnostics/webhook-sample`**

Envía a la dirección de avisos de la aplicación una muestra firmada del evento indicado y responde con lo que contestó tu servidor. Exige Integraciones y que el plan incluya el evento. 5 por minuto, compartidos con el panel.

### curl

```bash
curl -X POST https://os.utilia.ai/api/external/v1/diagnostics/webhook-sample \
  -H "X-Api-Key: $UTILIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "INVOICE_CREATED"}'
```

### SDK JavaScript

```typescript
const muestra = await sdk.diagnostics.sendWebhookSample({ event: 'INVOICE_CREATED' });

if (!muestra.success) console.log(muestra.statusCode, muestra.error);
console.log(muestra.subscribed, muestra.sampleShape);
```

### SDK Python

```python
muestra = sdk.diagnostics.send_webhook_sample("INVOICE_CREATED")

if not muestra.success:
    print(muestra.status_code, muestra.error)
print(muestra.subscribed, muestra.sample_shape)
```

Con `WEBHOOK_TEST`, la prueba de conexión de siempre. No hace falta que la aplicación esté suscrita al evento, pero la respuesta te dice si lo está (`subscribed`), porque sin suscripción los avisos reales no te llegarán. Las muestras no quedan en el historial de entregas ni se reintentan.

La respuesta trae `success` (tu servidor respondió `2xx`), `statusCode`, `responseTimeMs`, `error` si no fue bien, `event`, `deliveryId`, `subscribed`, `sampleShape`, el cuerpo exacto que se envió (`payload`) y los primeros 500 caracteres de lo que contestó tu servidor (`responseSnippet`).

La muestra espera la respuesta de tu servidor **10 segundos**; una entrega real espera 30. Si tu servidor tarda entre las dos cifras, la muestra falla aunque los avisos reales lleguen: contesta antes y procesa después.

Desde el panel se envía igual, en la pestaña **Pruebas**, o con la API del panel y tu sesión:

**`POST /api/integrations/apps/:id/webhook/test`**

La misma muestra, lanzada por una persona con sesión. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_WEBHOOKS_MANAGE.

Parámetro de ruta: `id`.

## Cómo es un aviso de prueba

Lleva las mismas cabeceras que una entrega real: `X-Utilia-Signature`, `X-Utilia-Webhook-Signatures`, `X-Utilia-Timestamp`, `X-Utilia-Event`, `X-Utilia-Schema-Version` y `X-Utilia-Delivery-Id`, que en las pruebas empieza por `test_`. El cuerpo lleva dos marcas que los avisos reales no llevan: `livemode: false` en el sobre y `test: true` en `meta`. Los identificadores son inventados y llevan `test` en el nombre (`inv_test_...`).

```json
{
  "event": "INVOICE_CREATED",
  "timestamp": "2026-10-21T10:00:00.000Z",
  "app": { "id": "ckapp1234567890", "slug": "mi-integracion" },
  "livemode": false,
  "data": {
    "invoice": {
      "id": "inv_test_8f3k2m1q",
      "invoiceNumber": "PRUEBA-202600001",
      "displayNumber": "PRUEBA-2026-00001",
      "status": "ISSUED",
      "totalAmount": 30,
      "currency": "eur"
    },
    "externalUser": { "externalId": "user_test_1234" }
  },
  "meta": { "schemaVersion": "2026-05", "test": true }
}
```

Las muestras de los eventos de facturación (`INVOICE_*`), de `PAYMENT_METHOD_ADDED`, `PAYMENT_METHOD_REMOVED` y de `APP_HEALTH_CHANGED` tienen la forma de los avisos reales campo por campo; las de los demás eventos son genéricas. `sampleShape` lo dice: `exact` o `generic`. La forma de cada evento está en el [Catálogo de eventos](https://os.utilia.ai/docs/developers/webhooks/events).

> **Importante: Primero la firma, después la marca de prueba**
>
> Comprueba la firma **antes** de mirar `livemode`: las marcas viajan dentro del cuerpo firmado, así que solo te puedes fiar de ellas en un aviso con la firma buena. Después, trata como prueba todo aviso con `livemode === false` o `meta.test === true`: respóndelo con `2xx` y no lo guardes en tus datos de producción.

## Comprobar tu receptor de avisos

Un receptor que acepta cualquier aviso deja que cualquiera se haga pasar por UTILIA OS. Esta comprobación envía tres avisos de prueba y mira qué contesta tu servidor a cada uno.

**Comprobación del receptor de avisos**

Participantes: UTILIA OS y Tu servidor.

1. UTILIA OS → Tu servidor: Aviso bien firmado. Firmado como una entrega real.
2. Tu servidor → UTILIA OS (respuesta): 2xx esperado
3. UTILIA OS → Tu servidor: Aviso con la firma falsa. El mismo aviso, firmado con otra clave.
4. Tu servidor → UTILIA OS (respuesta): 4xx esperado
5. UTILIA OS → Tu servidor: Aviso caducado. Bien firmado, con la marca de tiempo de hace una hora.
6. Tu servidor → UTILIA OS (respuesta): 4xx esperado

_Si el primero no obtiene respuesta, los otros dos no se envían._

**`POST /api/external/v1/diagnostics/webhook-receiver-check`**

Envía tres avisos WEBHOOK_TEST (firma buena, firma falsa y marca de tiempo caducada) y responde el veredicto y lo que contestó tu servidor a cada uno. Exige Integraciones, dirección, avisos activados y clave de firma.

Cada aviso espera 30 segundos, como una entrega real: prepara tu cliente para esperar hasta un minuto y medio. En los SDK, `checkWebhookReceiver()` y `check_webhook_receiver()`, que ya esperan lo necesario. Exige una dirección configurada, los avisos activados y una clave de firma, y comparte el cupo de 5 por minuto con las muestras.

```json
{
  "at": "2026-10-21T10:00:00.000Z",
  "validAccepted": true,
  "invalidSignatureRejected": false,
  "staleTimestampRejected": true,
  "verdict": "Tu servidor ha aceptado un aviso de prueba con una firma falsa; comprueba que verifica la firma de todos los avisos.",
  "attempts": [
    { "kind": "valid", "statusCode": 200, "responseTimeMs": 120, "error": null },
    { "kind": "invalid_signature", "statusCode": 200, "responseTimeMs": 98, "error": null },
    { "kind": "stale_timestamp", "statusCode": 401, "responseTimeMs": 101, "error": null }
  ]
}
```

El resultado queda guardado, lo lance quien lo lance: el diagnóstico lo usa para el hallazgo [`webhook-receiver-accepts-invalid-signature`](https://os.utilia.ai/docs/developers/getting-started/testing#webhook-receiver-accepts-invalid-signature) y para el paso [`recorrido-receiver-checked`](https://os.utilia.ai/docs/developers/getting-started/testing#recorrido-receiver-checked).

## Probar tu receptor sin red

Para las pruebas automáticas de tu servidor, los SDK firman un cuerpo en local con la misma cadena que UTILIA OS: `sdk.webhooks.signTestPayload(payload, secret)` en JavaScript y `sign_test_payload(payload, secret)` en Python, a partir de la 4.5.0 (si ya se puede instalar lo dice [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#lo-que-todavía-no-puedes-instalar)). Devuelven el cuerpo en texto, la marca de tiempo, la firma y las cabeceras listas, y `sdk.webhooks.verify` acepta el resultado. Usa una clave inventada, nunca la de producción. Cómo se calcula la firma sin los SDK está en [Firma HMAC de los avisos](https://os.utilia.ai/docs/developers/webhooks/hmac-signature).

## Si algo falla

- **`400 WEBHOOK_NO_URL`**: la aplicación no tiene dirección de avisos. Configúrala en su ficha.
- **`400 WEBHOOK_URL_INVALID`**: la dirección apunta a una red privada o resuelve a una dirección no permitida.
- **`400 VALIDATION_ERROR`**: el evento no existe. La lista está en el [Catálogo de eventos](https://os.utilia.ai/docs/developers/webhooks/events).
- **`403 MODULE_NOT_ENABLED`**: el plan no incluye Integraciones o el aplicativo del evento.
- **`403 OUTBOUND_DOMAIN_NOT_ALLOWED`**: el destino no está en la lista de dominios salientes autorizados de la organización. Lo añade un administrador.
- **`409` en la comprobación del receptor**: los avisos están desactivados o, con `WEBHOOK_SECRET_NOT_CONFIGURED`, la aplicación no tiene clave de firma.
- **`success: false` en la muestra**: los avisos están desactivados, con el motivo, o tu servidor no respondió `2xx` a tiempo. `statusCode`, `error` y `responseSnippet` dicen qué contestó.
- **`403 TENANT_SUSPENDED` o `TENANT_CANCELLED`**: con la organización suspendida o de baja no se envían avisos de prueba.
- **`503 DIAGNOSTICS_UNAVAILABLE`**: el contador del cupo no responde y la prueba no se lanza sin contarla. Repite en unos minutos.

## Siguiente paso

- [Firma HMAC de los avisos](https://os.utilia.ai/docs/developers/webhooks/hmac-signature): Cómo verificar la firma y la marca de tiempo de cada aviso.
- [Entregas y reintentos](https://os.utilia.ai/docs/developers/webhooks/delivery-and-retries): Qué pasa cuando tu servidor no contesta a un aviso real.
