# Ruvz API v1 — contexto para implementação com IA

Este arquivo é uma referência autocontida para fornecer a uma LLM ao implementar uma integração com a API da Ruvz. A API é REST, usa JSON UTF-8 e a base é `https://api.ruvz.com.br/v1`.

## Regras essenciais

1. Envie `Authorization: Bearer <token>` em toda chamada. Tokens começam com `rvz_live_` e são criados no painel da Ruvz.
2. Sempre informe `from`: o número WhatsApp conectado ou o usuário Instagram de origem.
3. Quando uma rota endereça uma pessoa, informe exatamente um de `to` (telefone), `bsuid` (contato Meta sem telefone) ou `instagram_id`.
4. Nunca use `flow_id`, `account_id`, `conversation_id` ou `contact_id` como entrada. Eles aparecem nas respostas e nos eventos apenas como informação; nenhuma rota os aceita. Endereça sempre por `from` + `to`/`bsuid`/`instagram_id`.
5. `POST /messages` retornar `200` significa **registrado e enfileirado**, não entregue. A confirmação chega em `message.status`.
6. Texto livre só pode ser enviado até 24h após a última mensagem do contato, nos dois canais. Fora da janela, no WhatsApp use um modelo aprovado; no Instagram não há modelos, então é preciso esperar o contato escrever de novo.
7. Em todos os envios, envie uma `Idempotency-Key` única e estável para a mensagem de negócio.

## Autenticação

```http
Authorization: Bearer rvz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Tokens podem ter escopo `messages:read`, `messages:write`, ou ambos. Token inválido, revogado ou expirado responde `401 invalid_token`.

## Endereçamento

| Campo | Uso |
| --- | --- |
| `from` | Obrigatório. Número conectado ou `@usuario` do Instagram. Formatação de telefone é normalizada. |
| `to` | Telefone do contato. Canal WhatsApp. |
| `bsuid` | ID Meta de um contato WhatsApp sem telefone exposto. |
| `instagram_id` | ID do contato no Instagram. |

Erros de resolução: `404 unknown_sender`, `404 unknown_recipient`, `404 no_conversation`, `409 ambiguous_recipient`.

## Rotas

| Método | Rota | Escopo | Finalidade |
| --- | --- | --- | --- |
| POST | `/messages` | `messages:write` | Envia texto, mídia ou modelo. |
| GET | `/conversations` | `messages:read` | Lista conversas ou retorna uma conversa endereçada. |
| GET | `/messages` | `messages:read` | Histórico de uma conversa. |
| GET | `/contacts` | `messages:read` | Lista contatos ou retorna um contato endereçado. |
| POST | `/media` | `messages:write` | Upload multipart e criação de `media_id`. |
| GET | `/media/:id` | `messages:read` | Gera nova URL assinada de uma mídia. |

Listagens aceitam `limit` (padrão 50, máximo 200) e `cursor`. Uma resposta paginada tem a forma `{ "data": [...], "next_cursor": "..." }`; ausência de `next_cursor` significa fim da lista.

## Enviar mensagens

`POST /messages` aceita exatamente um modo de conteúdo: `text`, `media`/`media_id` ou `template_name`. Dois modos, ou `media` junto de `media_id`, retornam `400 invalid_request`.

### Texto

```bash
curl -X POST https://api.ruvz.com.br/v1/messages \
  -H "Authorization: Bearer $RUVZ_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-8842-status" \
  -d '{
    "from": "5511988880000",
    "to": "5511999990000",
    "text": "Seu pedido saiu para entrega."
  }'
```

### Mídia por URL ou base64

`media` pode ser uma URL pública `https://` (até 16MB) ou um data URI base64 (até 5MB decodificado). Campos opcionais: `media_type` (`image`, `video`, `audio`, `document`), `caption`, `file_name`.

```json
{
  "from": "5511988880000",
  "to": "5511999990000",
  "media": "https://exemplo.com/nota-fiscal.pdf",
  "caption": "Segue a nota fiscal",
  "file_name": "nota-fiscal.pdf"
}
```

```json
{
  "from": "5511988880000",
  "to": "5511999990000",
  "media": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
  "media_type": "image"
}
```

Para URL, uma falha de download ou tipo inválido é informada posteriormente por `message.status` com `status: "failed"`.

### Upload e `media_id`

```bash
curl -X POST https://api.ruvz.com.br/v1/media \
  -H "Authorization: Bearer $RUVZ_TOKEN" \
  -F "from=5511988880000" \
  -F "file=@./nota-fiscal.pdf;type=application/pdf"
```

Resposta:

```json
{
  "media_id": "YWNjXzEvZmxvdy...",
  "mime_type": "application/pdf",
  "file_name": "nota-fiscal.pdf",
  "size_bytes": 184320
}
```

Use o ID em um envio:

```json
{
  "from": "5511988880000",
  "to": "5511999990000",
  "media_id": "YWNjXzEvZmxvdy...",
  "file_name": "nota-fiscal.pdf"
}
```

### Modelo aprovado

Modelos aprovados são obrigatórios fora da janela do WhatsApp e podem iniciar uma nova conversa com `to`. `name` só é necessário quando esse telefone ainda não é um contato.

```json
{
  "from": "5511988880000",
  "to": "5511999990000",
  "name": "Ana Souza",
  "template_name": "confirmacao_pedido",
  "parameters": ["8842"]
}
```

Todo envio bem-sucedido responde:

```json
{ "message_id": "msg_456", "status": "pending" }
```

## Leituras

```bash
# Listar conversas de um número
curl -G https://api.ruvz.com.br/v1/conversations \
  -H "Authorization: Bearer $RUVZ_TOKEN" \
  --data-urlencode "from=5511988880000" \
  --data-urlencode "status=open" \
  --data-urlencode "limit=50"

# Histórico de uma conversa
curl -G https://api.ruvz.com.br/v1/messages \
  -H "Authorization: Bearer $RUVZ_TOKEN" \
  --data-urlencode "from=5511988880000" \
  --data-urlencode "to=5511999990000"

# Procurar contatos
curl -G https://api.ruvz.com.br/v1/contacts \
  -H "Authorization: Bearer $RUVZ_TOKEN" \
  --data-urlencode "from=5511988880000" \
  --data-urlencode "search=Ana"

# Renovar URL de mídia
curl https://api.ruvz.com.br/v1/media/YWNjXzEvZmxvdy... \
  -H "Authorization: Bearer $RUVZ_TOKEN"
```

Objetos retornados incluem: conversa (`id`, `contact_id`, `to`/`bsuid`/`instagram_id`, `channel`, `status`, `window_expires_at`, `last_message_at`); mensagem (`id`, `conversation_id`, `direction`, `type`, `text`, `media_id`, `status`, `origin`, `created_at`); contato (`id`, `name`, `to`, `phone`, `bsuid`, `instagram_id`, `email`, `blocked`, `created_at`).

`GET /media/:id` retorna `{ "media_id", "url", "expires_at" }`. A URL é assinada e vale 24 horas.

## Idempotência

Use `Idempotency-Key` em cada `POST /messages`. Repetir mesma chave e mesmo corpo retorna a resposta original com `Idempotent-Replay: true`. A mesma chave com outro corpo retorna `409 idempotency_key_reuse`; enquanto o primeiro request está em curso, retorna `409 idempotency_in_flight`. A janela é de 24h.

## Webhooks

Cadastre uma URL HTTPS no fluxo. A Ruvz faz POST com JSON assinado. Responda `2xx` em até 10 segundos e processe o evento de modo assíncrono. A entrega é ao-menos-uma-vez e não possui ordenação garantida: deduplique por `event_id` e ordene por `occurred_at` quando necessário.

Envelope comum:

```json
{
  "api_version": "2026-09-01",
  "event_id": "evt_01J...",
  "delivery_id": "dlv_01J...",
  "type": "message.received",
  "occurred_at": "2026-09-01T14:22:31.412Z",
  "account_id": "acc_...",
  "flow_id": "flow_...",
  "data": {}
}
```

Headers: `X-Ruvz-Event`, `X-Ruvz-Delivery-Id`, `X-Ruvz-Timestamp`, `X-Ruvz-Signature-256: sha256=<hex>`. Verifique HMAC-SHA256 de `"<timestamp>.<corpo cru>"`, comparação em tempo constante e desvio máximo de 5 minutos.

Há até 5 tentativas de entrega. `410` desativa o webhook; os demais erros ou timeout são repetidos com backoff exponencial (10s a 600s).

## Todos os eventos

| Evento | Ação esperada |
| --- | --- |
| `message.received` | Criar/atualizar mensagem inbound. `data` traz endereço, mensagem, tipo, texto e mídia quando aplicável. |
| `message.sent` | Criar/atualizar mensagem outbound. `origin` é `api`, `inbox`, `echo` ou `campaign`; `client_reference` é a chave de idempotência quando o envio foi seu. |
| `message.status` | Atualizar status da mensagem: `sent`, `delivered`, `read` ou `failed`. Em falha, usar `error_code` e `error_message`. |
| `message.deleted` | Apagar/ocultar conteúdo local da mensagem identificada por `message_id`. |
| `message.edited` | Atualizar o texto existente, sem criar uma mensagem nova. |
| `message.reaction` | Atualizar reação; `emoji` vazio significa remoção. |
| `contact.created` | Fazer upsert do contato. |
| `contact.updated` | Atualizar dados do contato. |
| `conversation.opened` | Fazer upsert e marcar aberta; também pode indicar reabertura. |
| `conversation.closed` | Marcar a conversa como fechada. |

Campos relevantes por grupo:

- Eventos de mensagem: `message_id`, `conversation_id`, `from`, `to`/`bsuid`/`instagram_id`, `direction`, `type`, `status`, `origin`, `text`, `created_at`; mídia pode incluir `media_id`, `media_url`, `media_mime_type`, `media_file_name`.
- `message.status`: `message_id`, `conversation_id`, `status`, `occurred_at`, e em falhas `error_code`/`error_message`.
- `message.deleted`: `message_id`, `conversation_id`, `deleted_by`.
- `message.edited`: `message_id`, `conversation_id`, `text`, `edited_by`.
- `message.reaction`: `message_id`, `conversation_id`, `emoji`, `by`.
- Eventos de contato: `contact_id`, `name`, `to`, `bsuid`, `phone`, `instagram_id`, `email`, `created_at`.
- Eventos de conversa: `conversation_id`, `contact_id`, `from`, `to`/`bsuid`/`instagram_id`, `channel`, `status`, `window_expires_at`.

## Erros e limites

Erros têm corpo `{ "error": { "code": "...", "message": "..." } }`. Trate o `code`, não o texto. Erros frequentes: `invalid_request`, `window_expired`, `template_not_approved`, `invalid_template_parameters`, `unsupported_media_type`, `media_type_mismatch`, `file_too_large`, `rate_limited` e `channel_disconnected`.

Os menos frequentes, que ainda assim precisam de tratamento:

| Código | HTTP | Significado |
| --- | --- | --- |
| `invalid_idempotency_key` | 400 | `Idempotency-Key` fora do formato aceito. |
| `token_misconfigured` | 403 | O token não tem fluxo algum atribuído. Não adianta repetir: gere um novo no painel. |
| `unsupported_channel` | 422 | A operação não existe neste canal. Modelos são exclusivos do WhatsApp. |
| `internal_error` | 500 | Falha nossa. A mensagem não foi registrada; repetir com a **mesma** `Idempotency-Key` é seguro. |
| `storage_unavailable` | 503 | Armazenamento de mídia indisponível. Repita depois. |

Limites: 500 requests/s por token (pico 1000); upload e mídia por URL até 16MB; base64 até 5MB decodificado; URL de mídia e idempotência válidos por 24h.
