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 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:
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:
{
"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:
- Crie uma chave nova, com as mesmas permissões.
- Passe o seu sistema para a chave nova e confira que as chamadas funcionam. A rota
GET /v1/me serve para isso. - 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 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).
Convenções
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.
curl -G https://api.gaio.social/v1/contacts \
-H "Authorization: Bearer $GAIO_API_KEY" \
-d limit=100
{
"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:
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.
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 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 @.
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:
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:
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:
{
"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). 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). 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:
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 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:
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 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.
Mídia. Para enviar um arquivo, crie a mídia com POST /v1/media, informando o tipo do arquivo:
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:
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). 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.
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:
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).
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:
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:
{
"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 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).
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 e 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:
{
"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). | — |
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).
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.
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.