# 12 · SDK de TypeScript

`@wa/sdk` es un cliente fino y **sin dependencias** (usa `fetch` y `node:crypto`; Node 18+). Añade lo que todo cliente acaba escribiendo: reintentos con retroceso, `Idempotency-Key` automática, paginación, errores tipados y verificación de firmas.

> **Estado.** El paquete es `private` y expone el fuente (`src/index.ts`); todavía no está publicado en un registro. Dentro del monorepo: `"@wa/sdk": "workspace:*"`. Fuera: copiad `packages/sdk/src/index.ts` a vuestro proyecto (es un único archivo) o generad un cliente desde [`openapi.yaml`](../openapi/openapi.yaml). Probado con 8 pruebas automáticas contra el servidor real.

## Crear el cliente

```ts
import { WaGateway, WaGatewayError } from '@wa/sdk';

const wa = new WaGateway({
  baseUrl: process.env.WA_BASE_URL!,      // https://wa.ejemplo.com
  apiKey: process.env.WA_API_KEY!,        // solo en el servidor
  tenantRef: 'CLI-0042',                  // solo con claves multi-tenant
});

const otro = wa.forTenant({ ref: 'CLI-0099' });   // mismo cliente, otro tenant
```

Opciones: `tenantRef` o `tenantId`, `maxRetries` (3), `retryBaseMs` (500), `fetch` (para pruebas o proxies).

## Enviar

```ts
const msg = await wa.messages.sendTemplate({
  to: '+34600111222',
  template: 'recordatorio_cita',
  language: 'es',
  variables: ['Ana', '15 de octubre'],
  externalRef: 'cita-48213',
  idempotencyKey: 'recordatorio-cita-48213',   // opcional; si falta se genera UNA por llamada
});
// msg.status === 'queued': el resultado final llega por evento

await wa.messages.sendText({ to: '+34600111222', body: 'Hasta mañana' });   // solo dentro de la ventana de 24 h
await wa.messages.send({ to, type: 'document', /* … */ });                  // cualquier cuerpo de POST /v1/messages
```

El SDK reintenta `429`, `502`, `503` y errores de red **con la misma `Idempotency-Key`**, y respeta `Retry-After`. No reintenta ningún `4xx`.

## Errores

```ts
try {
  await wa.messages.sendTemplate({ /* … */ });
} catch (e) {
  if (e instanceof WaGatewayError) {
    console.error(e.status, e.code, e.message, e.details, e.requestId);
    if (e.code === 'window_closed') { /* enviar plantilla */ }
    if (e.code === 'payment_required') { /* avisar al cliente */ }
  } else throw e;   // red agotada tras los reintentos
}
```

Códigos: [referencia de errores](referencia/errores.md).

## Listados y paginación

```ts
for await (const m of wa.paginate((cursor) => wa.messages.list({ status: 'failed', cursor }))) {
  console.log(m.id, m.error?.code);
}
```

## Resto de la API

| Grupo | Métodos |
| --- | --- |
| `wa.messages` | `send`, `sendTemplate`, `sendText`, `get`, `list`, `retry` |
| `wa.conversations` | `list`, `get`, `update`, `messages`, `replyText`, `markRead` |
| `wa.contacts` | `create`, `list`, `optIn`, `optOut`, `delete` |
| `wa.templates` | `list`, `get`, `create`, `sync` |
| `wa.account` | `get`, `numbers`, `license`, `costs` |
| `wa.embed` | `createToken` (solo desde el backend) |
| `wa.request(método, ruta, {query, body})` | Cualquier endpoint que el SDK aún no envuelva |

## Token para el chat embebido

```ts
// En el endpoint de VUESTRO backend que llama el navegador:
const { token, url, expires_at } = await wa.embed.createToken({
  screen: 'chat',
  user: { externalUserId: String(session.userId), name: session.name, email: session.email, role: 'agent' },
  locale: 'es',
});
res.json({ token, url, expires_at });
```

Ver [06](06-ui-embebible.md) para el componente del lado del navegador.

## Recibir eventos

```ts
import express from 'express';
const app = express();

app.post('/hooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  let evento;
  try {
    // req.body es un Buffer con el cuerpo CRUDO: verificar la firma ANTES de parsear
    evento = WaGateway.verifyWebhook(req.body, String(req.get('x-wa-signature')), process.env.WA_WEBHOOK_SECRET!);
  } catch {
    return res.sendStatus(400);
  }
  // deduplicar por evento.id y encolar; responder rápido
  res.sendStatus(204);
});
```

`verifyWebhook(cuerpo, cabecera, secreto, toleranciaSeg = 300)` lanza `Error` si la firma es inválida o mal formada o si la marca de tiempo queda fuera de tolerancia, y devuelve el evento parseado si es correcta. (Se espera `express.raw`; con `express.json()` el cuerpo ya no es el original y la firma no coincidirá.)

## Pruebas en vuestra aplicación

Inyectad `fetch` para simular el gateway sin red:

```ts
const wa = new WaGateway({ baseUrl: 'http://gw.test', apiKey: 'k', fetch: async () => new Response(JSON.stringify({ id: 'msg_1', status: 'queued' }), { status: 202 }) });
```

O apuntad el cliente al [playground local](10-pruebas-y-produccion.md).
