# DomISP → WhatsLite: envío y seguimiento de mensajes

Contrato preparado el 3 de octubre de 2026. Dirigido al desarrollador de DomISP,
al administrador de WhatsLite y al diseñador de la página de documentación.
Esta guía describe la revisión que incorpora `provider=domisp`; su disponibilidad
requiere desplegar API/worker con migración 0004. No acredita un envío real ni
configura automáticamente una cuenta Rodritel en WhatsLite.

## 1. Recorrido

```text
Evento en DomISP → backend DomISP → API WhatsLite → cola durable
→ worker WhatsLite → Meta Gateway → WhatsApp
WhatsApp → Meta Gateway → WhatsLite → webhook firmado del backend DomISP
```

DomISP aporta destinatario, evento y valores. WhatsLite selecciona la plantilla
aprobada de la suscripción, la convierte al formato Meta, encola el envío y
procesa los estados. Meta Gateway conserva las credenciales Meta.

La API key de DomISP para consultar clientes **no se usa** en este recorrido.
Tampoco sirven las claves `bk_*` ni el UUID de BlazeConnector. Una cuenta ya
conectada en BlazeConnector no implica que tenga una suscripción/canal WhatsLite
activo. Rodritel necesita su propia configuración WhatsLite.

## 2. Preparación en Blaze/WhatsLite

El administrador debe tener una suscripción activa de producto `whatslite` y
un canal enlazado mediante Meta Gateway. El backend ERP usa su contrato interno:

| Operación ERP | Ruta relativa a WhatsLite |
| --- | --- |
| Ver configuración, catálogo y estado Meta | `GET /internal/v1/erp/tenants/{tenant_id}/native-integrations/domisp?subscription_id={subscription_id}` |
| Guardar configuración | `PUT` sobre la misma ruta |
| Crear las plantillas faltantes | `POST /internal/v1/erp/tenants/{tenant_id}/native-integrations/domisp/templates/reconcile?subscription_id={subscription_id}` |
| Emitir clave nativa DomISP | `POST /internal/v1/erp/tenants/{tenant_id}/native-integrations/domisp/credential?subscription_id={subscription_id}` |

Estas rutas usan `Authorization: ServiceKey ...` y `X-Blaze-Service: blazeerp`.
Solo el backend ERP debe utilizarlas; DomISP recibe únicamente su clave pública
de integración. Contrato administrativo completo: [ERP](../blazeerp-contract.md).

Body para guardar configuración (identificadores ilustrativos):

```json
{
  "channel_id": "chb_CANAL_WHATSLITE",
  "company_name": "Rodritel",
  "language": "es",
  "default_country_code": "1",
  "recipient_format": "e164",
  "samples": {}
}
```

Reconciliar con `{"create_missing":true}` crea los cuatro presets faltantes en
Meta; no envía mensajes. Esperar `APPROVED`. El catálogo devuelve el alias y el
`meta_template_name` real, aislado por suscripción y versión del contenido.
Cambiar empresa/idioma/contenido puede producir otro nombre que necesita aprobación.
El estado se cachea hasta un minuto; la consulta administrativa lo refresca.

Emitir la credencial con `{"name":"DomISP Rodritel"}`. El campo `key` aparece
una vez, con formato `bwl_live_...`; guardarlo en el backend DomISP. Su scope
exclusivo es `messages:compat` y su proveedor es `domisp`. No permite leer mensajes,
llamar a otros adaptadores ni enviar texto arbitrario con `/v1/messages`.

Configurar además el callback HTTPS del cliente mediante el ERP, por suscripción:
`PUT /internal/v1/erp/tenants/{tenant_id}/webhooks?subscription_id=...`.
Suscribir `message.sent`, `message.delivered`, `message.read`, `message.failed` y,
si DomISP recibirá respuestas, `message.received`. Guardar el secreto del webhook
por separado de la API key. Ninguna petición de envío admite un `callback_url`.

## 3. Petición que debe construir DomISP

```http
POST https://whatslite.blaze.do/v1/native-integrations/domisp/messages
Authorization: Bearer <CLAVE_NATIVA_WHATSLITE_DOMISP>
Content-Type: application/json
Idempotency-Key: domisp:factura:FAC-123:emitida:v1
```

```json
{
  "to": "18095550123",
  "alias": "FACTURA_GENERADA",
  "parameters": ["Ana Perez", "NIC-12345", "FAC-123", "RD$ 1,200.00", "10/10/2026"],
  "client_reference": "domisp:factura:FAC-123"
}
```

Los datos son ejemplos ficticios. No ejecutar contra un destinatario real sin
seleccionar antes el número de prueba y el evento que corresponde.

| Campo | Regla |
| --- | --- |
| `to` | Obligatorio. Con configuración `e164`, incluir prefijo internacional: `1809...` o `+1809...`. No pasar un número nacional sin prefijo. Máximo 64 bytes de entrada; normalización a 8–15 dígitos. |
| `alias` | Uno de los cuatro aliases de la tabla siguiente. |
| `parameters` | Array JSON de strings, cantidad y orden exactos. Valores resueltos, no macros. Cada valor no vacío, máximo 1024 bytes UTF-8. |
| `client_reference` | Opcional, máximo 128 bytes. Referencia de negocio que se conserva en mensaje y webhooks de estado. No deduplica. |
| `Idempotency-Key` | Un único encabezado obligatorio, no vacío, máximo 128 bytes. Identifica una notificación individual. |

No enviar `tenant_id`, `subscription_id`, `channel_id`, tokens Meta, `message`,
URLs de callback ni campos adicionales. WhatsLite toma el tenant, suscripción y
canal de la clave/configuración. JSON incorrecto, campos desconocidos o más de
1 MiB se rechazan. No poner `api_key` en la URL, ni siquiera junto al Bearer.

## 4. Catálogo y mapeo de datos

| Alias | `parameters` en orden |
| --- | --- |
| `FACTURA_GENERADA` | nombre, NIC, número de factura, total con moneda, vencimiento |
| `RECORDATORIO_PAGO` | nombre, NIC, balance con moneda, vencimiento |
| `CONFIRMACION_PAGO` | nombre, NIC, referencia del pago aplicado, importe con moneda |
| `BIENVENIDA` | nombre, NIC del servicio activado |

Estos nombres definen el contrato de WhatsLite; **no afirman que DomISP disponga
de macros o eventos con esos nombres**. El desarrollador conecta cada evento
real de su instalación y rellena el JSON desde los datos del negocio. El campo
`provider_message_format` del catálogo muestra un ejemplo JSON; no es texto
para pegar en una plantilla de SMS.

Si DomISP obtiene los datos por su API: primero buscar el titular por teléfono,
cédula o RNC y después seleccionar su servicio y NIC. No reemplazar NIC por
`client.id`. Si existen varios titulares/servicios, resolver el servicio correcto
antes de construir la notificación. En cuentas consolidadas, no sumar balances
repetidos por servicio; usar el monto de la factura o consolidado que corresponda.
No mandar `CONFIRMACION_PAGO` por un reporte de depósito aún no aplicado.

WhatsLite no consulta DomISP para completar campos faltantes ni realiza cobros.
Los parámetros son strings estructurados: un carácter `|` no divide el valor.

## 5. Respuesta, persistencia e idempotencia

Respuesta inicial de ejemplo:

```json
{
  "message_id": "msg_IDENTIFICADOR",
  "status": "queued",
  "provider": "domisp",
  "alias": "FACTURA_GENERADA",
  "idempotency_source": "explicit",
  "client_reference": "domisp:factura:FAC-123"
}
```

HTTP **202 significa aceptado**, no enviado ni entregado. DomISP debe guardar
`message_id`, la referencia, la clave de idempotencia y el JSON exacto antes de
marcar su trabajo como aceptado. Un replay puede devolver el estado actual del
mensaje original en vez de `queued`.

Una misma clave con los mismos datos normalizados devuelve el mensaje original;
con datos distintos devuelve `409 IDEMPOTENCY_CONFLICT`. DomISP no tiene el
fallback temporal de cinco minutos de otros adaptadores. La clave se aísla por
proveedor/canal; cambiar el canal durante un retry puede crear otro envío.
La deduplicación depende de que se conserve el registro del mensaje: no es
indefinida frente a la política de retención. No reactivar trabajos históricos
ya cerrados solo porque sus registros locales o remotos fueron archivados.

Patrón recomendado para la clave: `domisp:{tipo}:{id}:{evento}:{version}`.
Para recordatorios distintos de una misma factura incluir la fecha o ID único
del recordatorio. Mantener la misma clave y body en un timeout/retry. Si cambia
el destinatario o se desea un nuevo envío, es otra notificación deliberada.

| Situación | Acción de DomISP |
| --- | --- |
| 202 | Guardar recibo; esperar estado/callback. |
| Timeout, conexión cortada, 5xx | Reintentar con la misma clave/body y backoff limitado. |
| 429 | Respetar `Retry-After`; misma clave/body. |
| 400/415 | Corregir el contrato; no repetir en bucle. |
| 401/403 | Revisar clave, proveedor, suscripción activa y permisos. |
| 409 de plantilla | Revisar `TEMPLATE_MISSING`, `TEMPLATE_NOT_APPROVED` o `TEMPLATE_DRIFT`; no convertir automáticamente a texto libre. |
| 409 `IDEMPOTENCY_CONFLICT` | Revisar cambio de datos; no generar otra clave automáticamente para eludirlo. |
| Estado `uncertain` | Revisar el envío existente; no crear otra notificación a ciegas. |

Una credencial de lectura separada con `messages:read` en la misma suscripción
puede consultar `GET /v1/messages/{message_id}`. La clave nativa de envío no
puede usar esa ruta.

## 6. Ejemplo PHP de envío desde el backend DomISP

```php
<?php
// Persistir $eventKey y $payload junto al evento antes del primer intento.
$eventKey = 'domisp:factura:FAC-123:emitida:v1';
$payload = json_encode([
    'to' => '18095550123',
    'alias' => 'FACTURA_GENERADA',
    'parameters' => ['Ana Perez', 'NIC-12345', 'FAC-123', 'RD$ 1,200.00', '10/10/2026'],
    'client_reference' => 'domisp:factura:FAC-123',
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);

$ch = curl_init('https://whatslite.blaze.do/v1/native-integrations/domisp/messages');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('WHATSLITE_DOMISP_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . $eventKey,
    ],
    CURLOPT_POSTFIELDS => $payload,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === false) {
    throw new RuntimeException('Resultado desconocido: conservar clave y JSON para reintentar.');
}
$result = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
if ($status !== 202) {
    // Registrar código seguro; aplicar la tabla de errores, sin imprimir credenciales.
    throw new RuntimeException('WhatsLite rechazó el envío: HTTP ' . $status);
}
// Guardar $result['message_id'] y $result['status'] en el evento DomISP.
// No marcarlo como entregado aquí.
```

## 7. Confirmaciones y mensajes entrantes

WhatsLite recibe el webhook de Meta Gateway y envía al callback DomISP un
envelope firmado. Los estados son `queued → processing → submitted → sent →
delivered → read`; también existen `failed`, `uncertain` y `cancelled`.
`submitted` significa aceptación por el gateway; `sent`, envío informado por
el proveedor; `delivered`, entrega. No todos los estados generan callback:
consultar el mensaje si un trabajo sigue sin confirmación.

```json
{
  "event_id": "wev_EJEMPLO",
  "event_type": "message.delivered",
  "created_at": "2026-10-03T23:00:00Z",
  "tenant_id": "ten_EJEMPLO",
  "channel_id": "chb_EJEMPLO",
  "data": {
    "message_id": "msg_IDENTIFICADOR",
    "provider_message_id": "wamid.EJEMPLO",
    "channel_binding_id": "chb_EJEMPLO",
    "status": "delivered",
    "occurred_at": "2026-10-03T23:00:00Z",
    "client_reference": "domisp:factura:FAC-123"
  }
}
```

Headers: `X-Blaze-Event-ID`, `X-Blaze-Event-Type`, `X-Blaze-Timestamp` y
`X-Blaze-Signature`. En la implementación actual la firma es **hexadecimal
sin prefijo `sha256=`**:

```text
hex(HMAC-SHA256(secret_webhook, timestamp + "." + body_raw))
```

Verificar con comparación constante (`hash_equals` en PHP), validar timestamp
(por ejemplo ±300 s con relojes sincronizados) y deduplicar por `event_id`.
Firmar/verificar los bytes recibidos, sin decodificar y volver a codificar JSON.
Persistir el evento de forma durable y responder 2xx; usar 5xx si no se pudo
persistir para permitir retry. Procesar asíncronamente. El receptor debe tolerar
duplicados y eventos fuera de orden: no bajar `read` a `sent`. Si llegan estados
contradictorios, consultar el mensaje antes de sobrescribir el resultado.

`message.received` contiene la entrada del cliente. No representa confirmación
de entrega de una factura. Asociar por canal y remitente; un inbound no trae
necesariamente la referencia del envío original. Los mensajes enviados fuera de
WhatsLite no adquieren automáticamente historial en este producto.

## 8. Plantillas personalizadas

Los cuatro aliases son presets administrados y versionados. La integración
nativa no admite nombres arbitrarios ni CRUD de esos aliases desde DomISP.
Si el sistema necesita sus propias plantillas con componentes estilo Meta, usar
una clave pública ordinaria con `templates:read`, `templates:write` y
`messages:send`: `POST /v1/templates`, `GET /v1/templates` y `POST /v1/messages`.
La API actual ofrece crear/listar plantillas; no prometer edición/borrado que no
están implementados. El formato canónico de envío usa `template.language: "es"`
y `body_parameters`; no reutilizar el objeto de idioma de BlazeConnector.

## 9. Aceptación antes de habilitar envíos automáticos

1. Aplicar migración 0004 y desplegar API/worker de esta revisión.
2. Confirmar suscripción/canal WhatsLite, catálogo aprobado y clave DomISP.
3. Configurar los disparadores del backend DomISP y callback HTTPS firmado.
4. Seleccionar un destinatario de prueba y enviar un evento autorizado.
5. Repetir exactamente el envío: mismo `message_id`, sin segundo despacho.
6. Confirmar estados mediante webhook y probar una respuesta entrante.
7. Activar los disparadores automáticos una vez verificado ese recorrido.

Para el diseñador: publicar las secciones de configuración, petición, catálogo,
respuesta, errores y callbacks. Mantener separadas las instrucciones internas
del ERP de los campos que DomISP realmente debe enviar.
