# Começo rápido

> Do workspace à primeira mensagem enviada, com exemplos em curl e TypeScript.

Fonte: https://metahub.matratecnologia.com/docs/comeco-rapido

Este guia leva um app do zero até enviar a primeira mensagem. Os exemplos usam `curl` e TypeScript com `fetch` (Node 18+). Pré-requisito: a chave de API, o segredo do webhook e as URLs de retorno, que a equipe Matra entrega (ver [Visão geral](https://metahub.matratecnologia.com/docs/visao-geral.md#o-que-pedir-à-equipe-matra)).

```ts
// hub.ts: cliente mínimo, usado nos passos seguintes
const HUB_URL = 'https://api.metahub.matratecnologia.com/v1'

export const hub = async <T>(
  path: string,
  init: { method?: string; body?: unknown; headers?: Record<string, string> } = {},
): Promise<{ status: number; data: T }> => {
  const response = await fetch(`${HUB_URL}${path}`, {
    method: init.method ?? 'GET',
    headers: {
      authorization: `Bearer ${process.env.HUB_API_KEY}`,
      ...(init.body !== undefined && { 'content-type': 'application/json' }),
      ...init.headers,
    },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  })
  const data = (await response.json().catch(() => null)) as T
  return { status: response.status, data }
}
```

## 1. Confira a chave

```bash
curl https://api.metahub.matratecnologia.com/v1/me \
  -H "Authorization: Bearer $HUB_API_KEY"
```

Resposta `200`: `{ "app": { "id": "...", "name": "..." } }`. Com chave ausente ou inválida, `401` com `code: "INVALID_API_KEY"`.

## 2. Crie o workspace do cliente

Um workspace por cliente final. O `externalId` é o id do cliente no seu sistema, e chamar de novo com o mesmo valor devolve o mesmo workspace: pode chamar no cadastro do cliente sem se preocupar com duplicata.

```bash
curl -X POST https://api.metahub.matratecnologia.com/v1/workspaces \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "cliente-123", "name": "Clínica Exemplo" }'
```

```ts
const { status, data } = await hub<{ workspace: { id: string } }>('/workspaces', {
  method: 'POST',
  body: { externalId: 'cliente-123', name: 'Clínica Exemplo' },
})
// 201 criou, 200 já existia
const workspaceId = data.workspace.id
```

Guarde o `workspace.id` junto do cliente.

## 3. Abra a sessão de conexão

```ts
const session = await hub<{ id: string; url: string; expiresAt: string }>(
  `/workspaces/${workspaceId}/connect-sessions`,
  {
    method: 'POST',
    body: { kind: 'whatsapp', returnUrl: 'https://app.exemplo.com/settings/whatsapp' },
  },
)
// redirecione o navegador do cliente para session.data.url
```

- A `url` vale 15 minutos e serve uma única vez. Ela carrega um token: não grave em log.
- O `returnUrl` precisa estar na lista cadastrada para o seu app; senão, `400 RETURN_URL_NOT_ALLOWED`.
- `503 META_WEBHOOK_NOT_READY` indica que o hub ainda não recebe webhooks da Meta: tente de novo em instantes.

## 4. Trate o retorno do cliente

Quando o cliente termina, a página do hub redireciona para o seu `returnUrl`:

| Query | O que mostrar |
|---|---|
| `?status=connected&connectionId=<id>` | "Número conectado, ativando". A conexão ainda está `pending` |
| `?status=cancelled` | O cliente desistiu; ofereça tentar de novo |
| `?status=error&code=<code>` | Falha com o motivo (ver [Conexão do WhatsApp](https://metahub.matratecnologia.com/docs/conexoes.md#retorno-do-cliente)) |

O redirect é só uma dica de interface: o navegador pode fechar antes dele. A fonte da verdade é o webhook do passo 5.

## 5. Receba os webhooks

O hub faz `POST` no endpoint cadastrado. Confira a assinatura **sobre o corpo cru**, antes de qualquer `JSON.parse`, responda `2xx` rápido e processe depois.

```ts
// verify-hub-webhook.ts
import { createHmac, timingSafeEqual } from 'node:crypto'

const TOLERANCE_SECONDS = 5 * 60

export const verifyHubWebhook = (
  secret: string,
  headers: { timestamp: string; signature: string },
  rawBody: string,
  nowSeconds = Math.floor(Date.now() / 1000),
): boolean => {
  const timestamp = Number(headers.timestamp)
  if (!Number.isInteger(timestamp)) return false
  if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false

  const expected = `sha256=${createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')}`
  const received = Buffer.from(headers.signature)
  const computed = Buffer.from(expected)
  return received.length === computed.length && timingSafeEqual(received, computed)
}
```

O ponto em que a verificação costuma quebrar é o framework parsear o JSON antes. Pegue o corpo como texto:

**Next.js (route handler)**

```ts
// app/api/hub-webhook/route.ts
import { verifyHubWebhook } from '@/lib/verify-hub-webhook'

export const POST = async (request: Request) => {
  const rawBody = await request.text()
  const valid = verifyHubWebhook(
    process.env.HUB_WEBHOOK_SECRET!,
    {
      timestamp: request.headers.get('x-hub-timestamp') ?? '',
      signature: request.headers.get('x-hub-signature') ?? '',
    },
    rawBody,
  )
  if (!valid) return new Response(null, { status: 401 })

  const event = JSON.parse(rawBody)
  await saveForProcessing(event) // fila ou tabela; deduplique por event.id
  return new Response(null, { status: 204 })
}
```

**Express**

```ts
import express from 'express'
import { verifyHubWebhook } from './verify-hub-webhook'

const app = express()

// express.raw só nesta rota: o corpo chega como Buffer, sem parse
app.post('/hub-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const rawBody = req.body.toString('utf8')
  const valid = verifyHubWebhook(
    process.env.HUB_WEBHOOK_SECRET!,
    {
      timestamp: String(req.header('x-hub-timestamp') ?? ''),
      signature: String(req.header('x-hub-signature') ?? ''),
    },
    rawBody,
  )
  if (!valid) return res.sendStatus(401)

  await saveForProcessing(JSON.parse(rawBody))
  res.sendStatus(204)
})
```

Depois do passo 4 chegam, nesta ordem provável (a ordem não é garantida):

```json
{
  "id": "cm...",
  "type": "connection.created",
  "createdAt": "2026-10-22T15:00:00.000Z",
  "workspace": { "id": "...", "externalId": "cliente-123" },
  "connectionId": "...",
  "data": { "id": "...", "status": "pending", "whatsapp": { "displayPhoneNumber": "+55 43 99999-0000" } },
  "meta": null
}
```

e um `connection.updated` com `data.status: "active"` quando a ativação termina. Guarde o `connectionId` junto do workspace. Nos eventos de conexão, `data` é a conexão inteira, no formato de `GET /v1/connections/:id`. O formato completo de cada evento está no [catálogo](https://metahub.matratecnologia.com/docs/eventos.md).

## 6. Envie a primeira mensagem

Mensagem livre (texto, mídia, botões) só sai dentro da janela de atendimento de 24 h, que abre quando o contato escreve. Fora dela, use um [template](https://metahub.matratecnologia.com/docs/templates.md) aprovado.

```bash
curl -X POST https://api.metahub.matratecnologia.com/v1/connections/$CONNECTION_ID/messages \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-8812-aviso-entrega" \
  -d '{
    "to": { "userId": "BR.13491208655302741918" },
    "type": "text",
    "text": { "body": "Seu pedido saiu para entrega." }
  }'
```

```ts
const sent = await hub<{ message: { id: string; state: string } }>(
  `/connections/${connectionId}/messages`,
  {
    method: 'POST',
    headers: { 'idempotency-key': `pedido-8812-aviso-entrega` },
    body: {
      to: { userId: 'BR.13491208655302741918' },
      type: 'text',
      text: { body: 'Seu pedido saiu para entrega.' },
    },
  },
)
// 202: entrou na fila. sent.data.message.state === 'pending'
```

- **`Idempotency-Key` é obrigatório.** Use um valor ligado ao fato de negócio (o id do pedido e o tipo do aviso), não um valor aleatório por tentativa. Se a requisição cair, repita com a mesma chave: o hub devolve `200` com o envio existente, sem mandar de novo.
- **`to.userId`** é o BSUID do contato, que chega em toda mensagem recebida (`data.message.from.userId`). Use `to.phone` (só dígitos, com DDI) quando tiver apenas o telefone.

## 7. Acompanhe o resultado

O envio muda de estado e cada mudança chega como `whatsapp.message.status`:

- `source: "hub"`: o estado do envio no hub (`accepted`, `failed`, `uncertain`);
- `source: "meta"`: a entrega no aparelho (`sent`, `delivered`, `read`, `failed`).

```json
{
  "type": "whatsapp.message.status",
  "data": {
    "messageId": "...",
    "wamid": "wamid.HBgM...",
    "source": "meta",
    "status": "delivered",
    "timestamp": "2026-10-23T15:00:03.000Z"
  }
}
```

Se o webhook atrasar, consulte direto: `GET /v1/connections/:connectionId/messages/:messageId`. O estado `uncertain` pede cuidado: **não reenvie às cegas** (ver [Estados do envio](https://metahub.matratecnologia.com/docs/mensagens.md#estados-do-envio)).

## Checklist antes de ir para produção

- [ ] Chave de API e segredo do webhook num cofre, só no backend.
- [ ] Receptor verifica a assinatura sobre o corpo cru e recusa timestamp com mais de 5 min de diferença.
- [ ] Receptor responde `2xx` em menos de 10 s e processa depois.
- [ ] Eventos deduplicados pelo `id` do envelope.
- [ ] Estado decidido por `createdAt` do envelope, não pela ordem de chegada.
- [ ] Todo envio com `Idempotency-Key` derivada do fato de negócio.
- [ ] `uncertain` tratado como "não sei", sem reenvio automático.
- [ ] Erros tratados pelo `code`, com valor desconhecido caindo num erro genérico.
- [ ] Respostas `429` respeitam o `Retry-After`.
