> Índice da documentação para agentes: https://gaio.social/developers/llms.txt

# API da Gaio

Conecte os seus sistemas à sua conta da Gaio: leia e atualize contatos, acompanhe conversas, envie mensagens e opere as suas automações por código.

## Como obter acesso

Durante o beta fechado, a API é liberada conta por conta.

1. **Fale com a gente**: Chame o suporte da Gaio no WhatsApp e diga o @ da conta que vai usar a API. [Abrir o WhatsApp](https://wa.me/551152832977?text=Ol%C3%A1!%20Quero%20acesso%20%C3%A0%20API%20da%20Gaio.%20Minha%20conta%3A%20%40)
2. **Liberamos o acesso**: Liberamos a API para a sua conta e avisamos você.
3. **Crie a sua chave**: O dono da conta cria a chave no console da Gaio, em Configurações › Desenvolvedores › Chaves de API.

## O que a API cobre

Tudo isto já está disponível na `/v1`, no beta fechado:

- **Contatos, etiquetas e campos personalizados**: listar, buscar e sincronizar contatos, criar contatos do WhatsApp, pôr e tirar etiquetas, preencher e limpar campos personalizados, listar, criar, renomear e excluir etiquetas, saber quem tem uma etiqueta, e listar as conexões e os campos personalizados.
- **Conversas e mensagens**: ler as conversas, as mensagens e a mídia delas, e enviar mensagens com texto, botões de link e de resposta, mídia, cards e templates do WhatsApp.
- **Automações**: criar, editar, validar, publicar, despublicar e excluir, consultar as versões, acompanhar as execuções e iniciar uma execução para um contato.
- **[Pastas](https://gaio.social/developers/automacoes.md#pastas) e [listas de respostas](https://gaio.social/developers/automacoes.md#listas-de-respostas)** das automações: criar, renomear, mover e excluir pastas, pôr as automações nelas, e criar, alterar e excluir as listas de respostas públicas aos comentários.
- **[Webhooks](https://gaio.social/developers/webhooks.md)**: o seu sistema é avisado na hora quando algo acontece na conta.

A API cresce dentro da `/v1`: uma parte nova chega como rotas novas e aparece na referência da API assim que estiver no ar.

## Configuração

### Criar uma chave

No console da Gaio, vá em **Configurações › Desenvolvedores › Chaves de API**. O item **Desenvolvedores** só aparece para o dono da conta, e só depois que a API é liberada para ela.

Clique em **Criar chave** e preencha:

- **Nome**: onde a chave vai ser usada, como "Integração com o CRM". É o que identifica a chave na lista.
- **Permissões**: os [escopos](https://gaio.social/developers/index.md#escopos) da chave. Marque só o que a integração precisa.
- **Validade**: 30, 60, 90 ou 180 dias, ou 1 ano. Toda chave expira: não existe chave sem validade.

Só o dono da conta, entrando com o próprio login, cria e revoga chaves. Membros da equipe não têm essa opção, e o suporte da Gaio também não, mesmo quando está acessando a sua conta: a chave continua funcionando depois que a sessão termina, então só quem responde pela conta pode criá-la.

Cada conta pode ter até 25 chaves ativas ao mesmo tempo. Chaves revogadas ou expiradas não contam para esse limite.

### Guardar a chave

A chave aparece completa uma única vez, logo depois de criada. Copie e guarde em um gerenciador de segredos ou em uma variável de ambiente do servidor que vai chamar a API. A Gaio não guarda a chave em si, então não tem como mostrá-la de novo: se você perder a chave, revogue e crie outra.

Nunca coloque a chave:

- em código que roda no navegador ou no celular de quem usa o seu produto;
- em um repositório de código, mesmo que seja privado;
- em uma URL, porque URLs ficam registradas em históricos e logs.

Quem tem a chave age em nome da sua conta, dentro das permissões dela. Se uma chave vazar, revogue na hora.

A chave tem o formato `gaio_` seguido de 46 letras e números. O prefixo fixo ajuda a encontrar uma chave vazada, por exemplo com uma regra na varredura de segredos do seu repositório.

### Fazer a primeira chamada

A URL base da API é `https://api.gaio.social`. Envie a chave no cabeçalho `Authorization`, no formato `Bearer`:

```bash
curl https://api.gaio.social/v1/me \
  -H "Authorization: Bearer $GAIO_API_KEY"
```

A rota `GET /v1/me` não pede escopo e funciona com qualquer chave válida. Ela responde com a conta e a chave usadas na chamada:

```json
{
  "account": {
    "id": "…",
    "name": "…",
    "alias": "…"
  },
  "api_key": {
    "id": "…",
    "name": "…",
    "scopes": ["contacts:read"],
    "expires_at": "…"
  }
}
```

A API é feita para chamadas de servidor para servidor. Ela não libera CORS, então uma página aberta no navegador não consegue chamá-la.

### Trocar a chave

Para trocar uma chave sem interromper a integração:

1. Crie uma chave nova, com as mesmas permissões.
2. Passe o seu sistema para a chave nova e confira que as chamadas funcionam. A rota `GET /v1/me` serve para isso.
3. Revogue a chave antiga.

Faça a troca antes de a chave expirar. A data de expiração aparece na lista de chaves do console e no campo `api_key.expires_at` de `GET /v1/me`, que o seu sistema pode acompanhar.

### Revogar uma chave

No console, clique em **Revogar** ao lado da chave. Revogar é definitivo: para reconectar, é preciso criar uma chave nova.

A revogação vale em até 1 minuto. O mesmo vale quando a API é desligada para a conta. Nesse intervalo, chamadas com a chave ainda podem ser aceitas.

## Escopos

Cada chave só faz o que os escopos dela permitem. No console, eles aparecem como permissões, na criação da chave.

| Escopo | No console | O que a chave pode fazer |
| --- | --- | --- |
| `contacts:read` | Ler contatos | Ler os contatos, as etiquetas, os campos personalizados e as conexões da conta (as contas do Instagram e os números do WhatsApp). |
| `contacts:write` | Criar e editar contatos | Criar contatos do WhatsApp, pôr e tirar etiquetas, preencher e limpar campos personalizados, e criar, renomear e excluir etiquetas. |
| `conversations:read` | Ler conversas | Ler as conversas, as mensagens e a mídia delas. |
| `messages:send` | Enviar mensagens | Enviar mensagens nas conversas (texto, botões de link e de resposta, mídia, cards e templates do WhatsApp), subir a mídia que elas enviam e iniciar uma automação para um contato. As mensagens chegam de verdade. |
| `workflows:read` | Ler automações | Ler as automações, as versões e as execuções delas, as pastas e as listas de respostas, validar uma automação e ler o catálogo de blocos. |
| `workflows:write` | Criar e editar automações | Criar automações, editar o rascunho delas e excluí-las, e criar, alterar e excluir pastas e listas de respostas. |
| `workflows:publish` | Publicar automações | Publicar uma automação, que a partir daí responde os seus contatos, e tirá-la do ar. Excluir uma automação publicada também pede este escopo. |
| `webhooks:read` | Ler webhooks | Ler os endpoints de [webhook](https://gaio.social/developers/webhooks.md) da conta e as entregas deles. |
| `webhooks:write` | Gerenciar webhooks | Cadastrar, alterar e excluir endpoints de webhook, trocar o segredo deles, enviar um evento de teste e reenviar entregas. Cada tipo de evento também pede o escopo que lê os dados dele. |

Uma integração com um CRM que sincroniza contatos, põe etiquetas e responde conversas, por exemplo, precisa de `contacts:read`, `contacts:write`, `conversations:read` e `messages:send`. Para iniciar uma automação para um contato basta `messages:send`; para encontrar a automação e acompanhar as execuções, `workflows:read`.

Uma chave fica com exatamente os escopos com que foi criada: não dá para adicionar um escopo depois. Para ter um escopo novo, crie uma chave nova com ele e revogue a antiga.

Uma chamada para uma rota que exige um escopo que a chave não tem recebe `403`, com o código `insufficient_scope` (veja [Erros](https://gaio.social/developers/index.md#erros)).

## Convenções

### Formato

As requisições e as respostas são JSON, com os nomes dos campos em `snake_case`, como `contact_id` e `next_cursor`. Os ids nos caminhos são UUIDs, e os filtros de data recebem data e hora no formato ISO 8601, como `2026-10-05T14:30:00Z`. Codifique os valores dos parâmetros de consulta; nos exemplos, o `--data-urlencode` do curl faz isso.

Toda resposta de sucesso traz o cabeçalho `Cache-Control: no-store`: ela vale só para a chave que fez a chamada, e nenhum cache no caminho deve guardá-la.

### Listas

As listas são paginadas por cursor, com os parâmetros `limit`, de 1 a 100 (o padrão é 50), e `cursor`. A resposta tem o formato `{ "data": [...], "next_cursor": "…" }`.

Para ler a página seguinte, envie o `next_cursor` como `cursor`. Na última página, `next_cursor` vem `null`. O cursor é opaco: devolva exatamente o que a página anterior respondeu. Um cursor que não veio daquela lista recebe `400`, com o código `invalid_cursor`.

A exceção é `GET /v1/webhooks`: a conta tem no máximo 10 endpoints, então a lista vem numa página só, com `next_cursor: null`. Ela não aceita `limit` nem `cursor`, que recebem `400` como qualquer parâmetro de consulta desconhecido.

### Idempotência

As rotas que alteram algo aceitam o cabeçalho `Idempotency-Key`, a chave de idempotência: um texto de 1 a 255 caracteres que identifica a operação. Um UUID é uma boa escolha. Gere uma chave de idempotência nova para cada operação e repita a mesma só nas novas tentativas dela.

Testar um webhook (`POST /v1/webhooks/{webhookId}/test`) e reenviar uma entrega dele não têm o cabeçalho e podem ser repetidos sem risco: o teste não guarda nada, e um reenvio repetido encontra a entrega já na fila.

Enviar uma mensagem e iniciar uma automação para um contato exigem o cabeçalho, porque o efeito chega a pessoas de fora da conta. A referência marca o cabeçalho como obrigatório nessas duas rotas, e sem ele a resposta é `400`, como qualquer requisição fora do formato da rota.

Durante 24 horas, na mesma conta:

- a mesma chave de idempotência com a mesma requisição devolve a primeira resposta, sem fazer nada duas vezes;
- a mesma chave de idempotência com uma requisição diferente recebe `422`, com `idempotency_key_reused`;
- a mesma chave de idempotência enquanto a primeira requisição ainda está em andamento recebe `409`, com `idempotency_in_progress`;
- uma requisição que falhou libera a chave de idempotência, então repetir com ela executa a requisição de novo.

A exceção é o `outcome_unknown`, quando não dá para saber se a operação aconteceu: o canal ou o motor de automações não confirmou a tempo depois de receber o pedido (`502`), ou a primeira requisição com a chave de idempotência parou antes de responder (`409`). A chave de idempotência guarda essa resposta, e repetir com ela devolve o mesmo erro. Confira o resultado, como as mensagens da conversa ou as execuções da automação, antes de tentar de novo com uma chave de idempotência nova.

Se a chave de idempotência não puder ser conferida, a resposta é `503`, com `service_unavailable`, e nada é executado sem a garantia: tente de novo.

### O que fica registrado

As alterações que uma chave faz nas automações (criar, editar, publicar, despublicar e excluir) e nos webhooks (cadastrar, alterar, excluir e trocar o segredo) entram no **Log de auditoria** da conta como uma alteração feita no console, com o dono da chave como usuário e a própria chave como autora (`actor_type: api_key`).

As alterações nas etiquetas, nas pastas, nas listas de respostas, nos contatos e na mídia não entram no log de auditoria, como as mesmas alterações feitas no console também não entram. As etiquetas e os valores de campos que a chave muda em um contato aparecem no histórico do contato, no console, com o nome da chave.

Uma mensagem enviada pela API também não entra no log de auditoria: a própria mensagem registra a chave com que foi enviada, e a API a mostra com `sender.type: "api"`.

## Contatos

Um contato é uma pessoa em um canal. `channel.type` diz o canal (`instagram` ou `whatsapp`; outros podem aparecer) e `channel.connection_id`, a conexão da conta por onde o contato chegou. Um contato do Instagram tem `instagram.id`, que vale para aquela conexão, e `instagram.username`; um do WhatsApp tem `whatsapp.phone`, no formato E.164. A mesma pessoa no Instagram e no WhatsApp são dois contatos com o mesmo `person_id`.

As conexões da conta, as contas do Instagram e os números do WhatsApp, estão em `GET /v1/connections`.

### Sincronizar os contatos

Para a primeira cópia, leia `GET /v1/contacts` até o fim. A lista vai do contato com a atividade mais antiga para o mais recente.

```bash
curl -G https://api.gaio.social/v1/contacts \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -d limit=100
```

```json
{
  "data": [
    {
      "id": "…",
      "channel": { "type": "whatsapp", "connection_id": "…" },
      "instagram": null,
      "whatsapp": { "phone": "+5511987654321" },
      "name": "…",
      "person_id": "…",
      "tags": [{ "id": "…", "name": "Cliente" }],
      "custom_fields": [
        {
          "field_id": "…",
          "name": "Plano",
          "type": "dropdown",
          "value": "pro"
        }
      ],
      "last_interaction_at": "…",
      "created_at": "…",
      "updated_at": "…"
    }
  ],
  "next_cursor": "…"
}
```

Enquanto `next_cursor` não vier `null`, peça a página seguinte com ele:

```bash
curl -G https://api.gaio.social/v1/contacts \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -d limit=100 \
  --data-urlencode "cursor=$NEXT_CURSOR"
```

Nas sincronizações seguintes, leia só o que mudou com `updated_since`, marcado alguns minutos antes do início da sincronização anterior, e grave cada contato pelo `id`, atualizando o que já existe. Um contato que mudou durante a sincronização volta na próxima: ele nunca fica de fora.

```bash
curl -G https://api.gaio.social/v1/contacts \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -d limit=100 \
  --data-urlencode "updated_since=2026-10-05T13:55:00Z"
```

O `updated_since` tem limites, porque o `updated_at` é a última atividade do contato (uma mensagem ou uma edição no console), e não a última mudança nos dados dele:

- as mudanças feitas pela API não movem o `updated_at`, e importações e ações em massa também não;
- uma etiqueta ou um valor alterado por uma automação, por uma ação em massa, por outra chave ou por uma remoção em outro contato da mesma pessoa não traz o contato de volta;
- um contato excluído também não volta.

Os [eventos de contatos](https://gaio.social/developers/webhooks.md#contatos) dos webhooks avisam cada etiqueta e cada valor que muda num contato, venha a mudança de onde vier, menos a exclusão ou a renomeação da própria etiqueta ou do campo, que não gera evento nos contatos. Se você copia as etiquetas e os campos personalizados, faça também uma leitura completa de tempos em tempos.

Para encontrar um contato, filtre por `phone`, `instagram_username` ou `instagram_id`. No `phone`, um celular brasileiro é encontrado com ou sem o nono dígito, e um número de fora do Brasil precisa do `+`. O `instagram_username` vale com ou sem o `@`.

```bash
curl -G https://api.gaio.social/v1/contacts \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  --data-urlencode "phone=+5511987654321"
```

### Etiquetar um contato

Cada etiqueta tem um `id`. Liste as etiquetas da conta com `GET /v1/tags`, ou crie uma com `POST /v1/tags`:

```bash
curl https://api.gaio.social/v1/tags \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Cliente"}'
```

Os nomes de etiqueta não se repetem, nem com maiúsculas e minúsculas diferentes. Se a conta já tem uma etiqueta com o nome, a resposta é `409`, com o código `tag_already_exists` e o `id` dela em `context.tag_id`.

Para pôr a etiqueta no contato:

```bash
curl https://api.gaio.social/v1/contacts/$CONTACT_ID/tags/$TAG_ID \
  -X PUT \
  -H "Authorization: Bearer $GAIO_API_KEY"
```

A resposta traz as etiquetas do contato:

```json
{
  "contact_id": "…",
  "tags": [{ "id": "…", "name": "Cliente" }]
}
```

Pôr uma etiqueta que o contato já tem não muda nada, então repetir a chamada é seguro. Para tirar a etiqueta, use `DELETE` no mesmo caminho: ela sai de todos os contatos da mesma pessoa, como no console.

### Gerenciar etiquetas

Para saber quem tem uma etiqueta, use `GET /v1/tags/{tagId}/contacts`, na ordem em que os contatos ganharam a etiqueta; com `tagged_since`, só os que a ganharam a partir daquele momento. Tirar e pôr de novo a etiqueta leva o contato para o fim da lista.

Para renomear uma etiqueta, envie o `name` novo em `PATCH /v1/tags/{tagId}`: os contatos que a têm continuam com ela, com o nome novo, e um nome que outra etiqueta já tem recebe `tag_already_exists`, como na criação. `DELETE /v1/tags/{tagId}` exclui a etiqueta e a tira de todos os contatos que a têm. Nenhum dos dois move o `updated_at` dos contatos nem gera eventos de webhook neles.

Uma etiqueta que uma automação da conta usa, num bloco de etiquetas ou numa condição de etiqueta, no rascunho ou na versão publicada, não é renomeada nem excluída: a resposta é `409`, com `tag_in_use` e as automações em `context.workflow_ids`. A automação procura a etiqueta pelo nome, então até uma troca só de maiúsculas e minúsculas é recusada. Mude as automações antes; as excluídas e os templates não contam.

### Campos personalizados

Os valores dos campos personalizados de um contato são gravados pelo `field_id` de cada campo (`GET /v1/custom-fields`): `PATCH /v1/contacts/{contactId}/custom-fields` preenche alguns valores, e `DELETE /v1/contacts/{contactId}/custom-fields/{fieldId}` limpa um. Um campo `number` recebe um número JSON (`"1.234"` é recusado); `text` e `dropdown` recebem um texto de 1 a 1000 caracteres, e o `dropdown` aceita o `value` ou o `label` de uma opção. A requisição é conferida inteira: se um valor for recusado, nenhum é gravado.

Criar campos personalizados continua no console.

### Criar um contato do WhatsApp

`POST /v1/contacts` cria um contato do WhatsApp, como a importação de uma planilha de uma linha: num número de WhatsApp conectado da conta e com `consent: true`, que fica registrado no histórico do contato. Se a conta já tem um contato com o telefone, ele é atualizado, mantém o `id` e a resposta é `200`, em vez de `201`.

O corpo leva `phone`, `name`, `connection_id` e `consent` e, opcionais, `tag_ids` (até 20 etiquetas) e `custom_fields` (até 50 valores, como no `PATCH` dos [campos personalizados](https://gaio.social/developers/index.md#campos-personalizados)). Num contato que já existe, o `name` substitui o nome, as etiquetas e os valores enviados são acrescentados, e as etiquetas e os valores que não vieram no corpo ficam como estão.

Criar o contato não abre a janela de 24 horas para enviar mensagens (veja [Enviar uma mensagem](https://gaio.social/developers/index.md#enviar-uma-mensagem)). Um contato criado assim só é cobrado depois que a primeira mensagem com ele é registrada, enviada ou recebida.

## Conversas e mensagens

Uma conversa é a troca de mensagens de um contato com a conta, no canal do contato. O `id` da conversa é opaco; o do contato está em `contact.id`. A mesma pessoa no Instagram e no WhatsApp são duas conversas com o mesmo `person_id`.

### Acompanhar as conversas

`GET /v1/conversations` lista o que o Live Chat do console lista, pela última mensagem, da mais recente para a mais antiga. Um contato que só comentou em um post não tem conversa até mandar uma mensagem direta ou até a conta escrever para ele. Os filtros são `channel`, `unread` (`true`: com mensagens não lidas ou marcada como não lida) e `last_message_after`.

Para sincronizar, leia em ordem crescente a partir do maior `last_message_at` que você já viu, até `next_cursor` vir `null`:

```bash
curl -G https://api.gaio.social/v1/conversations \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -d order=asc \
  -d limit=100 \
  --data-urlencode "last_message_after=2026-10-05T13:55:00Z"
```

Em ordem crescente, uma conversa que recebe uma mensagem enquanto você lê vai para o fim da lista, em vez de ficar de fora. As mensagens recebidas chegam com o horário do canal e alguns segundos de atraso: comece cada sincronização alguns minutos antes do último `last_message_at` e descarte as mensagens repetidas pelo `id`.

Para saber de cada mensagem quando ela chega, sem consultar a lista, use os [eventos de mensagens](https://gaio.social/developers/webhooks.md#mensagens-e-comentários) dos webhooks.

Uma conversa sozinha está em `GET /v1/conversations/{conversationId}`, que lê também as que a lista deixa de fora. As mensagens dela estão em `GET /v1/conversations/{conversationId}/messages`, da mais recente para a mais antiga (`order=asc` para importar a conversa; `created_after` para só as posteriores a um momento). `sender.type` diz quem enviou: `contact`, `user` (alguém da equipe), `workflow`, `api` ou `unattributed` (a conta, sem registro de como, por exemplo uma mensagem digitada no app do Instagram ou do WhatsApp).

Um toque num botão é uma mensagem do contato com o rótulo em `text` e `button` (`id`, `title` e `source`: `api`, `workflow`, `ice_breaker` ou `other`).

A mídia de cada anexo tem uma `url` da própria API, que pede a mesma chave e redireciona (`302`) para um download válido por 5 minutos. A mídia recebida fica guardada por até 90 dias, então baixe ao sincronizar. Logo depois que a mensagem chega, a mídia ainda pode estar sendo guardada (`404`): tente de novo em instantes.

### Enviar uma mensagem

Envie a mensagem na conversa do contato, com uma chave de idempotência:

```bash
curl https://api.gaio.social/v1/conversations/$CONVERSATION_ID/messages \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"text": "Olá! Seu pedido saiu para entrega."}'
```

O corpo tem um destes formatos:

| Corpo | Instagram | WhatsApp |
| --- | --- | --- |
| `text` | Até 1000 caracteres. | Até 4096 caracteres. |
| `text` e `buttons` | Texto de até 640 caracteres e de 1 a 3 botões, de link ou de resposta. | Texto de até 1024 caracteres e 1 botão de link, ou de 1 a 3 botões de resposta com títulos diferentes. |
| `attachments` com uma mídia (`image`, `video`, `audio` ou `file`) | Sem `text`. | O `text` é a legenda, de até 1024 caracteres, numa imagem, num vídeo ou num arquivo. |
| `attachments` com `cards` | De 1 a 10 cards, sem `text`, cada um com até 3 botões, de link ou de resposta. | De 2 a 10 cards, cada um com imagem e 1 botão de link ou de 1 a 3 botões de resposta, o mesmo tipo e o mesmo número em todos, e um `text` obrigatório de até 1024 caracteres acima deles. |
| `template` | Não existe. | Um template aprovado da conta, sozinho. |

Um botão tem um rótulo (`title`) de até 20 caracteres e é de um destes tipos:

- `{"type": "open_url", "title": "…", "url": "…"}`, um botão de link: abre uma URL `https` absoluta, enviada como está, sem contar os toques.
- `{"type": "reply", "id": "…", "title": "…"}`, um botão de resposta: o `id` é seu, de 1 a 64 letras, dígitos, `_` e `-`, sem repetir entre os botões de resposta da mensagem, cards incluídos (`invalid_message`, com `reason: duplicate_button_id`).

O toque num botão de resposta volta como uma mensagem do contato, com o rótulo em `text`, `button: { id, title, source: "api" }` e a mensagem do botão em `reply_to_message_id`, e também como um evento [`message.received`](https://gaio.social/developers/webhooks.md#mensagens-e-comentários) dos webhooks. Ele não inicia nem retoma nenhuma automação.

Os limites de cada card estão na referência da API. O que um canal não leva recebe `422`: `content_not_supported`, ou `text_too_long` para um texto longo demais naquele lugar, com o campo em `context.field` e o limite em `context.max_length`. No WhatsApp, `content_not_supported` também recusa botões de link e de resposta juntos, dois botões de resposta com o mesmo título e cards com botões diferentes.

A resposta, `201`, é a mensagem enviada, como `GET /v1/messages/{messageId}` a lê, com `sender.type: "api"`. Um texto com botões aparece como um anexo `cards` com um card só, cujo `title` é o texto (o `text` vem `null`), como as mensagens com botões das automações. Nos cards, um botão de resposta aparece com `type: "postback"` e o `id` dele, e os outros botões com `id: null`. Um template aparece com o corpo já preenchido em `text`, e o nome e o idioma dele em `template`. O `status` é `sent` quando o canal aceitou a mensagem. Um envio pelo WhatsApp ainda pode falhar depois disso: `GET /v1/messages/{messageId}` passa a mostrar `failed`, e os webhooks enviam um [`message.failed`](https://gaio.social/developers/webhooks.md#mensagens-e-comentários).

**Mídia.** Para enviar um arquivo, crie a mídia com `POST /v1/media`, informando o tipo do arquivo:

```bash
curl https://api.gaio.social/v1/media \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content_type": "image/jpeg"}'
```

A resposta traz o `id`, que o envio usa como `media_id`, e a `upload_url`. Mande o arquivo para a `upload_url` com `PUT` em até 5 minutos, com o mesmo `Content-Type`:

```bash
curl "$UPLOAD_URL" \
  -X PUT \
  -H "Content-Type: image/jpeg" \
  --data-binary @foto.jpg
```

Depois, envie `{"attachments": [{"type": "image", "media_id": "…"}]}` na conversa. A mídia fica guardada enquanto a conta existir, então o mesmo `media_id` pode ser enviado quantas vezes você quiser. Criar a mídia pede o escopo `messages:send` e conta como escrita nos limites de uso.

O tamanho não é conferido no upload, e sim no envio, pelo que o canal da conversa aceita:

| Tipo | Instagram | WhatsApp |
| --- | --- | --- |
| `image` | JPEG, PNG ou GIF, até 8 MB | JPEG, PNG ou WebP, até 5 MB |
| `video` | MP4, OGG, WebM, AVI ou QuickTime, até 25 MB | MP4, até 16 MB |
| `audio` | AAC, MP4, M4A ou WAV, até 25 MB | AAC, MP3 ou MP4, até 16 MB |
| `file` | PDF, até 25 MB | PDF, até 100 MB |

Uma mídia fora disso recebe `media_not_supported`, com o motivo em `context.reason`: `type_mismatch` (o arquivo não é do `type` informado), `content_type` ou `too_large`.

**Templates.** No WhatsApp, `template` envia um template aprovado da conta, pelo `name` e pelo `language` (como `pt_BR`), com um valor em `body_variables` para cada `{{1}}`, `{{2}}`… do corpo. O template vai sozinho, sem `text`, `attachments` ou `buttons`. A API não envia um template que precise de outros parâmetros, como mídia ou variáveis no cabeçalho, variáveis nos botões, carrossel, variáveis com nome ou código de autenticação: a resposta é `422`, com `template_not_supported`.

**A janela de 24 horas.** Fora o template, a API só envia dentro da janela de mensagens do canal: 24 horas a partir da última mensagem do contato naquela conexão. O campo `reply_window` da conversa antecipa a resposta, com `state` (`open` ou `closed`) e `closes_at`, quando a janela fecha; o envio confere de novo. Fora da janela, a resposta é `422`, com o código `messaging_window_closed`. No WhatsApp, o código é `template_required`: só um template alcança o contato, e a janela abre de novo quando ele responde. A API não usa a tag de agente humano do Instagram, que declara que uma pessoa escreveu a mensagem, então no Instagram a janela também fecha em 24 horas.

A mensagem sai pela conexão da própria conversa. Quando o plano da conta inclui a assinatura da Gaio, ela é acrescentada dentro do limite do canal a um texto sozinho e a um texto acima de botões, nunca a uma legenda, a cards ou a um template, e a resposta traz o que foi entregue.

Como uma resposta digitada no console, o envio marca como lido o que o contato mandou até ali e passa os cards pendentes dele no inbox para em andamento. Ele não dispara as automações de "mensagem enviada" do Instagram.

**Quando o resultado é incerto.** Com `outcome_unknown`, a mensagem pode ter saído: confira as mensagens da conversa antes de tentar com uma chave nova, para não enviar duas vezes (veja [Idempotência](https://gaio.social/developers/index.md#idempotência)). No envio, o `502` diz que o canal não confirmou o envio, e `context.provider_accepted: true`, que ele aceitou a mensagem. No Instagram, uma recusa que o Instagram não explica também responde `outcome_unknown`, porque o erro genérico dele pode vir depois de a mensagem ter sido entregue. Um vídeo no Instagram também pode responder `outcome_unknown` mesmo tendo saído: o Instagram busca o vídeo pelo link e pode demorar mais para confirmar o envio do que a API espera.

## Automações

Uma automação tem um rascunho e uma versão publicada. Editar o grafo, o gatilho ou o canal muda o rascunho, e nada muda no que está rodando até a publicação, que guarda o grafo como uma versão. As execuções em andamento terminam com a cópia com que começaram, depois de uma publicação ou de uma despublicação.

Como escrever o corpo de uma automação (o canal, o gatilho e o grafo de blocos), com o catálogo dos blocos, as variáveis e exemplos prontos, está em [Montar automações pela API](https://gaio.social/developers/automacoes.md).

### Editar e publicar uma automação

Toda resposta que traz uma automação tem o cabeçalho `ETag`, com a `revision` dela. Para alterar a automação (`PATCH /v1/workflows/{workflowId}`), envie essa revisão no cabeçalho `If-Match`. Só os campos enviados mudam:

```bash
curl https://api.gaio.social/v1/workflows/$WORKFLOW_ID \
  -X PATCH \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "7"' \
  -d '{"title": "Boas-vindas"}'
```

Sem `If-Match`, a resposta é `428`, com `precondition_required`. Se a automação mudou depois daquela revisão, a resposta é `412`, com `version_conflict` e a revisão atual em `context.current_revision`: leia a automação de novo e refaça a alteração.

O editor do console salva sem revisão. Ele pode sobrescrever uma mudança feita pela API, enquanto uma mudança enviada pela API depois de uma feita no editor é recusada, em vez de sobrescrevê-la.

Antes de publicar, valide. `POST /v1/workflows/{workflowId}/validate` confere o rascunho salvo e responde a `revision` que conferiu; `POST /v1/workflows/validate` confere uma definição sem salvá-la. Os `errors` são exatamente o que faz a publicação ser recusada (`workflow_invalid`); os `warnings` não impedem a publicação, mas provavelmente não fazem o que você queria. As duas validações contam como leitura.

Depois, publique com `POST /v1/workflows/{workflowId}/publish`, que pede o escopo `workflows:publish`. Aqui o `If-Match` é opcional: com a `revision` da validação, você publica exatamente o que validou. Sem ele, a publicação leva o rascunho como está e responde `409`, com `workflow_changed`, se ele continuar mudando durante a publicação.

Ao reescrever um grafo, devolva como recebeu os tipos de bloco e os campos que você não conhece. Mantenha o `id` dos botões, porque as métricas de clique são contadas por ele, e a ordem das opções de um `randomizer` que já rodou, porque ele conta as escolhas pela posição (veja [O grafo](https://gaio.social/developers/automacoes.md#o-grafo)).

Cada publicação guarda o grafo como uma versão: `GET /v1/workflows/{workflowId}/versions` lista as versões, da mais recente para a mais antiga, e `GET /v1/workflows/{workflowId}/versions/{versionId}` lê uma, com o grafo que ela pôs no ar.

`POST /v1/workflows/{workflowId}/unpublish` tira a automação do ar: nada mais a inicia, e o rascunho fica como está. `DELETE /v1/workflows/{workflowId}` exclui a automação, tira do ar a que está publicada e para as execuções em andamento; excluir uma automação publicada também pede o escopo `workflows:publish`. Os dois aceitam um `If-Match` opcional: com ele, uma automação que mudou depois daquela revisão recebe `412`, com `version_conflict`.

### Iniciar uma automação para um contato

Encontre a automação em `GET /v1/workflows` (filtros `status`, `channel`, `connection_id`, `trigger_event` e `folder_id`) e inicie uma execução para o contato, com uma chave de idempotência:

```bash
curl https://api.gaio.social/v1/workflows/$WORKFLOW_ID/runs \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d "{\"contact_id\": \"$CONTACT_ID\"}"
```

A resposta é `202`, com o id da execução:

```json
{
  "id": "…",
  "workflow_id": "…",
  "contact_id": "…",
  "status": "queued",
  "created_at": "…"
}
```

A automação precisa estar publicada (`workflow_not_published`). A execução começa no canal da automação como um início simples, sem comentário para responder e sem texto de mensagem; por isso, se a automação envia mensagens, a janela de 24 horas do contato nesse canal precisa estar aberta (`messaging_window_closed`). O início conta para o mesmo limite de execuções por hora, por contato, de um gatilho (`run_limit_reached`), e um contato no meio de uma execução da mesma automação é recusado (`run_in_progress`).

A execução aparece em `GET /v1/workflows/{workflowId}/runs` alguns segundos depois, ou mais quando a fila da conta está cheia; filtre por `contact_id` para ver as do contato, e por `started_after` e `started_before` para um período. Uma execução esperando o contato fica `waiting`. O histórico de execuções guarda 90 dias e serve para acompanhar, não como registro de cobrança. Para saber quando a execução termina, sem consultar a lista, use o [`workflow_run.ended`](https://gaio.social/developers/webhooks.md#automações) dos webhooks.

Com `502` e `outcome_unknown`, confira as execuções da automação para o contato antes de tentar com uma chave nova (veja [Idempotência](https://gaio.social/developers/index.md#idempotência)).

### Pastas e listas de respostas

As pastas (`/v1/folders`) organizam as automações como no console: uma automação entra numa pasta pelo `folder_id`, e `GET /v1/workflows?folder_id=…` lista as que estão direto nela, sem as das subpastas. As listas de respostas (`/v1/replies-lists`) são o que uma automação do Instagram publica embaixo do comentário que a iniciou, e o gatilho aponta para uma pelo `replies_list_id`. Como criar e usar as duas está em [Pastas](https://gaio.social/developers/automacoes.md#pastas) e [Listas de respostas](https://gaio.social/developers/automacoes.md#listas-de-respostas).

## Limites de uso

Os limites valem por conta, para todas as chaves dela juntas, em janelas de 1 minuto. Cada tipo de chamada tem o seu próprio limite, então um pico de leituras não bloqueia uma escrita.

| Tipo de chamada | Limite por minuto |
| --- | --- |
| Leitura: toda rota `GET` e as validações de automação | 120 |
| Escrita: as outras rotas que alteram algo | 60 |
| Envio: enviar uma mensagem, iniciar uma automação para um contato, testar um webhook ou reenviar uma entrega dele | 20 |

Criar mais chaves não aumenta o limite.

Toda resposta de sucesso traz estes cabeçalhos:

| Cabeçalho | O que informa |
| --- | --- |
| `RateLimit-Limit` | O limite por minuto do tipo da chamada. |
| `RateLimit-Remaining` | Quantas chamadas desse tipo ainda cabem na janela atual. |
| `RateLimit-Reset` | Em quantos segundos a janela atual termina. |

Acima do limite, a resposta é `429`, com o código `rate_limited`, os mesmos cabeçalhos e o cabeçalho `Retry-After`, em segundos.

Para lidar com o limite:

- Acompanhe `RateLimit-Remaining` e diminua o ritmo antes de chegar a zero.
- Ao receber `429`, espere o tempo de `Retry-After` antes de tentar de novo.
- Nas novas tentativas, aumente a espera a cada falha e some um intervalo aleatório (*backoff* exponencial com *jitter*), para que várias instâncias do seu sistema não voltem todas no mesmo instante.

Nem todo `429` é o limite da conta: `run_limit_reached` é o limite de execuções de uma automação por contato, e `provider_throttled` é o canal segurando os envios da conta. Decida pelo `context.code`.

Além do limite por conta, a API inteira tem um teto de chamadas por segundo. Se ele for atingido, o `429` vem do gateway, sem `Retry-After` e sem o corpo de erro descrito abaixo. Trate do mesmo jeito, com *backoff*.

Antes do gateway, a borda da API bloqueia um IP com má reputação ou com mais de 1200 chamadas por minuto, somadas todas as contas que chamam dele. O bloqueio responde `403`, também sem o corpo de erro descrito abaixo. Quem chama a API por muitas contas de um mesmo servidor precisa ficar abaixo desse teto.

## Erros

Os erros das rotas têm sempre este formato:

```json
{
  "type": "error",
  "message": "The API key does not have the contacts:read scope.",
  "context": {
    "code": "insufficient_scope",
    "required_scope": "contacts:read"
  }
}
```

Use `context.code` para decidir o que fazer, e não só o status HTTP: o mesmo status tem códigos diferentes. A `message` é um texto para quem está depurando e pode mudar a qualquer momento: nunca dependa dela.

### Comuns às rotas

| Código | Status | Quando acontece | Campos extras em `context` |
| --- | --- | --- | --- |
| `insufficient_scope` | `403` | A chave não tem o escopo que a rota exige. | `required_scope` |
| `rate_limited` | `429` | A conta usou todas as chamadas daquele tipo no minuto. | `kind` (`read`, `write` ou `send`), `limit`, `retry_after` |
| `not_found` | `404` | O recurso não existe nesta conta. Um recurso de outra conta também responde assim. | — |
| `invalid_cursor` | `400` | O cursor não veio daquela lista. | — |
| `idempotency_key_reused` | `422` | A chave de idempotência já foi usada com outra requisição. | — |
| `idempotency_in_progress` | `409` | A primeira requisição com a chave de idempotência ainda está em andamento. | — |
| `outcome_unknown` | `502` ou `409` | Não dá para saber se a operação aconteceu. Confira antes de tentar de novo (veja [Idempotência](https://gaio.social/developers/index.md#idempotência)). | — |
| `service_unavailable` | `503` | A chave de idempotência não pôde ser conferida, ou o armazenamento de mídia ou o motor de automações não respondeu. Tente de novo. | — |

Outras respostas de erro:

- **`400`**: a chamada não segue o formato da rota, por exemplo com um campo desconhecido, um tipo errado, um parâmetro de consulta que a rota não aceita ou sem um cabeçalho obrigatório. Essa resposta não tem `context.code`: o que está errado vem em `context.details`. Um parâmetro de consulta desconhecido é recusado, e não ignorado, para que um filtro com erro de digitação não devolva a lista inteira como se estivesse filtrada.
- **`401`**, com o corpo `{"message": "Unauthorized"}`: a chave não foi enviada, não existe, foi revogada ou expirou.
- **`403`**, com o corpo `{"message": "Forbidden"}`: a conta está suspensa ou não tem acesso à API.

Essas respostas `401` e `403` vêm do gateway, antes de a chamada chegar à rota, e por isso têm um corpo próprio, sem `type` nem `context`. Um `403` com `context.code` é falta de escopo na chave; um `403` com o corpo `{"message": "Forbidden"}` é a conta sem acesso à API. Um `403` sem um corpo JSON vem da borda da API, que bloqueia um IP com má reputação ou com mais de 1200 chamadas por minuto, somadas todas as contas que chamam dele (veja [Limites de uso](https://gaio.social/developers/index.md#limites-de-uso)).

### Contatos

| Código | Status | Quando acontece | Campos extras em `context` |
| --- | --- | --- | --- |
| `invalid_phone` | `422` | Um telefone que nenhum contato pode ter. Um número de fora do Brasil precisa do `+`. | — |
| `invalid_name` | `422` | Um nome em branco: de contato ou de etiqueta. | — |
| `invalid_connection` | `422` | `connection_id` não é um número de WhatsApp conectado da conta. | — |
| `consent_required` | `422` | Criar um contato sem `consent: true`. | — |
| `tag_not_found` | `422` | Ids de etiqueta no corpo que não são etiquetas da conta. | `tag_ids` |
| `custom_field_not_found` | `422` | Ids de campo no corpo que não são campos da conta. | `field_ids` |
| `invalid_custom_field_value` | `422` | Valores que o campo recusa. Nenhum valor foi gravado. | `errors`, com `field_id` e `reason` (`expected_number`, `expected_string`, `invalid_number`, `invalid_option` ou `duplicate_field`) |
| `tag_already_exists` | `409` | A conta já tem uma etiqueta com esse nome, com quaisquer maiúsculas e minúsculas. | `tag_id` |
| `tag_in_use` | `409` | Uma automação da conta usa a etiqueta, que não pode ser renomeada nem excluída. | `workflow_ids` (até 50), `workflow_count` |

### Conversas e mensagens

| Código | Status | Quando acontece | Campos extras em `context` |
| --- | --- | --- | --- |
| `media_expired` | `410` | A mídia não está mais guardada. | — |
| `invalid_message` | `422` | A mensagem não serve para nenhum canal: vazia, um template com mais conteúdo, botões sem texto ou com anexo, uma URL que não é `https` ou dois botões de resposta com o mesmo `id`. | `reason` (`empty`, `template_with_content`, `buttons_without_text`, `buttons_with_attachment`, `invalid_url` ou `duplicate_button_id`), `field` |
| `content_not_supported` | `422` | O canal da conversa não leva a mensagem ou uma parte dela, como um template no Instagram, uma legenda num áudio ou, no WhatsApp, botões de link e de resposta juntos, dois botões de resposta com o mesmo título ou cards com botões diferentes. | `channel`, `field` |
| `text_too_long` | `422` | O texto é mais longo do que o canal da conversa entrega naquele lugar. | `channel`, `field`, `max_length` |
| `media_not_found` | `422` | O `media_id` não é uma mídia criada pela conta com `POST /v1/media`. | `media_id` |
| `media_not_uploaded` | `422` | Nada foi enviado para a `upload_url` da mídia. Se a URL expirou, crie outra mídia. | `media_id` |
| `media_not_supported` | `422` | O canal não aceita a mídia: o tipo, o formato ou o tamanho. | `channel`, `field`, `media_id`, `reason` (`type_mismatch`, `content_type` ou `too_large`), `content_type`, `size`, `max_size` |
| `template_not_found` | `422` | A conta não tem um template do WhatsApp com esse `name` e esse `language`. | `name`, `language` |
| `template_not_approved` | `422` | O WhatsApp não aprovou o template. | `status` |
| `template_not_supported` | `422` | O template precisa de parâmetros que a API não envia. | `reason` (`header_media`, `header_variables`, `button_variables`, `carousel`, `named_variables` ou `authentication`) |
| `template_variables_mismatch` | `422` | `body_variables` não tem um valor para cada `{{n}}` do corpo do template. | `expected`, `received` |
| `channel_disconnected` | `422` | A conexão da conversa está desconectada. | `channel`, e `reason: authorization_lost` quando é preciso reconectá-la |
| `messaging_window_closed` | `422` | O contato escreveu pela última vez há mais de 24 horas. | `channel`, `last_inbound_at` |
| `template_required` | `422` | O mesmo, no WhatsApp, onde só um template alcança o contato. | `channel`, `last_inbound_at` |
| `contact_blocked` | `422` | A conta bloqueou o contato das automações. | — |
| `conversation_not_owned` | `422` | Outro app controla a conversa do Instagram no momento. Ela volta sozinha. | — |
| `recipient_unavailable` | `422` | A conversa ou o contato não existe mais no canal. | — |
| `provider_rejected` | `422` | O canal recusou a mensagem de vez, por um motivo sem código próprio. | `channel`, `provider_message` (o texto do canal) |
| `provider_throttled` | `429` | O canal está segurando os envios da conta. Espere antes de tentar de novo. | `channel` |
| `outcome_unknown` | `502` | O canal não confirmou o envio, e a mensagem pode ter saído. | `provider_accepted` (`true` quando o canal aceitou) |
| `provider_unavailable` | `503` | O Instagram falhou sem enviar a mensagem. Tente de novo. | `channel` |
| `service_unavailable` | `503` | O armazenamento de mídia não respondeu ao ler a mídia do envio. Tente de novo. | — |

Toda recusa desta tabela, menos o `outcome_unknown`, libera a chave de idempotência.

### Automações

| Código | Status | Quando acontece | Campos extras em `context` |
| --- | --- | --- | --- |
| `invalid_graph` | `422` | O grafo não segue o contrato. | `errors`, cada um com `node_id` e `path` |
| `invalid_channel` | `422` | O canal não pode ser vinculado. | `reason` (`required`, `not_found` ou `type_mismatch`) |
| `invalid_trigger` | `422` | O gatilho não é coerente. | `reason` |
| `precondition_required` | `428` | Uma alteração enviada sem `If-Match`. | — |
| `version_conflict` | `412` | O `If-Match` não é a revisão atual. | `current_revision` |
| `workflow_published` | `409` | A alteração exige despublicar a automação antes, como trocar o canal. | — |
| `folder_not_found` | `422` | O `folder_id` não é uma pasta de automações da conta. | `field` (`folder_id`) |
| `workflow_invalid` | `422` | As verificações da publicação recusaram a automação. | `errors`, os mesmos da validação |
| `workflow_not_publishable` | `422` | O motor de automações recusou a automação compilada. | `details` |
| `workflow_changed` | `409` | Sem `If-Match`, a automação continuou mudando durante a publicação. Leia de novo e tente outra vez. | — |
| `workflow_not_published` | `409` | Uma execução pedida para uma automação que não está publicada. | — |
| `channel_disconnected` | `422` | O canal da automação não está conectado. | `connection_id` |
| `contact_not_on_channel` | `422` | O contato nunca chegou pelo canal da automação. | `connection_id` |
| `messaging_window_closed` | `422` | A automação envia mensagens, e a janela de 24 horas do contato nesse canal está fechada. | `last_inbound_at` |
| `contact_automations_paused` | `409` | A conta pausou as automações para o contato. | — |
| `integration_disconnected` | `422` | A automação usa uma integração que não está conectada. | `integration_type` |
| `run_in_progress` | `409` | O contato está no meio de uma execução da automação. | — |
| `run_limit_reached` | `429` | O contato atingiu o limite de execuções por hora da automação, o mesmo de um gatilho. | — |
| `outcome_unknown` | `502` | O motor não confirmou o início da execução, que pode ter acontecido. | — |

### Pastas e listas de respostas

| Código | Status | Quando acontece | Campos extras em `context` |
| --- | --- | --- | --- |
| `invalid_name` | `422` | Um nome de pasta ou de lista em branco. | — |
| `folder_not_found` | `422` | O `parent_id` não é uma pasta de automações da conta. | `field` (`parent_id`) |
| `invalid_parent` | `422` | Uma pasta não vai para dentro dela mesma nem de uma subpasta dela. | — |
| `replies_list_already_exists` | `409` | A conta já tem uma lista de respostas com esse nome, com quaisquer maiúsculas e minúsculas. | `replies_list_id` |
| `replies_list_in_use` | `409` | Uma automação da conta usa a lista de respostas, que não pode ser excluída. | `workflow_ids` (até 50), `workflow_count` |
| `invalid_reply` | `422` | Uma resposta em branco na lista. | `index` |
| `service_unavailable` | `503` | O motor de automações não respondeu ao salvar ou excluir uma lista de respostas, e nada mudou. Tente de novo. | — |

### Webhooks

Os códigos próprios das rotas de webhooks estão em [Webhooks › Erros](https://gaio.social/developers/webhooks.md#erros).

## Versionamento

A versão principal fica no caminho da URL: `/v1`. Dentro de uma versão, o contrato só cresce.

Pode chegar a qualquer momento dentro da `/v1`:

- rotas novas;
- campos novos nas respostas;
- campos opcionais novos nas requisições;
- valores novos em enumerações, como um canal ou um status novo;
- códigos de erro novos.

Por isso, o seu código deve ignorar os campos que não conhece e tratar valores de enumeração desconhecidos sem quebrar.

Nunca acontece dentro da `/v1`, só em uma versão nova, como a `/v2`:

- remover ou renomear uma rota, um campo, um valor de enumeração ou um código de erro;
- mudar o tipo, o formato ou o valor padrão de um campo;
- mudar a ordenação ou a paginação de uma lista;
- tornar obrigatório um campo opcional da requisição, ou validar um campo existente com mais rigor;
- mudar o status ou o código de um erro que já existe.

O catálogo de blocos das automações evolui fora da versão do caminho. Ele cresce por adição: um tipo de bloco, um campo opcional, um valor. Uma mudança que recusaria um grafo aceito hoje é anunciada com 90 dias de antecedência, e nesse prazo a validação avisa sobre os grafos afetados.

Quando uma rota for aposentada:

- ela passa a responder com os cabeçalhos `Deprecation` e `Sunset`, este com a data em que ela deixa de funcionar;
- a mudança é anunciada nesta documentação;
- o dono de cada chave que usou a rota nos últimos 30 dias recebe um e-mail.

Quando sair uma versão principal nova, a anterior continua no ar por 6 meses.

## Referência da API

Todas as rotas da API, com parâmetros, respostas e autenticação, estão no [documento OpenAPI](https://gaio.social/developers/openapi.yml), também aberto na [referência](https://gaio.social/developers/referencia), que precisa de JavaScript. Como montar o corpo de uma automação está em [Montar automações pela API](https://gaio.social/developers/automacoes.md), e como receber os eventos da conta, em [Webhooks](https://gaio.social/developers/webhooks.md).

## Para agentes de código

Se um agente de código, como Claude Code, Codex, Cursor ou GitHub Copilot, vai escrever a sua integração, aponte-o para a documentação em Markdown, feita para ele ler:

- [`llms.txt`](https://gaio.social/developers/llms.txt): o índice, com as regras principais e os links para o resto.
- [`index.md`](https://gaio.social/developers/index.md): esta página.
- [`automacoes.md`](https://gaio.social/developers/automacoes.md): como montar automações, com as pastas e as listas de respostas.
- [`webhooks.md`](https://gaio.social/developers/webhooks.md): como receber os eventos da conta e conferir a assinatura.
- [`openapi.yml`](https://gaio.social/developers/openapi.yml): todas as rotas.
- [`workflow-graph-schema.json`](https://gaio.social/developers/workflow-graph-schema.json): o catálogo de blocos das automações, em JSON Schema.

Para começar, peça ao agente: "Leia https://gaio.social/developers/llms.txt antes de escrever código para a API da Gaio."

Nunca cole a chave no chat do agente nem num arquivo do repositório. Guarde-a na variável de ambiente `GAIO_API_KEY`, onde o código vai ler.

### O trecho para o AGENTS.md

O agente lê as instruções do repositório a cada tarefa. Com o trecho abaixo no `AGENTS.md` do repositório que chama a API, ele segue as regras da API sem que você precise lembrá-lo: usar a chave de idempotência, validar antes de publicar, conferir a assinatura dos webhooks, não enviar mensagens de verdade para testar.

| Ferramenta | Onde pôr o trecho |
| --- | --- |
| Codex, Cursor e GitHub Copilot | No `AGENTS.md` da raiz do repositório. |
| Claude Code | No `AGENTS.md`, com uma linha `@AGENTS.md` no `CLAUDE.md`, que importa o arquivo. |

O mesmo trecho está em [`AGENTS.md`](https://gaio.social/developers/AGENTS.md). As linhas `BEGIN` e `END` marcam onde ele começa e termina, para que uma atualização troque só ele.

```markdown
<!-- BEGIN:gaio-api-agent-rules -->
## API da Gaio

Este projeto chama a API pública da Gaio, em beta fechado, que não está nos seus dados de treino. Antes de escrever ou mudar código que a usa, leia https://gaio.social/developers/llms.txt e a página da tarefa. As rotas e os corpos estão em https://gaio.social/developers/openapi.yml: uma rota que não está lá não existe.

Base `https://api.gaio.social/v1`, JSON em snake_case, `Authorization: Bearer $GAIO_API_KEY`, só de servidor para servidor (sem CORS).

### Chave
- Leia a chave só da variável de ambiente `GAIO_API_KEY`, no servidor. Nunca a ponha em código, repositório, URL, código de navegador ou de app, log ou mensagem de erro, e mascare o cabeçalho `Authorization` nos logs de HTTP.
- Não peça a chave no chat: peça para exportarem `GAIO_API_KEY`.
- `GET /v1/me` mostra os escopos e a validade (`expires_at`) da chave.

### Efeitos reais
- Não há modo de teste. Enviar mensagem (`POST /v1/conversations/{id}/messages`), iniciar uma automação (`POST /v1/workflows/{id}/runs`), publicar, despublicar e excluir atingem pessoas e a conta de verdade. Não chame essas rotas para explorar nem em testes automatizados (simule a API), e peça confirmação a uma pessoa antes.
- Contatos e mensagens são dados pessoais: não os copie para logs, fixtures ou prompts.
- Envie `consent: true` em `POST /v1/contacts` só com o opt-in daquela pessoa registrado. Nunca fixe esse valor no código.

### Envios
- Sempre com `Idempotency-Key`: derivada do evento de negócio (`pedido-enviado:<id>`) ou um UUID guardado antes da primeira tentativa. Repita a mesma chave nas novas tentativas da mesma operação e nunca a gere dentro do laço de novas tentativas.
- Confira `reply_window.state` da conversa antes de enviar. `messaging_window_closed` e `template_required` não se resolvem repetindo.
- `outcome_unknown` (502, ou 409 com a mesma chave): o efeito pode ter acontecido, e repetir com a mesma chave devolve o mesmo erro. Não reenvie sozinho: confira as mensagens da conversa ou as execuções da automação e, na dúvida, deixe para uma pessoa.
- `idempotency_in_progress` (409) e `service_unavailable` (503): espere e repita com a mesma chave. `idempotency_key_reused` (422) é erro na geração da chave: não repita.

### Automações
- Monte o corpo pelo guia https://gaio.social/developers/automacoes.md, partindo dos exemplos dele, e pelo catálogo atual (`GET /v1/workflow-graph-schema`).
- Valide antes de salvar e de publicar: `POST /v1/workflows/validate` (só `channel`, `trigger` e `graph`; com `title` ou `description`, a resposta é 400) e `POST /v1/workflows/{id}/validate` (o rascunho salvo). Os `errors` impedem a publicação; leia também os `warnings`.
- No `PATCH`, envie o `ETag` da leitura em `If-Match` (sem ele, 428). `graph` e `trigger` substituem o valor inteiro. Em `412 version_conflict`, leia de novo e reaplique a mudança; nunca sobrescreva às cegas.
- Publique com `If-Match` igual ao `ETag` da validação do rascunho.
- Devolva como vieram os tipos de bloco e os campos que você não conhece. Mantenha o `id` dos botões e a ordem das opções de um `randomizer` que já rodou.

### Webhooks
- Monte o receptor pelo guia https://gaio.social/developers/webhooks.md.
- Confira o `Gaio-Signature` sobre o corpo cru, antes de interpretar o JSON: HMAC-SHA256 de `<t>.<corpo>` com o segredo inteiro (`whsec_…`), comparado em tempo constante com cada `v1`, e recuse um `t` a mais de 5 minutos. Leia o segredo de uma variável de ambiente, como a chave.
- Responda `2xx` em até 10 segundos e processe depois, por uma fila. Descarte os eventos repetidos pelo `id` e não conte com a ordem: quando ela importa, leia o recurso de novo pela API.
- Cadastrar um endpoint ou mudar a URL dele passa a enviar os dados da conta para aquele endereço: peça confirmação a uma pessoa antes.

### Erros e limites
- Decida pelo `context.code`, nunca pela `message` nem só pelo status. `401` e `403` sem `context` vêm do gateway: chave inválida ou conta sem acesso.
- Limites por conta, por minuto: leitura 120, escrita 60, envio 20. No `429`, espere o `Retry-After`; sem ele, use backoff exponencial com jitter.
- Listas usam cursor opaco e `limit` até 100, menos `GET /v1/webhooks`, que vem numa página só e não aceita `limit` nem `cursor`. Um parâmetro de consulta desconhecido dá `400`.
- A `/v1` só cresce: tolere campos, valores de enumeração e códigos de erro novos.

Para atualizar, troque o que está entre os marcadores pelo conteúdo de https://gaio.social/developers/AGENTS.md.
<!-- END:gaio-api-agent-rules -->
```
