Pular para o conteúdo
Páginas da documentação

Começo rápido

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

Abrir o Markdown

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).

// 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

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.

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" }'
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

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)

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.

// 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)

// 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

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):

{
  "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.

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 aprovado.

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." }
  }'
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).
{
  "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).

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.