# API do FORSI Chat (v1): documentação completa para IA > API REST do FORSI Chat (WhatsApp, atendimento, funil, agenda, equipe e relatórios). Versão 1.0.0. Gerado do contrato OpenAPI em cada requisição: está sempre igual à API no ar. - Base: `https://chat.forsi.com.br/api/v1` - Autenticação: `Authorization: Bearer ` (chave `fcv2_live_...`, criada em Configurações → API e webhooks; nos exemplos, `fcv2_live_EXAMPLE`) - Documentação completa para IA: https://docs.forsi.com.br/llms-full.txt (a mesma em https://chat.forsi.com.br/api/docs/llm.md) - Resumo para IA: https://chat.forsi.com.br/api/docs/llm-resumo.md - Índice (llms.txt): https://docs.forsi.com.br/llms.txt - OpenAPI 3.1: https://docs.forsi.com.br/openapi.json - Páginas para pessoas: https://docs.forsi.com.br (cada página tem a versão .md: https://docs.forsi.com.br/comecando.md) ## Para a IA que vai construir a integração - A chave é segredo de servidor: leia de variável de ambiente (`FORSI_CHAVE`), nunca coloque no navegador nem no código. Chamadas com cabeçalho `Origin` são recusadas. - Trate erros pelo `erro.codigo` (estável), não pela mensagem. Repita só 429 (respeite `Retry-After`) e 5xx, com espera crescente. - Em `POST /mensagens` e nas criações, mande `Idempotency-Key` única por operação e reutilize a mesma ao repetir. - Listas: siga `proximo` até vir `null`. - Para eventos em tempo real use webhooks e confira a assinatura `X-Forsi-Signature` (HMAC-SHA256 de `.`, chave = segredo inteiro com `whsec_`). - Datas em ISO 8601 com fuso; dinheiro em centavos; ids UUID. Ignore campos que não conhece. - Não invente endpoints, campos ou códigos: use só o que está neste arquivo. ## Sumário - Começando: Primeira chamada em 3 passos. - Autenticação e chaves: Bearer, escopos, validade, IPs e troca de chave. - `GET /me` Quem sou eu - Limites de uso: Requisições por minuto, tamanho e cabeçalhos de limite. - Erros: Formato único, status HTTP e todos os códigos. - Paginação: Listas com cursor. - Idempotência: Repetir com segurança com Idempotency-Key. - Webhooks de saída e assinatura: Eventos, tentativas, assinatura HMAC e gestão dos webhooks. - `GET /webhooks` Listar webhooks - `POST /webhooks` Criar webhook - `GET /webhooks/{id}` Ver um webhook - `PATCH /webhooks/{id}` Atualizar webhook - `DELETE /webhooks/{id}` Apagar webhook - `POST /webhooks/{id}/rotacionar` Trocar o segredo - `POST /webhooks/{id}/testar` Mandar um evento de teste - `GET /webhooks/{id}/entregas` Histórico de entregas - `POST /webhooks/entregas/{id}/reenviar` Reenviar uma entrega - Mensagens: Enviar texto, mídia e modelos; ver status. - `POST /mensagens` Enviar mensagem - `GET /mensagens` Listar mensagens - `GET /mensagens/{id}` Ver uma mensagem - `GET /mensagens/{id}/midia` Baixar mídia de uma mensagem - Conversas: Listar conversas e ler mensagens. - `GET /conversas` Listar conversas - `GET /conversas/{id}` Ver uma conversa - `GET /conversas/{id}/mensagens` Mensagens de uma conversa - Contatos: Criar, buscar, editar e apagar contatos. - `GET /contatos` Listar e buscar contatos - `POST /contatos` Criar contato - `GET /contatos/{id}` Ver um contato - `PATCH /contatos/{id}` Atualizar contato - `DELETE /contatos/{id}` Apagar contato - Modelos: Modelos aprovados do WhatsApp. - `GET /modelos` Listar modelos do WhatsApp - Funil e negócios: Funis, etapas, negócios e notas. - `GET /funis` Listar funis - `POST /funis` Criar funil - `GET /funis/{id}` Ver um funil - `PATCH /funis/{id}` Renomear funil - `DELETE /funis/{id}` Apagar funil - `POST /etapas` Criar etapa - `GET /etapas/{id}` Ver uma etapa - `PATCH /etapas/{id}` Atualizar etapa - `DELETE /etapas/{id}` Apagar etapa - `GET /negocios` Listar negócios - `POST /negocios` Criar negócio - `GET /negocios/{id}` Ver um negócio - `PATCH /negocios/{id}` Atualizar negócio - `DELETE /negocios/{id}` Apagar negócio - `POST /negocios/{id}/mover` Mover para outra etapa - `POST /negocios/{id}/ganhar` Marcar como ganho - `POST /negocios/{id}/perder` Marcar como perdido - `POST /negocios/{id}/reabrir` Reabrir negócio - `GET /negocios/{id}/notas` Notas de um negócio - `POST /negocios/{id}/notas` Anotar no negócio - `DELETE /negocios/notas/{id}` Apagar uma nota - Agenda: Agendas, horários livres, compromissos e bloqueios. - `GET /agendas` Listar agendas - `POST /agendas` Criar agenda - `GET /agendas/{id}` Ver uma agenda - `PATCH /agendas/{id}` Atualizar agenda - `GET /disponibilidade` Horários livres - `GET /compromissos` Listar compromissos - `POST /compromissos` Marcar compromisso - `GET /compromissos/{id}` Ver um compromisso - `PATCH /compromissos/{id}` Atualizar compromisso - `POST /compromissos/{id}/remarcar` Remarcar compromisso - `POST /compromissos/{id}/cancelar` Cancelar compromisso - `POST /compromissos/{id}/confirmar` Confirmar compromisso - `GET /bloqueios` Listar bloqueios - `POST /bloqueios` Criar bloqueio - `GET /bloqueios/{id}` Ver um bloqueio - `PATCH /bloqueios/{id}` Atualizar bloqueio - `DELETE /bloqueios/{id}` Apagar bloqueio - Equipe e permissões: Pessoas, convites, equipes e atribuição de conversas. - `GET /equipe/membros` Listar pessoas da equipe - `GET /equipe/membros/{id}` Ver uma pessoa - `PATCH /equipe/membros/{id}` Mudar permissões de um atendente - `DELETE /equipe/membros/{id}` Tirar um atendente da equipe - `GET /equipe/presenca` Quem está disponível - `GET /equipe/convites` Listar convites abertos - `POST /equipe/convites` Convidar atendente - `DELETE /equipe/convites/{id}` Cancelar convite - `POST /equipe/convites/{id}/reenviar` Reenviar convite - `GET /equipe/equipes` Listar equipes (setores) - `POST /equipe/equipes` Criar equipe - `GET /equipe/equipes/{id}` Ver uma equipe - `PATCH /equipe/equipes/{id}` Atualizar equipe - `DELETE /equipe/equipes/{id}` Apagar equipe - `PATCH /equipe/equipes/{id}/membros` Pôr e tirar pessoas da equipe - `POST /conversas/{id}/atribuir` Atribuir ou transferir conversa - Automações do Instagram: Regras de comentário: palavra-chave, resposta pública e mensagem direta. - `GET /instagram/contas` Listar contas do Instagram - `GET /automacoes` Listar automações para ligar a uma regra - `GET /regras-comentario` Listar regras de comentário - `POST /regras-comentario` Criar regra de comentário - `GET /regras-comentario/{id}` Ver uma regra de comentário - `PATCH /regras-comentario/{id}` Editar regra de comentário - `DELETE /regras-comentario/{id}` Apagar regra de comentário - Seguir para receber: Entregue um conteúdo no direct só para quem segue o seu perfil do Instagram. - Relatórios: Volume, tempo de resposta, equipe, caixas, IA e créditos. - `GET /relatorios/volume` Volume de conversas e mensagens - `GET /relatorios/tempo-de-resposta` Tempo de resposta e de resolução - `GET /relatorios/atendentes` Desempenho por atendente - `GET /relatorios/caixas` Desempenho por caixa (canal) - `GET /relatorios/agente-de-ia` Atendimento do agente de IA - `GET /relatorios/creditos` Créditos de IA usados - Escopos e planos: O que cada escopo libera e o que o plano precisa ter. - Boas práticas de segurança: Como guardar chaves e receber webhooks com segurança. - Changelog: O que mudou em cada versão. ## Começando A API do FORSI Chat é uma API REST, em JSON e só por https. Com ela o seu sistema faz pelo código o que a sua equipe faz na tela: manda mensagens de WhatsApp, lê conversas e contatos, mexe no funil, na agenda e na equipe, puxa relatórios e recebe avisos por webhook assinado. Endereço base: `https://chat.forsi.com.br/api/v1` ### Primeira chamada em 3 passos 1. Um dono ou gestor cria uma chave em **Configurações → API e webhooks → Nova chave** (planos com o módulo API). A chave aparece uma vez só. 2. Guarde a chave numa variável de ambiente do seu servidor. Ela é secreta: nunca coloque no site, no app ou no navegador. 3. Confira a chave com [`GET /me`](https://docs.forsi.com.br/api/conta/me) e mande a primeira mensagem: ```bash title="terminal" export FORSI_CHAVE="fcv2_live_EXAMPLE" # troque pela sua chave curl "https://chat.forsi.com.br/api/v1/me" -H "Authorization: Bearer $FORSI_CHAVE" curl -X POST "https://chat.forsi.com.br/api/v1/mensagens" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "telefone": "+55 31 98888-7777", "tipo": "texto", "texto": "Olá! Sua consulta está confirmada para amanhã às 9h." }' ``` ### O que acontece depois do envio > **Figura: O caminho de uma mensagem enviada.** O seu sistema chama POST /mensagens. A API responde 202 na hora e põe a mensagem na fila (status pendente). A FORSI entrega à Meta (enviada); a Meta avisa quando chega ao aparelho (entregue) e quando a pessoa lê (lida). Se algo der errado, o status vira falhou, com o motivo. Cada mudança chega ao seu webhook como o evento mensagem.status; também dá para consultar com GET /mensagens/{id}. A resposta `202` quer dizer que a mensagem entrou na fila, não que chegou. Para acompanhar o resto do caminho, cadastre um [webhook](https://docs.forsi.com.br/webhooks) com o evento `mensagem.status` ou consulte [`GET /mensagens/{id}`](https://docs.forsi.com.br/api/mensagens/ver-mensagem). Se a rede cair no meio do envio, repita com a mesma `Idempotency-Key`: a mensagem não sai duas vezes (veja [Idempotência](https://docs.forsi.com.br/idempotencia)). ### Convenções - Toda requisição leva `Authorization: Bearer `. Corpo em JSON com `Content-Type: application/json`. - Resposta de sucesso: `{"dados": ...}`. Listas: `{"dados": [...], "proximo": "..."}` (veja [Paginação](https://docs.forsi.com.br/paginacao)). - Resposta de erro: `{"erro": {"codigo": "...", "mensagem": "...", "requisicao_id": "..."}}` (veja [Erros](https://docs.forsi.com.br/erros)). - Ids são UUID. Datas e horas são ISO 8601 em UTC (`2026-10-04T13:05:12.481Z`). Dinheiro vem em centavos (`valor_centavos`). - Campos novos podem aparecer nas respostas a qualquer momento: o seu código deve ignorar o que não conhece. ### Vai usar uma IA para construir a integração? Mande para ela o link [https://docs.forsi.com.br/llms-full.txt](https://docs.forsi.com.br/llms-full.txt): é esta documentação inteira num arquivo só, com todos os endpoints, exemplos e regras. Para um índice curto, use [https://docs.forsi.com.br/llms.txt](https://docs.forsi.com.br/llms.txt). Cada página também tem uma versão em Markdown: é só pôr `.md` no fim do endereço (por exemplo, [https://docs.forsi.com.br/api/mensagens/enviar-mensagem.md](https://docs.forsi.com.br/api/mensagens/enviar-mensagem.md)). O contrato formal está em [https://docs.forsi.com.br/openapi.json](https://docs.forsi.com.br/openapi.json) (OpenAPI 3.1). ### Perguntas frequentes #### Qual plano preciso para usar a API? Um plano com o módulo API. Sem ele, toda chamada responde `403 modulo_fora_do_plano`. Funil e agenda pedem também os módulos Funil e Agenda. #### Posso chamar a API direto do navegador ou do app de celular? Não. A chave é segredo de servidor, e chamadas com cabeçalho `Origin` são recusadas com `403 origem_nao_permitida`. O navegador fala com o seu servidor; o seu servidor fala com a API. #### Recebi 202 no envio. A mensagem foi entregue? Ainda não. `202` quer dizer que a mensagem entrou na fila. O status passa para `enviada`, `entregue` e `lida` (ou `falhou`, com o motivo), e cada mudança chega pelo webhook `mensagem.status`. #### Como mando mensagem para quem nunca falou comigo? Com um modelo aprovado pela Meta (`tipo: "modelo"`). Na API oficial do WhatsApp, texto livre só sai dentro da janela de 24 horas desde a última mensagem do contato; fora dela, a resposta é `422 janela_fechada`. Veja os seus modelos em [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos). ## Autenticação e chaves A API reconhece quem chama por uma chave secreta, enviada em toda requisição no cabeçalho `Authorization: Bearer `. Cada chave pertence a uma organização e só faz o que os seus escopos permitem. ```bash title="terminal" curl "https://chat.forsi.com.br/api/v1/me" -H "Authorization: Bearer $FORSI_CHAVE" ``` A chave tem o formato `fcv2_live__` (nos exemplos, `fcv2_live_EXAMPLE`). Guardamos só um resumo (sha256) e não temos como mostrar a chave de novo: perdeu, crie outra. ### Criar uma chave 1. Entre como dono ou gestor em **Configurações → API e webhooks** e toque em **Nova chave**. 2. Dê um nome que diga qual integração vai usar a chave (`ERP`, `site`, `n8n`) e marque só os escopos de que ela precisa. 3. Se quiser, defina a validade e a lista de IPs que podem usar a chave. 4. Copie a chave na hora e guarde no cofre de segredos ou numa variável de ambiente do servidor. Ela não aparece de novo. ### O que a API confere em cada chamada > **Figura: O que a API confere em cada chamada.** O seu servidor manda Authorization: Bearer fcv2_live_... . Primeiro a API exige https e recusa chamadas com cabeçalho Origin, vindas de navegador (403). Depois confere a chave: existe, não foi revogada, não venceu e o IP está na lista (senão, 401 nao_autorizado, sempre igual). Em seguida o plano e a assinatura (403 modulo_fora_do_plano ou assinatura_bloqueada), o escopo da rota (403 escopo_insuficiente) e o limite por minuto (429 com Retry-After). Passou em tudo: a resposta sai com 2xx e {"dados": ...}. A conferência segue esta ordem, e a primeira que falhar responde: | Ordem | O que confere | Se falhar | | --- | --- | --- | | 1 | Conexão https | `403 https_obrigatorio` | | 2 | Sem cabeçalho `Origin` (a chamada não vem de navegador) | `403 origem_nao_permitida` | | 3 | A chave existe, não foi revogada, não venceu e o IP está na lista | `401 nao_autorizado`, sempre igual, sem dizer o motivo | | 4 | Plano com o módulo API e assinatura em dia | `403 modulo_fora_do_plano` ou `403 assinatura_bloqueada` | | 5 | Escopo da rota | `403 escopo_insuficiente` (o escopo que faltou vem em `detalhe`) | | 6 | Limite por minuto | `429 limite_excedido`, com `Retry-After` | Muitas tentativas com chave errada do mesmo endereço bloqueiam esse endereço por alguns minutos: `429 muitas_falhas`, com `Retry-After`. ### Escopos, validade e IPs - Cada chave tem **escopos** (o que pode fazer; veja [Escopos e planos](https://docs.forsi.com.br/escopos)). Peça só os que a integração usa. - A chave pode ter **validade** (vence sozinha) e uma **lista de IPs** permitidos. - Revogar corta na hora. ### Trocar a chave sem parar o sistema Use **Trocar a chave**: sai uma chave nova e a antiga continua valendo pelo tempo que você escolher (até 7 dias). Coloque a nova no servidor, confira que as chamadas passam e deixe a antiga vencer. ### Quem sou eu [`GET /me`](https://docs.forsi.com.br/api/conta/me) aceita qualquer chave válida e mostra a organização, os escopos e a validade da chave, os limites da organização e as caixas (WhatsApps) com o estado da conexão. É a melhor primeira chamada para conferir a configuração. ### Perguntas frequentes #### Perdi a chave. Dá para ver de novo? Não. Guardamos só um resumo (sha256), que não permite recuperar o texto da chave. Crie outra e revogue a perdida. #### Por que o 401 não diz o motivo? De propósito. Chave ausente, errada, revogada, vencida ou fora da lista de IPs recebem a mesma resposta, para não ajudar quem tenta adivinhar chaves. Confira no app se a chave está ativa, se não venceu e se o IP do seu servidor está na lista. #### Posso usar a mesma chave em duas integrações? Pode, mas não é bom. Com uma chave por integração dá para revogar uma sem parar a outra, e a auditoria mostra o nome da chave que fez cada mudança. #### A chave vale para outra organização? Não. Cada chave pertence a uma organização. Ids de outra organização respondem `404`, como se não existissem. **Códigos de erro comuns nesta seção**: `nao_autorizado`, `muitas_falhas`. ### GET /me — Quem sou eu - URL: `GET https://chat.forsi.com.br/api/v1/me` - Acesso: qualquer chave válida · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Eu` - Página: https://docs.forsi.com.br/api/conta/me (Markdown: https://docs.forsi.com.br/api/conta/me.md) A organização, a chave (escopos, validade), os limites e as caixas com o estado das conexões. **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/me" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "organizacao": {"id":"dddecb67-6b55-43da-94f6-18c8d6680571","nome":"Maria Souza"}, "chave": { "id": "71c5c606-a9d7-4270-8891-45d783aa9b71", "nome": "Maria Souza", "prefixo": "fcv2_live_EXAMPLE", "escopos": ["texto"], "expira_em": "2026-10-04T13:05:12.481Z" }, "limites": {"por_minuto":300,"org_por_minuto":600,"envios_por_minuto":60,"envios_org_por_minuto":120}, "trava_spam_meta": {"ligada":false,"horas":12}, "caixas": [ { "id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "nome": "Maria Souza", "canal": "whatsapp", "telefone": "+5531988887777", "conexoes": [ { "tipo": "texto", "estado": "texto", "sem_acesso_meta": {"desde":"2026-10-04T13:05:12.481Z","ultima_leitura_ok":"2026-10-04T13:05:12.481Z"}, "restricao_meta": {} } ] } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Limites de uso Janelas de 1 minuto, contadas por chave, por organização e por IP. Toda resposta traz `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset` (segundos até a janela reiniciar). Ao passar do limite, a resposta é `429 limite_excedido` com `Retry-After` em segundos. | O quê | Limite | | --- | --- | | Por chave | 300 requisições/min | | Por organização (todas as chaves) | 600 requisições/min | | Por IP de origem | 600 requisições/min | | Envio de mensagem (`POST /mensagens`), por chave | 60/min | | Envio de mensagem, por organização | 120/min | | Escritas em funil, agenda e equipe, por chave | 120/min | | Escritas em funil, agenda e equipe, por organização | 240/min | | Relatórios, por chave | 12/min (`429 limite_relatorios`) | | Relatórios, por organização | 30/min | | Corpo da requisição | até 64 KB (`413 corpo_grande`) | | Itens por página | 1 a 100 (padrão 50) | | Webhooks por organização | 10 | Os números valem no plano atual e podem subir; o [`GET /me`](https://docs.forsi.com.br/api/conta/me) mostra os da sua organização em `limites`. ### Como tratar o 429 Espere os segundos de `Retry-After` e tente de novo. Para 5xx, tente de novo com espera crescente. Não repita 4xx (exceto 429): a mesma chamada vai falhar igual. ```js title="JavaScript" // Repete só o que vale repetir: 429 (limite) e 5xx (nosso lado). // No POST /mensagens, repita sempre com a mesma Idempotency-Key. async function chamar(url, opcoes, tentativas = 5) { for (let i = 0; ; i++) { const resposta = await fetch(url, opcoes); const repetir = resposta.status === 429 || resposta.status >= 500; if (!repetir || i >= tentativas) return resposta; const espera = Number(resposta.headers.get('Retry-After')) || 2 ** i; await new Promise((r) => setTimeout(r, espera * 1000)); } } ``` ## Erros Todo erro da API tem o mesmo formato JSON, com um `codigo` fixo que o seu código pode testar. Use o `codigo` (ele não muda); a `mensagem` é só ajuda e pode mudar. `detalhe` diz o campo inválido, o escopo que faltou etc. Mande o `requisicao_id` (também no cabeçalho `X-Request-Id`) ao suporte. ### Como tratar - **4xx (menos 429)**: o pedido está errado. Corrija antes de repetir; a mesma chamada vai falhar igual. - **429**: espere os segundos de `Retry-After` e repita (veja [Limites de uso](https://docs.forsi.com.br/limites)). - **5xx**: repita com espera crescente. No envio de mensagem, com a mesma `Idempotency-Key`. ```json title="resposta 422" { "erro": { "codigo": "janela_fechada", "mensagem": "A janela de 24 horas desta conversa está fechada. Na API oficial do WhatsApp, só um modelo aprovado reabre a conversa.", "requisicao_id": "5d2f0c3e-8b1a-4c6e-9f7d-1a2b3c4d5e6f" } } ``` ### Conta restrita pela Meta (`conta_restrita`) Enquanto a Meta restringe o número por spam e a trava de spam da organização está ligada (Configurações > API), o envio barrado volta `422 conta_restrita` na hora, sem enviar nada. O `erro` traz os campos do objeto `RestricaoMeta` (os mesmos de `caixas[].conexoes[].restricao_meta` no [`GET /me`](https://docs.forsi.com.br/api/conta/me)): `ate` e `desde` (ISO-8601 UTC), `ate_fonte` (`meta` ou `padrao`), `escopo` (`iniciadas` = só modelos; `tudo` = nada sai), `motivo` (`meta_131031`, `meta_368`, `meta_131048`, `account_restriction`, `account_disabled`, `outro`), `motivo_texto`, `caixa_id` e `retry_after_s`. O cabeçalho `Retry-After` repete `retry_after_s`: mostre `ate` ao usuário e tente de novo depois desse tempo. Com a trava desligada, nada é barrado e a restrição só aparece no `GET /me`. ```json title="resposta 422 conta_restrita" { "erro": { "codigo": "conta_restrita", "mensagem": "A Meta restringiu esta conta por spam até 25/10/2026 às 11:00 (horário de Brasília): mensagens que abrem conversa (modelos) estão barradas; respostas dentro da janela de 24 h saem.", "ate": "2026-10-25T14:00:00Z", "desde": "2026-10-24T14:00:00Z", "ate_fonte": "padrao", "escopo": "iniciadas", "motivo": "meta_131031", "motivo_texto": "A Meta travou a conta do WhatsApp (131031).", "caixa_id": "0b6f5a52-3c1d-4e8f-9a7b-2c4d6e8f0a1b", "retry_after_s": 86400, "api_recusa": true, "requisicao_id": "5d2f0c3e-8b1a-4c6e-9f7d-1a2b3c4d5e6f" } } ``` ### Status HTTP | Status | Quando | | --- | --- | | 400 | JSON inválido ou cabeçalho mal formado. | | 401 | Chave ausente, inválida, revogada, vencida ou fora dos IPs permitidos. A resposta é sempre a mesma, de propósito. | | 403 | Sem permissão: escopo que falta, plano sem API (ou sem o módulo), assinatura suspensa, chamada pelo navegador ou sem https. | | 404 | Não existe, ou é de outra organização (para a API, é a mesma coisa). | | 409 | Conflito: registro repetido, horário ocupado, `Idempotency-Key` reaproveitada com outro corpo. | | 413 | Corpo acima de 64 KB. | | 415 | Corpo que não é JSON. | | 422 | Dados inválidos ou regra de negócio (ex.: janela de 24 h fechada). Veja `codigo` e `detalhe`. | | 429 | Passou do limite. Espere os segundos de `Retry-After`. | | 500 / 503 | Erro do nosso lado ou relatório que demorou demais. Pode tentar de novo (no envio, com a mesma `Idempotency-Key`). | ### Perguntas frequentes #### O que mando ao suporte? O `requisicao_id` da resposta (também no cabeçalho `X-Request-Id`), a hora e a rota chamada. Nunca mande a chave. #### Por que recebo 404 num id que existe? Porque ele é de outra organização ou foi apagado. A chave enxerga só a organização dela e, para a API, as duas coisas são iguais. #### A mensagem do erro pode mudar? Pode. A `mensagem` é para pessoas e pode mudar de texto. O `codigo` não muda: é ele que o seu código deve testar. ### Todos os códigos | codigo | O que significa | | --- | --- | | `nao_autorizado` | Chave de API ausente, inválida, revogada, vencida ou fora dos IPs permitidos. Mande "Authorization: Bearer fcv2_live_...". | | `modulo_fora_do_plano` | A API não está liberada para esta organização: o plano não inclui a API ou a assinatura não está ativa (teste vencido, conta em modo leitura). Fale com um dono. | | `escopo_insuficiente` | Esta chave não tem permissão para esta operação. | | `limite_excedido` | Muitas requisições. Espere o tempo de Retry-After e tente de novo. | | `muitas_falhas` | Muitas tentativas com chave errada deste endereço. Espere o tempo de Retry-After. | | `operacao_invalida` | Rota não encontrada. | | `recurso_inexistente` | Não encontrado. | | `metodo_nao_permitido` | Método não permitido nesta rota. | | `https_obrigatorio` | Use https. | | `origem_nao_permitida` | A chave de API é secreta: chame a API do seu servidor, nunca do navegador. | | `corpo_grande` | Corpo grande demais (máximo de 64 KB). | | `tipo_de_conteudo` | Mande o corpo como JSON (Content-Type: application/json). | | `json_invalido` | O corpo não é um JSON válido. | | `dados_invalidos` | Dados inválidos. Confira o campo indicado em "detalhe". | | `idempotencia_invalida` | Idempotency-Key deve ter de 1 a 200 caracteres visíveis, sem espaço. | | `idempotencia_obrigatoria` | Envie Idempotency-Key ou id_cliente para identificar a mensagem. | | `idempotencia_em_andamento` | Este envio ainda está em andamento. Repita a consulta em instantes. | | `idempotencia_conflito` | Esta Idempotency-Key já foi usada com outro corpo. Use uma chave nova para um pedido novo. | | `conflito` | Conflito com um registro que já existe. | | `erro_interno` | Erro do nosso lado. Já ficou registrado; tente de novo em instantes. | | `indisponivel` | Serviço indisponível no momento. Tente de novo em instantes. | | `cursor_invalido` | Cursor inválido. Use o valor de "proximo" da página anterior. | | `tipo_invalido` | tipo deve ser texto, imagem, video, audio, documento ou modelo. | | `texto_vazio` | Mensagem de texto precisa de texto. | | `texto_longo` | Texto longo demais (máximo de 4096 caracteres). | | `midia_invalida` | Link de mídia inválido: use https com um endereço público. | | `midia_indisponivel` | O arquivo desta mídia está indisponível. | | `midia_baixando` | O download desta mídia está na fila. Tente novamente após Retry-After. | | `janela_fechada` | A janela de 24 horas desta conversa está fechada. Na API oficial do WhatsApp, só um modelo aprovado reabre a conversa. | | `conta_restrita` | A Meta restringiu esta conta do WhatsApp por spam. Nada foi enviado: erro.ate diz até quando (erro.retry_after_s e o cabeçalho Retry-After, em segundos), erro.escopo o que está barrado e erro.motivo o código. Quem administra pode desligar a trava em Configurações > API. | | `caixa_obrigatoria` | A organização tem mais de um WhatsApp conectado: informe caixa_id (veja GET /me). | | `caixa_sem_conexao` | Nenhum WhatsApp conectado nesta caixa. | | `canal_sem_iniciar` | Este canal não permite começar conversa por telefone. Use uma caixa de WhatsApp. | | `modelo_sem_api_oficial` | Modelos só saem pela API oficial do WhatsApp. Conecte a API oficial nesta caixa. | | `modelo_inexistente` | Modelo não encontrado nesta caixa (veja GET /modelos). | | `canal_invalido` | canal deve ser "auto", "oficial" ou "qr". | | `canal_so_whatsapp` | canal "oficial"/"qr" só vale em caixa de WhatsApp. Omita o campo (ou use "auto"). | | `canal_oficial_indisponivel` | canal "oficial": a API oficial do WhatsApp não está conectada nesta caixa. Nada foi enviado pelo QR. | | `canal_oficial_sem_grupo` | canal "oficial": a API oficial do WhatsApp não envia para grupo. Use "qr" ou omita o campo. | | `canal_oficial_tipo` | canal "oficial": a API oficial do WhatsApp não envia este tipo de mensagem. Nada foi enviado pelo QR. | | `canal_qr_indisponivel` | canal "qr": o WhatsApp por QR não está conectado nesta caixa. Nada foi enviado pela API oficial. | | `canal_qr_sem_modelo` | canal "qr": modelos só saem pela API oficial do WhatsApp. Use "oficial" ou omita o campo. | | `canal_qr_tipo` | canal "qr": este tipo de mensagem só sai pela API oficial do WhatsApp. Nada foi enviado pela API oficial. | | `mensagem_inexistente` | Mensagem não encontrada. | | `conversa_inexistente` | Conversa não encontrada. | | `contato_inexistente` | Contato não encontrado. | | `caixa_inexistente` | Caixa não encontrada (veja GET /me). | | `webhook_inexistente` | Webhook não encontrado. | | `entrega_inexistente` | Entrega não encontrada. | | `webhook_desativado` | Este webhook está desativado. Reative com PATCH {"ativo": true}. | | `entrega_em_andamento` | Esta entrega ainda está em andamento. | | `entrega_ja_despachada` | Esta entrega já saiu uma vez e não é repetida (entrega única, sem duplicar). Só entregas que nunca saíram podem ser reenviadas. | | `url_invalida` | URL inválida: use https://, porta 443 e um nome de domínio público. | | `destino_bloqueado` | Esse endereço aponta para uma rede interna ou reservada. Use um endereço público. | | `dns_falhou` | Não conseguimos resolver esse domínio. Confira o endereço. | | `eventos_invalidos` | Algum evento não existe. Veja a lista na documentação. | | `limite_de_webhooks` | Limite de 10 webhooks por organização. | | `nome_invalido` | Nome deve ter de 1 a 60 caracteres. | | `telefone_invalido` | Telefone inválido. Mande com DDI e DDD. | | `telefone_repetido` | Já existe um contato com esse telefone (com ou sem o 9). | | `graca_invalida` | horas deve ser de 0 a 168. | | `negocio_inexistente` | Negócio não encontrado. | | `etapa_inexistente` | Etapa não encontrada (veja GET /funis). | | `funil_inexistente` | Funil não encontrado. | | `nota_inexistente` | Nota não encontrada. | | `etapa_fora_do_funil` | Essa etapa não pertence ao funil informado. | | `etapa_ganha_e_perdida` | Uma etapa não pode ser de ganho e de perda ao mesmo tempo. | | `estado_invalido` | estado deve ser aberto, ganho ou perdido. | | `valor_invalido` | O valor não pode ser negativo. | | `limite_de_funis` | Limite de 20 funis por organização. | | `limite_de_etapas` | Limite de 30 etapas por funil. | | `limite_de_notas` | Limite de 500 notas por negócio. | | `periodo_longo` | Escolha um período de até 366 dias. | | `periodo_invalido` | Período inválido: `de` precisa ser antes de `ate` (AAAA-MM-DD). | | `granularidade_invalida` | granularidade deve ser dia, semana ou mes. | | `caixa_invalida` | Caixa não encontrada nesta organização (veja GET /me). | | `limite_relatorios` | Relatórios têm limite próprio, mais baixo. Espere o tempo de Retry-After. | | `relatorio_demorou` | O relatório demorou demais. Escolha um período menor ou uma granularidade maior (semana ou mes). | | `assinatura_bloqueada` | A assinatura da organização está suspensa. Fale com um dono. | | `horario_invalido` | Data e hora inválidas. Use o formato 2026-10-12T14:00:00-03:00 (sempre com o fuso), com o fim depois do início. | | `janela_grande` | A janela de consulta passa do máximo (62 dias). | | `duracao_invalida` | Duração inválida: de 5 a 720 minutos (compromisso de até 12 horas). | | `campo_longo` | Um dos textos passa do tamanho máximo. O campo vem em "detalhe". | | `campo_invalido` | Campo obrigatório ou inválido. O campo vem em "detalhe". | | `link_invalido` | O link precisa começar com https://. | | `contato_ambiguo` | Informe só um entre contato_id, conversa_id e telefone. | | `conversa_de_outro_contato` | A conversa informada é de outro contato. | | `evento_cancelado` | O compromisso está cancelado e não pode ser alterado. Marque um novo. | | `evento_finalizado` | O compromisso já foi encerrado (concluído ou faltou) e não pode ser alterado. | | `horario_ocupado` | Esse horário já está ocupado nessa agenda. | | `horario_bloqueado` | Esse horário está bloqueado (folga, almoço ou feriado). | | `fora_do_horario` | Esse horário está fora do horário de trabalho da agenda. | | `antecedencia` | Esse horário está mais perto do que a antecedência mínima da agenda. | | `compromisso_estado_invalido` | estado só pode ser concluido ou faltou. Para confirmar ou cancelar, use as rotas próprias. | | `nada_para_atualizar` | Mande ao menos um campo para atualizar. | | `fuso_invalido` | Fuso inválido. Use um nome IANA, como America/Sao_Paulo. | | `cor_invalida` | Cor inválida. Use slate, blue, violet, amber, emerald, green, rose, cyan ou orange. | | `lembretes_invalidos` | Lembretes inválidos: até 5 itens, cada um só com antes_min (1 a 43200). | | `usuario_inexistente` | usuario_id não é de uma pessoa desta equipe. | | `agenda_inexistente` | Agenda não encontrada (ou inativa, para marcar compromisso). | | `evento_inexistente` | Compromisso não encontrado. | | `bloqueio_inexistente` | Bloqueio não encontrado. | | `agenda_repetida` | Essa pessoa já tem uma agenda. | | `limite_agendas` | Limite de 100 agendas por organização. | | `limite_bloqueios` | Limite de 500 bloqueios por organização. | | `bloqueio_tipo_invalido` | tipo deve ser folga, almoco, feriado ou outro. | | `repeticao_invalida` | repetir deve ser nunca, diario, semanal ou anual; repetir_ate só vale com repetição. | | `sem_permissao` | A chave de API não pode fazer isso. Veja o motivo em "detalhe". | | `ja_e_membro` | Esse e-mail já é de alguém da equipe. | | `limite_de_acessos` | O plano chegou ao limite de acessos (pessoas + convites abertos). Remova alguém ou cancele um convite. | | `limite_de_convites` | Limite de convites: 20 por dia por organização e 3 por dia para o mesmo e-mail. Tente amanhã. | | `membro_inexistente` | Pessoa não encontrada nesta organização (veja GET /equipe/membros). | | `convite_inexistente` | Convite não encontrado, já aceito ou vencido (veja GET /equipe/convites). | | `equipe_inexistente` | Equipe não encontrada (veja GET /equipe/equipes). | | `equipe_repetida` | Já existe uma equipe com esse nome. | | `papel_inexistente` | Papel de acesso não encontrado nesta organização. | | `modulos_do_papel` | Com papel de acesso, os módulos e a agenda vêm do papel. Mande papel_de_acesso_id: null para escolher à mão. | | `regra_inexistente` | Regra de comentário não encontrada (veja GET /regras-comentario). | | `regra_conexao_invalida` | conexao_id precisa ser uma conta do Instagram conectada por login (veja GET /instagram/contas). | | `regra_sem_palavras` | Informe de 1 a 30 palavras, ou use qualquer_comentario: true. | | `regra_modo_invalido` | modo deve ser contem ou igual. | | `regra_sem_acao` | A regra precisa de uma resposta pública ou de uma resposta privada. | | `regra_texto_longo` | Texto longo demais: resposta pública até 2200 caracteres e resposta privada até 1000. | | `regra_variantes_demais` | No máximo 5 respostas_publicas. | | `regra_extra_sem_publico` | respostas_publicas e ocultar_apos só valem com responder_publico preenchido. | | `regra_automacao_invalida` | automacao_id não é uma automação desta organização (veja GET /automacoes?gatilho=comentario). | | `regra_automacao_sem_privada` | Para ligar uma automação, preencha resposta_privada. | | `regra_botao_longo` | Cada botão tem no máximo 20 caracteres. | | `regra_botao_repetido` | Botões repetidos. Cada botão deve ser diferente. | | `regra_botoes_demais` | No máximo 3 botões. | | `regra_botoes_invalidos` | botoes deve ser uma lista de textos ou de objetos { titulo, url }. | | `regra_botao_url_invalida` | A url do botão deve ser um link https:// válido, com até 2000 caracteres. | | `regra_botoes_sem_privada` | Para ter botões, preencha resposta_privada. | | `recurso_desligado` | Este recurso não está liberado para a sua organização. | | `seguir_invalido` | Algum campo de `seguir` é inválido. A mensagem diz qual: textos de 1 a 640 caracteres (os de "já segue" e "seguiu depois" podem ser vazios), botões de 1 a 20 e sem `{` (botão não troca variáveis), cada texto até 900 bytes (emoji e acento contam mais), sem caractere de controle ou invisível, `max_tentativas` de 1 a 50 e link do conteúdo em https. | | `limite_de_regras` | Limite de 200 regras de comentário por organização. Apague alguma para criar outra. | | `modulo_invalido` | Módulo inválido. Use conversas, contatos, automacoes, agentes, funil, agenda, followup ou relatorios. | ## Paginação Listas respondem `{"dados": [...], "proximo": "..."}`. Para a próxima página, repita a mesma chamada com `?cursor=` e o valor de `proximo`. Quando `proximo` vier `null`, acabou. Use `?limite=` de 1 a 100 (padrão 50). - O cursor é opaco: não monte nem altere; só devolva o que recebeu. Cursor estranho dá `422 cursor_invalido`. - Mantenha os mesmos filtros entre as páginas. - Rotas com cursor: [`GET /mensagens`](https://docs.forsi.com.br/api/mensagens/listar-mensagens-org), [`GET /conversas`](https://docs.forsi.com.br/api/conversas/listar-conversas), [`GET /conversas/{id}/mensagens`](https://docs.forsi.com.br/api/conversas/listar-mensagens), [`GET /contatos`](https://docs.forsi.com.br/api/contatos/listar-contatos), [`GET /negocios`](https://docs.forsi.com.br/api/funil/listar-negocios), [`GET /negocios/{id}/notas`](https://docs.forsi.com.br/api/funil/listar-notas-negocio), [`GET /agendas`](https://docs.forsi.com.br/api/agenda/listar-agendas), [`GET /compromissos`](https://docs.forsi.com.br/api/agenda/listar-compromissos), [`GET /bloqueios`](https://docs.forsi.com.br/api/agenda/listar-bloqueios), [`GET /equipe/membros`](https://docs.forsi.com.br/api/equipe/listar-membros), [`GET /equipe/presenca`](https://docs.forsi.com.br/api/equipe/listar-presenca), [`GET /equipe/convites`](https://docs.forsi.com.br/api/equipe/listar-convites), [`GET /equipe/equipes`](https://docs.forsi.com.br/api/equipe/listar-equipes), [`GET /regras-comentario`](https://docs.forsi.com.br/api/automacoes/listar-regras-comentario). - Estas listas vêm inteiras, sem cursor: [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos), [`GET /webhooks`](https://docs.forsi.com.br/api/webhooks/listar-webhooks), [`GET /webhooks/{id}/entregas`](https://docs.forsi.com.br/api/webhooks/listar-entregas), [`GET /funis`](https://docs.forsi.com.br/api/funil/listar-funis), [`GET /instagram/contas`](https://docs.forsi.com.br/api/automacoes/listar-contas-instagram), [`GET /automacoes`](https://docs.forsi.com.br/api/automacoes/listar-automacoes-de-comentario). - Entregas de webhook usam `?antes=` em vez de cursor. ```js title="JavaScript" async function todasAsConversas() { const itens = []; let cursor = null; do { const url = new URL('https://chat.forsi.com.br/api/v1/conversas'); url.searchParams.set('limite', '100'); if (cursor) url.searchParams.set('cursor', cursor); const resposta = await fetch(url, { headers: { Authorization: 'Bearer ' + process.env.FORSI_CHAVE } }); const json = await resposta.json(); if (!resposta.ok) throw new Error(json.erro.codigo + ': ' + json.erro.mensagem); itens.push(...json.dados); cursor = json.proximo; // null quando acabou } while (cursor); return itens; } ``` ```python title="Python" import os import requests def todas_as_conversas(): itens, cursor = [], None while True: params = {"limite": 100, **({"cursor": cursor} if cursor else {})} resposta = requests.get( "https://chat.forsi.com.br/api/v1/conversas", headers={"Authorization": "Bearer " + os.environ["FORSI_CHAVE"]}, params=params, timeout=30, ) corpo = resposta.json() resposta.raise_for_status() itens += corpo["dados"] cursor = corpo.get("proximo") if not cursor: return itens ``` ## Idempotência Rede cai. Para poder repetir um pedido sem duplicar, mande o cabeçalho `Idempotency-Key` com um valor único por operação (o id do pedido no seu sistema, por exemplo). Em `POST /mensagens`, ele é obrigatório; você pode usar `id_cliente` (UUID) no corpo no lugar do cabeçalho. - Mesma chave e mesmo corpo em até 7 dias para envios (24 horas para outras operações): devolvemos a primeira resposta, com `Idempotent-Replayed: true` e `Idempotent-Replayed-At`, e nada é feito de novo. Depois do prazo, o mesmo identificador de mensagem ainda impede novo envio. - Mesma chave com corpo diferente: `409 idempotencia_conflito`. Use uma chave nova para um pedido novo. - A chave tem de 1 a 200 caracteres visíveis, sem espaço (`422 idempotencia_invalida`). - A chave vale para a organização inteira (todas as chaves de API) e para uma operação só. Reaproveitar a mesma chave em outra rota também dá `409 idempotencia_conflito`. Com mais de uma integração, use um prefixo por integração (`erp-pedido-4821`). ```bash title="repetir com segurança" curl -X POST "https://chat.forsi.com.br/api/v1/mensagens" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "telefone": "+55 31 98888-7777", "tipo": "texto", "texto": "Olá! Sua consulta está confirmada para amanhã às 9h." }' ``` ### Rotas que aceitam Idempotency-Key | Rota | Operação | | --- | --- | | [`POST /mensagens`](https://docs.forsi.com.br/api/mensagens/enviar-mensagem) | Enviar mensagem | | [`POST /contatos`](https://docs.forsi.com.br/api/contatos/criar-contato) | Criar contato | | [`POST /webhooks`](https://docs.forsi.com.br/api/webhooks/criar-webhook) | Criar webhook | | [`POST /webhooks/{id}/rotacionar`](https://docs.forsi.com.br/api/webhooks/rotacionar-webhook) | Trocar o segredo | | [`POST /webhooks/{id}/testar`](https://docs.forsi.com.br/api/webhooks/testar-webhook) | Mandar um evento de teste | | [`POST /webhooks/entregas/{id}/reenviar`](https://docs.forsi.com.br/api/webhooks/reenviar-entrega) | Reenviar uma entrega | | [`POST /funis`](https://docs.forsi.com.br/api/funil/criar-funil) | Criar funil | | [`POST /etapas`](https://docs.forsi.com.br/api/funil/criar-etapa) | Criar etapa | | [`POST /negocios`](https://docs.forsi.com.br/api/funil/criar-negocio) | Criar negócio | | [`POST /negocios/{id}/mover`](https://docs.forsi.com.br/api/funil/mover-negocio) | Mover para outra etapa | | [`POST /negocios/{id}/ganhar`](https://docs.forsi.com.br/api/funil/ganhar-negocio) | Marcar como ganho | | [`POST /negocios/{id}/perder`](https://docs.forsi.com.br/api/funil/perder-negocio) | Marcar como perdido | | [`POST /negocios/{id}/reabrir`](https://docs.forsi.com.br/api/funil/reabrir-negocio) | Reabrir negócio | | [`POST /negocios/{id}/notas`](https://docs.forsi.com.br/api/funil/criar-nota-negocio) | Anotar no negócio | | [`POST /agendas`](https://docs.forsi.com.br/api/agenda/criar-agenda) | Criar agenda | | [`POST /compromissos`](https://docs.forsi.com.br/api/agenda/criar-compromisso) | Marcar compromisso | | [`POST /compromissos/{id}/remarcar`](https://docs.forsi.com.br/api/agenda/remarcar-compromisso) | Remarcar compromisso | | [`POST /bloqueios`](https://docs.forsi.com.br/api/agenda/criar-bloqueio) | Criar bloqueio | | [`POST /equipe/convites`](https://docs.forsi.com.br/api/equipe/criar-convite) | Convidar atendente | | [`POST /equipe/convites/{id}/reenviar`](https://docs.forsi.com.br/api/equipe/reenviar-convite) | Reenviar convite | | [`POST /equipe/equipes`](https://docs.forsi.com.br/api/equipe/criar-equipe) | Criar equipe | | [`POST /conversas/{id}/atribuir`](https://docs.forsi.com.br/api/equipe/atribuir-conversa) | Atribuir ou transferir conversa | | [`POST /regras-comentario`](https://docs.forsi.com.br/api/automacoes/criar-regra-comentario) | Criar regra de comentário | | [`PATCH /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/editar-regra-comentario) | Editar regra de comentário | As demais escritas são naturalmente seguras de repetir (atualizar com o mesmo valor, cancelar duas vezes) ou dão `409`/`404` na segunda vez. ## Webhooks de saída e assinatura Webhook é como a FORSI avisa o seu sistema quando algo acontece (mensagem recebida, status de um envio, negócio movido, compromisso marcado) sem você precisar perguntar. Cadastre um endereço https público (pela tela ou por [`POST /webhooks`](https://docs.forsi.com.br/api/webhooks/criar-webhook)) e escolha os eventos. A cada evento, mandamos um `POST` com JSON para o seu endereço, assinado com HMAC-SHA256. - Responda `2xx` em até 10 segundos; outra resposta ou demora conta como falha. Não seguimos redirecionamentos. - O endereço precisa ser https, porta 443 e um nome de domínio público (sem IP, sem rede interna). - Até 10 webhooks por organização. Downgrade que tira a API do plano também para os webhooks. ### Eventos | Evento | Quando | | --- | --- | | `mensagem.recebida` | O contato mandou uma mensagem. | | `mensagem.enviada` | Uma mensagem sua saiu (pela tela, por um agente ou pela API). | | `mensagem.status` | Mudou o status de uma mensagem enviada: entregue, lida, falhou... | | `conversa.criada` | Nasceu uma conversa nova. | | `conversa.atribuida` | A conversa mudou de responsável ou de equipe. | | `conversa.encerrada` | A conversa foi resolvida. | | `contato.criado` | Um contato novo foi criado. | | `negocio.criado` | Um negócio novo entrou no funil. | | `negocio.movido` | Um negócio mudou de etapa. | | `negocio.ganho` | Um negócio foi marcado como ganho. | | `negocio.perdido` | Um negócio foi marcado como perdido. | | `agendamento.criado` | Um compromisso foi marcado (pela tela, pelo agente ou pela API). | | `agendamento.remarcado` | Um compromisso mudou de dia, hora ou agenda. Vem com o horário anterior. | | `agendamento.cancelado` | Um compromisso foi cancelado. | | `agendamento.confirmado` | Um compromisso foi confirmado. | | `membro.convidado` | Alguém foi convidado para a equipe (pela tela ou pela API). | | `membro.entrou` | Uma pessoa aceitou o convite e entrou na equipe. | | `membro.alterado` | Mudou o papel ou as permissões de alguém. Vem com o valor anterior. | | `membro.removido` | Uma pessoa saiu da equipe. | Todos têm o mesmo envelope: `id` (o mesmo em todas as tentativas), `evento`, `criado_em`, `organizacao_id` e `dados`. Em `dados` vem o que importa ao evento: `mensagem`, `conversa`, `contato`, `negocio`, `agendamento`, `membro`, `convite`... Mudanças trazem `estado_anterior` ou `anterior` (`agendamento.remarcado` e `membro.alterado`). ```json title="corpo de mensagem.recebida (resumido)" { "id": "5e458e2a-2f89-4c56-8f5e-f6bfa9ef0a35", "evento": "mensagem.recebida", "criado_em": "2026-10-04T13:05:12.481Z", "organizacao_id": "dddecb67-6b55-43da-94f6-18c8d6680571", "dados": { "mensagem": { "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "direcao": "entrada", "tipo": "texto", "texto": "Oi, queria remarcar minha consulta", "autor_tipo": "contato", "criado_em": "2026-10-04T13:05:11.902Z" }, "conversa": { "id": "495a7cae-128c-4b66-adbc-807b3959c129", "estado": "aberta", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" } } } ``` ### Cabeçalhos - `X-Forsi-Event`: o evento. - `X-Forsi-Delivery`: id da entrega (muda no reenvio manual). - `X-Forsi-Timestamp`: segundos Unix do envio. - `X-Forsi-Signature`: `t=,v1=`. Durante a troca de segredo vêm dois `v1`. - `User-Agent`: começa com `FORSI-Chat-Webhooks/1`. ### Conferir a assinatura Cada webhook tem um segredo (`whsec_...`), mostrado uma vez quando é criado ou trocado. `v1` é o HMAC-SHA256, em hexadecimal, do texto `.`, usando como chave o **segredo inteiro, com o prefixo `whsec_`**. > **Figura: Como conferir a assinatura de um webhook.** A FORSI calcula o HMAC-SHA256 de "." com o segredo whsec_ inteiro e manda no cabeçalho X-Forsi-Signature (t=,v1=). O seu servidor lê o corpo cru, recusa se t estiver a mais de 5 minutos do relógio, refaz o HMAC com o mesmo segredo e compara em tempo constante com cada v1. Bateu: processe e responda 2xx em até 10 segundos. Não bateu: responda 401 e descarte. 1. Leia o corpo cru, byte a byte, antes de converter para JSON (reformatar muda a assinatura). 2. Pegue `t` e os `v1` do cabeçalho `X-Forsi-Signature`. 3. Recuse se `t` estiver a mais de 5 minutos do seu relógio (protege contra reenvio de um evento capturado). 4. Calcule o HMAC e compare em tempo constante com cada `v1`. Aceite se qualquer um bater. ```js title="Node.js (Express)" import crypto from 'node:crypto'; import express from 'express'; const app = express(); const SEGREDO = process.env.FORSI_WEBHOOK_SEGREDO; // whsec_... (o texto inteiro, com o prefixo) // leia o corpo CRU (texto), antes de qualquer JSON.parse app.post('/webhooks/forsi', express.text({ type: '*/*' }), (req, res) => { const cabecalho = req.get('X-Forsi-Signature') ?? ''; const t = /t=(\d+)/.exec(cabecalho)?.[1]; const recebidas = [...cabecalho.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]); if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400); const esperada = crypto.createHmac('sha256', SEGREDO).update(t + '.' + req.body).digest('hex'); const ok = recebidas.some((a) => crypto.timingSafeEqual(Buffer.from(a), Buffer.from(esperada))); if (!ok) return res.sendStatus(401); const evento = JSON.parse(req.body); // evento.id é o mesmo em todas as tentativas: ignore se já processou res.sendStatus(200); // responda em até 10 s; trabalho pesado vai para a sua fila }); ``` ```python title="Python (Flask)" import hashlib import hmac import json import os import re import time from flask import Flask, abort, request app = Flask(__name__) SEGREDO = os.environ["FORSI_WEBHOOK_SEGREDO"] # whsec_... (o texto inteiro, com o prefixo) @app.post("/webhooks/forsi") def receber(): cabecalho = request.headers.get("X-Forsi-Signature", "") t = re.search(r"t=(\d+)", cabecalho) recebidas = re.findall(r"v1=([0-9a-f]{64})", cabecalho) if not t or abs(time.time() - int(t.group(1))) > 300: abort(400) corpo = request.get_data() # bytes crus, antes de qualquer json.loads esperada = hmac.new(SEGREDO.encode(), t.group(1).encode() + b"." + corpo, hashlib.sha256).hexdigest() if not any(hmac.compare_digest(esperada, a) for a in recebidas): abort(401) evento = json.loads(corpo) # evento["id"] é o mesmo em todas as tentativas: ignore se já processou return "", 200 ``` ```php title="PHP" 300) { http_response_code(400); exit; } $esperada = hash_hmac('sha256', $t[1] . '.' . $corpo, $segredo); foreach ($v[1] as $a) { if (hash_equals($esperada, $a)) { $evento = json_decode($corpo, true); // $evento['id'] é o mesmo em todas as tentativas: ignore se já processou http_response_code(200); exit; } } http_response_code(401); ``` ```bash title="curl + openssl (testar o seu receptor)" # Simula uma entrega assinada para testar o seu receptor (mesma conta que a FORSI faz). SEGREDO="whsec_EXAMPLE" # o segredo do webhook, inteiro, com o prefixo T=$(date +%s) CORPO='{"id":"9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","evento":"mensagem.recebida","criado_em":"2026-10-04T13:05:12.481Z","dados":{}}' V1=$(printf '%s.%s' "$T" "$CORPO" | openssl dgst -sha256 -hmac "$SEGREDO" | sed 's/^.*= //') curl -X POST "https://seu-sistema.com.br/webhooks/forsi" \ -H "Content-Type: application/json" \ -H "X-Forsi-Event: mensagem.recebida" \ -H "X-Forsi-Delivery: 5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e" \ -H "X-Forsi-Timestamp: $T" \ -H "X-Forsi-Signature: t=$T,v1=$V1" \ --data-raw "$CORPO" ``` ### Tentativas, ordem e repetição - Cada entrega é aceita no máximo uma vez. Se o seu endereço responder erro, demorar demais depois de receber ou cair no meio, a entrega fica como "resultado incerto" e não é repetida, para nunca duplicar um efeito no seu sistema. Só é tentada de novo (30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h e 12 h) o que comprovadamente não foi aceito: falha antes do envio (DNS, conexão recusada, certificado, conexão que não abre) ou resposta `429` / `503` do seu servidor. Outros erros 5xx não são repetidos, pois podem ter sido processados. - Uma entrega que nunca chegou a sair (por exemplo, cancelada quando o webhook foi desligado, ou que esgotou as tentativas sem conseguir conectar) pode ser reenviada pela tela ou por [`POST /webhooks/entregas/{id}/reenviar`](https://docs.forsi.com.br/api/webhooks/reenviar-entrega). A que já saiu uma vez é recusada com o código `entrega_ja_despachada`. - Um webhook que falha por 24 horas seguidas é desligado e o dono recebe um aviso. Reative com [`PATCH /webhooks/{id}`](https://docs.forsi.com.br/api/webhooks/atualizar-webhook) e `{"ativo": true}`. - A ordem de chegada não é garantida e o mesmo evento pode chegar mais de uma vez. Use o `id` do evento para ignorar repetidos e o `criado_em` para ordenar. - [`POST /webhooks/{id}/testar`](https://docs.forsi.com.br/api/webhooks/testar-webhook) manda um evento de teste assinado; [`GET /webhooks/{id}/entregas`](https://docs.forsi.com.br/api/webhooks/listar-entregas) mostra as últimas entregas, com status e resposta. ### Trocar o segredo [`POST /webhooks/{id}/rotacionar`](https://docs.forsi.com.br/api/webhooks/rotacionar-webhook) gera um segredo novo (mostrado uma vez). O antigo continua assinando junto pelas `horas` que você escolher (0 a 168, padrão 24): nesse tempo vêm dois `v1`. Atualize o segredo no seu receptor dentro desse prazo. ### Perguntas frequentes #### Por que a assinatura não bate? Quase sempre é o corpo: o receptor converteu para JSON e de volta para texto antes de calcular, ou o framework já tinha lido o corpo. Calcule o HMAC sobre o corpo cru, exatamente como chegou. As outras causas comuns são usar o segredo sem o prefixo `whsec_` e comparar com o cabeçalho inteiro em vez de cada `v1`. #### O mesmo evento pode chegar duas vezes? Pode. Guarde o `id` do evento e ignore os repetidos. A ordem de chegada também não é garantida: ordene pelo `criado_em`. #### Meu servidor ficou fora do ar. Perdi os eventos? O que não chegou a conectar (DNS, conexão recusada, certificado) é tentado de novo em 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h e 12 h. Responder `503` enquanto o servidor se recupera também faz a FORSI tentar depois. Veja o que chegou em [`GET /webhooks/{id}/entregas`](https://docs.forsi.com.br/api/webhooks/listar-entregas). #### Quanto tempo tenho para responder? 10 segundos. Responda `2xx` logo e faça o trabalho pesado depois, numa fila sua. **Códigos de erro comuns nesta seção**: `webhook_inexistente`, `entrega_inexistente`, `webhook_desativado`, `entrega_em_andamento`, `entrega_ja_despachada`, `url_invalida`, `destino_bloqueado`, `dns_falhou`, `eventos_invalidos`, `limite_de_webhooks`, `nome_invalido`, `graca_invalida`. ### GET /webhooks — Listar webhooks - URL: `GET https://chat.forsi.com.br/api/v1/webhooks` - Acesso: escopo `webhooks:gerir` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Webhook` - Página: https://docs.forsi.com.br/api/webhooks/listar-webhooks (Markdown: https://docs.forsi.com.br/api/webhooks/listar-webhooks.md) **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/webhooks" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "506b7dac-d0bc-4954-8d45-0015588d64b5", "nome": "CRM", "url": "https://seu-sistema.com.br/webhooks/forsi", "eventos": ["mensagem.recebida"], "ativo": true, "segredo_dica": "…a1B2c3", "segredo_anterior_ate": "2026-10-04T13:05:12.481Z", "falhas_seguidas": 0, "falhando_desde": "2026-10-04T13:05:12.481Z", "desativado_em": "2026-10-04T13:05:12.481Z", "motivo_desativacao": "falhas_seguidas", "ultima_entrega_em": "2026-10-04T13:05:12.481Z", "ultimo_status": 200, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "entregas_24h": {"entregues":42,"falhas":0,"pendentes":0} } ] } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /webhooks — Criar webhook - URL: `POST https://chat.forsi.com.br/api/v1/webhooks` - Acesso: escopo `webhooks:gerir` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `WebhookComSegredo` - Página: https://docs.forsi.com.br/api/webhooks/criar-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/criar-webhook.md) Responde com `segredo` UMA vez: guarde para conferir a assinatura `X-Forsi-Signature`. Repetir com a mesma `Idempotency-Key` devolve o webhook sem `segredo` e sem criar outro. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | até 60 caracteres | | `url` | string | sim | https, porta 443, nome público (sem IP, sem rede interna). até 2000 caracteres | | `eventos` | lista de "mensagem.recebida" \| "mensagem.enviada" \| "mensagem.status" \| "conversa.criada" \| "conversa.atribuida" \| "conversa.encerrada" \| "contato.criado" \| "negocio.criado" \| "negocio.movido" \| "negocio.ganho" \| "negocio.perdido" \| "agendamento.criado" \| "agendamento.remarcado" \| "agendamento.cancelado" \| "agendamento.confirmado" \| "membro.convidado" \| "membro.entrou" \| "membro.alterado" \| "membro.removido" | sim | mín. 1 item até 19 itens | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/webhooks" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "CRM", "url": "https://seu-sistema.com.br/webhooks/forsi", "eventos": ["mensagem.recebida","mensagem.status"] }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "506b7dac-d0bc-4954-8d45-0015588d64b5", "nome": "CRM", "url": "https://seu-sistema.com.br/webhooks/forsi", "eventos": ["mensagem.recebida"], "ativo": true, "segredo_dica": "…a1B2c3", "segredo_anterior_ate": "2026-10-04T13:05:12.481Z", "falhas_seguidas": 0, "falhando_desde": "2026-10-04T13:05:12.481Z", "desativado_em": "2026-10-04T13:05:12.481Z", "motivo_desativacao": "falhas_seguidas", "ultima_entrega_em": "2026-10-04T13:05:12.481Z", "ultimo_status": 200, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "entregas_24h": {"entregues":42,"falhas":0,"pendentes":0}, "segredo": "whsec_EXAMPLE" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /webhooks/{id} — Ver um webhook - URL: `GET https://chat.forsi.com.br/api/v1/webhooks/{id}` - Acesso: escopo `webhooks:gerir` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Webhook` - Página: https://docs.forsi.com.br/api/webhooks/ver-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/ver-webhook.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "id": "506b7dac-d0bc-4954-8d45-0015588d64b5", "nome": "CRM", "url": "https://seu-sistema.com.br/webhooks/forsi", "eventos": ["mensagem.recebida"], "ativo": true, "segredo_dica": "…a1B2c3", "segredo_anterior_ate": "2026-10-04T13:05:12.481Z", "falhas_seguidas": 0, "falhando_desde": "2026-10-04T13:05:12.481Z", "desativado_em": "2026-10-04T13:05:12.481Z", "motivo_desativacao": "falhas_seguidas", "ultima_entrega_em": "2026-10-04T13:05:12.481Z", "ultimo_status": 200, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "entregas_24h": {"entregues":42,"falhas":0,"pendentes":0} } } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /webhooks/{id} — Atualizar webhook - URL: `PATCH https://chat.forsi.com.br/api/v1/webhooks/{id}` - Acesso: escopo `webhooks:gerir` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Webhook` - Página: https://docs.forsi.com.br/api/webhooks/atualizar-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/atualizar-webhook.md) Mude nome, URL, eventos, ou pause/reative. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | não | até 60 caracteres | | `url` | string | não | até 2000 caracteres | | `eventos` | lista de "mensagem.recebida" \| "mensagem.enviada" \| "mensagem.status" \| "conversa.criada" \| "conversa.atribuida" \| "conversa.encerrada" \| "contato.criado" \| "negocio.criado" \| "negocio.movido" \| "negocio.ganho" \| "negocio.perdido" \| "agendamento.criado" \| "agendamento.remarcado" \| "agendamento.cancelado" \| "agendamento.confirmado" \| "membro.convidado" \| "membro.entrou" \| "membro.alterado" \| "membro.removido" | não | mín. 1 item até 19 itens | | `ativo` | boolean | não | false pausa; true reativa (zera as falhas). | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"ativo":true}' ``` **Resposta `200`**: mesmo formato do exemplo de `Webhook` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /webhooks/{id} — Apagar webhook - URL: `DELETE https://chat.forsi.com.br/api/v1/webhooks/{id}` - Acesso: escopo `webhooks:gerir` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Apagado` - Página: https://docs.forsi.com.br/api/webhooks/apagar-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/apagar-webhook.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":{"apagado":false,"conversas_apagadas":1}} ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /webhooks/{id}/rotacionar — Trocar o segredo - URL: `POST https://chat.forsi.com.br/api/v1/webhooks/{id}/rotacionar` - Acesso: escopo `webhooks:gerir` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `WebhookComSegredo` - Página: https://docs.forsi.com.br/api/webhooks/rotacionar-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/rotacionar-webhook.md) Gera um segredo novo (mostrado uma vez; repetir com a mesma `Idempotency-Key` não gira de novo nem repete o segredo). Durante a graça as entregas levam as duas assinaturas. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `horas` | integer | não | Quanto tempo o segredo antigo ainda assina junto (0 a 168, padrão 24). | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5/rotacionar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"horas":24}' ``` **Resposta `200`**: mesmo formato do exemplo de `WebhookComSegredo` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /webhooks/{id}/testar — Mandar um evento de teste - URL: `POST https://chat.forsi.com.br/api/v1/webhooks/{id}/testar` - Acesso: escopo `webhooks:gerir` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `202`, `{"dados": ...}` com `TesteAceito` - Página: https://docs.forsi.com.br/api/webhooks/testar-webhook (Markdown: https://docs.forsi.com.br/api/webhooks/testar-webhook.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5/testar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{}' ``` **Resposta `202` (exemplo)** ```json {"dados":{"entrega_id":"4a989b41-b6e1-414a-ba98-158a58f4195d"}} ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /webhooks/{id}/entregas — Histórico de entregas - URL: `GET https://chat.forsi.com.br/api/v1/webhooks/{id}/entregas` - Acesso: escopo `webhooks:gerir` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Entrega` - Página: https://docs.forsi.com.br/api/webhooks/listar-entregas (Markdown: https://docs.forsi.com.br/api/webhooks/listar-entregas.md) 50 por página, da mais nova para a mais antiga, com o corpo enviado e a resposta. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `estado` | "pendente" \| "falhou" \| "entregue" \| "morta" \| "cancelada" | não | | | `antes` | uuid | não | Id da última entrega vista (página seguinte). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/webhooks/506b7dac-d0bc-4954-8d45-0015588d64b5/entregas?estado=falhou" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "4a989b41-b6e1-414a-ba98-158a58f4195d", "webhook_id": "506b7dac-d0bc-4954-8d45-0015588d64b5", "evento": "mensagem.recebida", "evento_id": "5e458e2a-2f89-4c56-8f5e-f6bfa9ef0a35", "estado": "pendente", "tentativas": 1, "status_http": 200, "resposta": "ok", "erro": "Connection timed out", "duracao_ms": 182, "criado_em": "2026-10-04T13:05:12.481Z", "ultima_tentativa_em": "2026-10-04T13:05:12.481Z", "proxima_em": "2026-10-04T13:05:12.481Z", "entregue_em": "2026-10-04T13:05:12.481Z", "payload": { "id": "5e458e2a-2f89-4c56-8f5e-f6bfa9ef0a35", "evento": "mensagem.recebida", "criado_em": "2026-10-04T13:05:12.481Z", "organizacao_id": "dddecb67-6b55-43da-94f6-18c8d6680571", "dados": { "mensagem": { "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "direcao": "entrada", "tipo": "texto", "texto": "Oi, queria remarcar minha consulta", "autor_tipo": "contato", "criado_em": "2026-10-04T13:05:11.902Z" }, "conversa": { "id": "495a7cae-128c-4b66-adbc-807b3959c129", "estado": "aberta", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" } } } } ] } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /webhooks/entregas/{id}/reenviar — Reenviar uma entrega - URL: `POST https://chat.forsi.com.br/api/v1/webhooks/entregas/{id}/reenviar` - Acesso: escopo `webhooks:gerir` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `202`, `{"dados": ...}` com `Entrega` - Página: https://docs.forsi.com.br/api/webhooks/reenviar-entrega (Markdown: https://docs.forsi.com.br/api/webhooks/reenviar-entrega.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/webhooks/entregas/4a989b41-b6e1-414a-ba98-158a58f4195d/reenviar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{}' ``` **Resposta `202` (exemplo)** ```json { "dados": { "id": "4a989b41-b6e1-414a-ba98-158a58f4195d", "webhook_id": "506b7dac-d0bc-4954-8d45-0015588d64b5", "evento": "mensagem.recebida", "evento_id": "5e458e2a-2f89-4c56-8f5e-f6bfa9ef0a35", "estado": "pendente", "tentativas": 1, "status_http": 200, "resposta": "ok", "erro": "Connection timed out", "duracao_ms": 182, "criado_em": "2026-10-04T13:05:12.481Z", "ultima_tentativa_em": "2026-10-04T13:05:12.481Z", "proxima_em": "2026-10-04T13:05:12.481Z", "entregue_em": "2026-10-04T13:05:12.481Z", "payload": { "id": "5e458e2a-2f89-4c56-8f5e-f6bfa9ef0a35", "evento": "mensagem.recebida", "criado_em": "2026-10-04T13:05:12.481Z", "organizacao_id": "dddecb67-6b55-43da-94f6-18c8d6680571", "dados": { "mensagem": { "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "direcao": "entrada", "tipo": "texto", "texto": "Oi, queria remarcar minha consulta", "autor_tipo": "contato", "criado_em": "2026-10-04T13:05:11.902Z" }, "conversa": { "id": "495a7cae-128c-4b66-adbc-807b3959c129", "estado": "aberta", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" } } } } } ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Mensagens Envie texto, mídia (imagem, vídeo, áudio, documento) ou modelo aprovado. O envio vai para a fila e responde `202` na hora; o status (pendente, enviada, entregue, lida, falhou) chega pelo webhook `mensagem.status` ou por [`GET /mensagens/{id}`](https://docs.forsi.com.br/api/mensagens/ver-mensagem). Para consultar mensagens de toda a organização use [`GET /mensagens`](https://docs.forsi.com.br/api/mensagens/listar-mensagens-org); também é possível listar mensagens de uma conversa. > **Figura: O caminho de uma mensagem enviada.** O seu sistema chama POST /mensagens. A API responde 202 na hora e põe a mensagem na fila (status pendente). A FORSI entrega à Meta (enviada); a Meta avisa quando chega ao aparelho (entregue) e quando a pessoa lê (lida). Se algo der errado, o status vira falhou, com o motivo. Cada mudança chega ao seu webhook como o evento mensagem.status; também dá para consultar com GET /mensagens/{id}. - Para quem: `conversa_id` (responder numa conversa que já existe) **ou** `telefone` (com DDI e DDD). Só um dos dois. - Pelo telefone, a conversa e o contato são criados se ainda não existirem; `nome` dá nome ao contato novo. - Pelo telefone, com mais de um WhatsApp conectado, mande `caixa_id` (veja [`GET /me`](https://docs.forsi.com.br/api/conta/me)), senão vem `422 caixa_obrigatoria`. Com `conversa_id` a mensagem sai pela caixa da conversa (`caixa_id` e `nome` só valem com telefone). - `responde_a_id` cita uma mensagem da mesma conversa. - Na API oficial do WhatsApp, fora da janela de 24 horas desde a última mensagem do contato, só sai **modelo aprovado** (`422 janela_fechada`). Veja os modelos em [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos). - Mídia vai por link https público (`midia.url`): o WhatsApp baixa dele na hora do envio. O `texto` vira a legenda. - Mande `Idempotency-Key` para poder repetir sem mandar duas vezes (veja [Idempotência](https://docs.forsi.com.br/idempotencia)). - Limite próprio: 60 envios por minuto por chave e 120 por organização. - `canal` (opcional, só WhatsApp): `auto` (padrão, igual a omitir) segue a regra da caixa; `oficial` sai só pela API oficial da Meta; `qr` sai só pelo WhatsApp por QR. A escolha manual nunca troca de canal: se o canal escolhido não puder enviar, vem erro (`422 canal_oficial_indisponivel`, `canal_oficial_sem_grupo`, `canal_qr_indisponivel`, `canal_qr_sem_modelo`, `canal_so_whatsapp`; a janela de 24 h vale em `oficial`) ou, se cair depois de aceito, a mensagem fica `falhou` com o código em `erro`. Modelo só sai pela oficial. Repetir o mesmo `id_cliente` com outro `canal` dá `409 idempotencia_conflito`. ### Baixar mídia recebida Cada item de `midia[]` nas mensagens e nos webhooks `mensagem.recebida` / `mensagem.enviada` traz `id` estável, além de `mime`, `nome_arquivo`, `tamanho` e `duracao_s` quando disponíveis. Baixe os bytes por [`GET /mensagens/{id}/midia`](https://docs.forsi.com.br/api/mensagens/baixar-midia), autenticado com escopo `mensagens:ler`. Use `?item=` para escolher um anexo por esse id ou `?item=0` para escolher pelo índice começando em zero; sem `item`, baixa o primeiro. - A resposta `200` é o arquivo binário, com `Content-Type` e `Content-Disposition`. O endpoint aceita `Range: bytes=...` e responde `206` com `Content-Range`. - Se o download sob demanda estiver em andamento, recebe `202` com `erro.codigo: midia_baixando` e `Retry-After` (aguarde esse número de segundos antes de tentar de novo). Se não for possível obter o arquivo, recebe `409 midia_indisponivel`. - Mensagem inexistente, de outra organização ou sem mídia responde `404`. ```json title="Responder numa conversa que já existe" { "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "tipo": "texto", "texto": "Perfeito, remarquei para quinta às 10h." } ``` ```json title="Imagem com legenda" { "telefone": "+55 31 98888-7777", "tipo": "imagem", "midia": {"url":"https://seu-sistema.com.br/arquivos/orcamento.jpg"}, "texto": "Segue o orçamento." } ``` ```json title="Documento (PDF)" { "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "tipo": "documento", "midia": { "url": "https://seu-sistema.com.br/arquivos/orcamento.pdf", "nome_arquivo": "Orçamento.pdf" } } ``` ```json title="Modelo aprovado (abre a conversa fora da janela de 24 h)" { "telefone": "+55 31 98888-7777", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "tipo": "modelo", "modelo": {"nome":"lembrete_consulta","idioma":"pt_BR","corpo":["Maria","9h"]} } ``` ```json title="Escolher o canal (só pela API oficial, sem cair para o QR)" { "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "tipo": "texto", "texto": "Seu pedido saiu para entrega.", "canal": "oficial" } ``` **Códigos de erro comuns nesta seção**: `tipo_invalido`, `texto_vazio`, `texto_longo`, `midia_invalida`, `midia_indisponivel`, `midia_baixando`, `janela_fechada`, `conta_restrita`, `caixa_obrigatoria`, `caixa_sem_conexao`, `caixa_inexistente`, `canal_sem_iniciar`, `modelo_sem_api_oficial`, `modelo_inexistente`, `telefone_invalido`, `conversa_inexistente`, `mensagem_inexistente`, `canal_oficial_indisponivel`, `canal_oficial_sem_grupo`, `canal_qr_indisponivel`, `canal_qr_sem_modelo`, `canal_so_whatsapp`. ### POST /mensagens — Enviar mensagem - URL: `POST https://chat.forsi.com.br/api/v1/mensagens` - Acesso: escopo `mensagens:enviar` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `202`, `{"dados": ...}` com `EnvioAceito` - Página: https://docs.forsi.com.br/api/mensagens/enviar-mensagem (Markdown: https://docs.forsi.com.br/api/mensagens/enviar-mensagem.md) Texto, mídia (imagem, vídeo, áudio, documento) ou modelo aprovado. Vai para a fila de envio e responde 202 na hora; acompanhe pelo webhook `mensagem.status` ou por GET /mensagens/{id}. Na API oficial, fora da janela de 24 h só modelo. Envie `Idempotency-Key` ou `id_cliente` (UUID) para identificar o envio. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id_cliente` | uuid | não | UUID estável do envio; substitui Idempotency-Key. | | `conversa_id` | uuid | não | Responder numa conversa que já existe. Ou use telefone. | | `telefone` | string | não | Com DDI e DDD. Acha o contato com ou sem o 9º dígito. mín. 8 caracteres até 30 caracteres ex.: +55 31 98888-7777 | | `caixa_id` | uuid | não | Obrigatório quando a organização tem mais de um WhatsApp conectado. | | `nome` | string | não | Nome do contato, se ele ainda não tiver. até 120 caracteres | | `tipo` | "texto" \| "imagem" \| "video" \| "audio" \| "documento" \| "modelo" | não | padrão "texto" | | `texto` | string | não | Texto (ou legenda da mídia). até 4096 caracteres | | `midia` | object | não | | | `midia.url` | string | sim | Link público https do arquivo. O WhatsApp baixa dele na hora do envio. até 2000 caracteres | | `midia.mime` | string | não | até 100 caracteres | | `midia.nome_arquivo` | string | não | Nome que o contato vê (documentos). até 200 caracteres | | `modelo` | object | não | | | `modelo.id` | uuid | não | Id do modelo (GET /modelos). Ou use nome + idioma. | | `modelo.nome` | string | não | até 512 caracteres | | `modelo.idioma` | string | não | até 15 caracteres ex.: pt_BR | | `modelo.corpo` | lista de string ou objeto livre | não | Valores das variáveis do corpo: lista na ordem ({{1}}, {{2}}...) ou objeto por nome. | | `modelo.cabecalho` | object | não | | | `modelo.botoes` | objeto livre | não | Final do link (ou código) por índice do botão: {"0": "abc123"}. | | `responde_a_id` | uuid | não | Id de uma mensagem desta conversa para citar. | | `canal` | "auto" \| "oficial" \| "qr" | não | Opcional, só WhatsApp. "auto" (padrão, igual a omitir) segue a regra da caixa; "oficial" sai só pela API oficial da Meta; "qr" sai só pelo WhatsApp por QR. Manual nunca troca de canal: se o escolhido não puder enviar, dá erro. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/mensagens" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "telefone": "+55 31 98888-7777", "tipo": "texto", "texto": "Olá! Sua consulta está confirmada para amanhã às 9h." }' ``` **Resposta `202` (exemplo)** ```json { "dados": { "mensagem_id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "conversa_nova": true, "estado": "pendente" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /mensagens — Listar mensagens - URL: `GET https://chat.forsi.com.br/api/v1/mensagens` - Acesso: escopo `mensagens:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Mensagem` (paginada por cursor) - Página: https://docs.forsi.com.br/api/mensagens/listar-mensagens-org (Markdown: https://docs.forsi.com.br/api/mensagens/listar-mensagens-org.md) Mensagens da organização, da mais nova para a mais antiga. Notas internas não aparecem. Filtre por conversa_id ou caixa_id. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | | `conversa_id` | uuid | não | | | `caixa_id` | uuid | não | | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/mensagens" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "direcao": "entrada", "tipo": "texto", "texto": "Oi, queria remarcar minha consulta", "estado": "pendente", "erro": "Connection timed out", "autor_tipo": "contato", "responde_a_id": "ad52f13b-411d-4526-9d07-91f222c74891", "id_whatsapp": "wamid.HBgNNTUzMTk4ODg4Nzc3NxUCABEYEjQ", "enviada_em": "2026-10-04T13:05:12.481Z", "criado_em": "2026-10-04T13:05:12.481Z", "editada_em": "2026-10-04T13:05:12.481Z", "apagada_em": "2026-10-04T13:05:12.481Z", "pela_api": {"chave_id":"71c5c606-a9d7-4270-8891-45d783aa9b71"}, "modelo": "texto", "midia": [ { "id": "94ac725d-5a70-40b2-8fa6-cdd23a32ac85", "mime": "image/jpeg", "nome_arquivo": "foto.jpg", "tamanho": 48213, "duracao_s": 1.5 } ] } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /mensagens/{id} — Ver uma mensagem - URL: `GET https://chat.forsi.com.br/api/v1/mensagens/{id}` - Acesso: escopo `mensagens:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Mensagem` - Página: https://docs.forsi.com.br/api/mensagens/ver-mensagem (Markdown: https://docs.forsi.com.br/api/mensagens/ver-mensagem.md) Inclusive o status atual (pendente, enviada, entregue, lida, falhou). **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/mensagens/4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "id": "4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "direcao": "entrada", "tipo": "texto", "texto": "Oi, queria remarcar minha consulta", "estado": "pendente", "erro": "Connection timed out", "autor_tipo": "contato", "responde_a_id": "ad52f13b-411d-4526-9d07-91f222c74891", "id_whatsapp": "wamid.HBgNNTUzMTk4ODg4Nzc3NxUCABEYEjQ", "enviada_em": "2026-10-04T13:05:12.481Z", "criado_em": "2026-10-04T13:05:12.481Z", "editada_em": "2026-10-04T13:05:12.481Z", "apagada_em": "2026-10-04T13:05:12.481Z", "pela_api": {"chave_id":"71c5c606-a9d7-4270-8891-45d783aa9b71"}, "modelo": "texto", "midia": [ { "id": "94ac725d-5a70-40b2-8fa6-cdd23a32ac85", "mime": "image/jpeg", "nome_arquivo": "foto.jpg", "tamanho": 48213, "duracao_s": 1.5 } ] } } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /mensagens/{id}/midia — Baixar mídia de uma mensagem - URL: `GET https://chat.forsi.com.br/api/v1/mensagens/{id}/midia` - Acesso: escopo `mensagens:ler` · módulo do plano: API - Sucesso: `200` arquivo binário (`Content-Type` e `Content-Disposition`); `206` para Range, `202` enquanto baixa, `409 midia_indisponivel` se ainda não está disponível e `416` para Range inválido. - Página: https://docs.forsi.com.br/api/mensagens/baixar-midia (Markdown: https://docs.forsi.com.br/api/mensagens/baixar-midia.md) Arquivo binário, com Content-Type original e Content-Disposition. Aceita Range, sem redirecionar. Use item com o id de midia[] ou o índice a partir de zero. Mídia sob demanda retorna 202 midia_baixando com Retry-After; mídia indisponível retorna 409 midia_indisponivel. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `item` | uuid ou string | não | Id de midia[] ou índice a partir de zero. Sem item, baixa a primeira mídia. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/mensagens/4e8cb5d0-2d89-4fc2-93bb-c8e5e17d4629/midia?item=0" \ -H "Authorization: Bearer $FORSI_CHAVE" \ --output arquivo-midia ``` **Resposta `200`**: bytes do arquivo. Confira `Content-Type`, `Content-Disposition` e `Accept-Ranges`; `206` inclui `Content-Range`. Se vier `202`, leia o JSON `erro` e tente novamente após `Retry-After`. **Erros**: `401`, `403`, `404`, `409`, `416`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Conversas Conversas são os atendimentos de cada contato numa caixa (WhatsApp ou outro canal). Liste com filtros (`estado`, `caixa_id`, `contato_id`, `telefone`, `desde`) e leia as mensagens de cada uma, da mais nova para a mais antiga. - Notas internas da equipe não aparecem na API. - Para acompanhar em tempo real, prefira webhooks (`mensagem.recebida`, `conversa.criada`, `conversa.atribuida`, `conversa.encerrada`) a ficar consultando. - Para atribuir uma conversa a alguém ou a uma equipe, veja [`POST /conversas/{id}/atribuir`](https://docs.forsi.com.br/api/equipe/atribuir-conversa) em Equipe e permissões. **Códigos de erro comuns nesta seção**: `conversa_inexistente`, `cursor_invalido`. ### GET /conversas — Listar conversas - URL: `GET https://chat.forsi.com.br/api/v1/conversas` - Acesso: escopo `conversas:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Conversa` (paginada por cursor) - Página: https://docs.forsi.com.br/api/conversas/listar-conversas (Markdown: https://docs.forsi.com.br/api/conversas/listar-conversas.md) Da mais recente para a mais antiga, paginado por cursor. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `estado` | "aberta" \| "pendente" \| "adiada" \| "resolvida" | não | | | `caixa_id` | uuid | não | | | `contato_id` | uuid | não | | | `telefone` | string | não | Com DDI e DDD. Acha o contato com ou sem o 9º dígito. mín. 8 caracteres até 30 caracteres ex.: +55 31 98888-7777 | | `desde` | data e hora ISO 8601 | não | Só conversas atualizadas a partir desta data (ISO 8601). | | `anuncio` | string | não | Só conversas que vieram de anúncio: `1` para qualquer anúncio ou o id de um anúncio. | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/conversas?estado=aberta&limite=20" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "495a7cae-128c-4b66-adbc-807b3959c129", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "grupo": false, "titulo": "Consulta de retorno", "estado": "aberta", "responsavel_id": "42b8028f-d7ce-4706-9567-44ac3bff4219", "equipe_id": "55174f08-dda3-49e6-bc5d-9309becdaf6d", "agente_id": "d1ab0f6b-41bf-4522-baaa-c3e281a79935", "etiquetas": ["ea287b0d-9968-4022-b9c7-b6dc239e6101"], "nao_lidas": 2, "janela_ate": "2026-10-04T13:05:12.481Z", "ultima_mensagem_em": "2026-10-04T13:05:12.481Z", "ultima_previa": "Oi, queria remarcar minha consulta", "ultima_direcao": "texto", "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "resolvida_em": "2026-10-04T13:05:12.481Z", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" }, "origem_anuncio": { "anuncio_id": "texto", "titulo": "Consulta de retorno", "canal": "whatsapp", "clicado_em": "2026-10-04T13:05:12.481Z" } } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /conversas/{id} — Ver uma conversa - URL: `GET https://chat.forsi.com.br/api/v1/conversas/{id}` - Acesso: escopo `conversas:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Conversa` - Página: https://docs.forsi.com.br/api/conversas/ver-conversa (Markdown: https://docs.forsi.com.br/api/conversas/ver-conversa.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/conversas/495a7cae-128c-4b66-adbc-807b3959c129" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "id": "495a7cae-128c-4b66-adbc-807b3959c129", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "grupo": false, "titulo": "Consulta de retorno", "estado": "aberta", "responsavel_id": "42b8028f-d7ce-4706-9567-44ac3bff4219", "equipe_id": "55174f08-dda3-49e6-bc5d-9309becdaf6d", "agente_id": "d1ab0f6b-41bf-4522-baaa-c3e281a79935", "etiquetas": ["ea287b0d-9968-4022-b9c7-b6dc239e6101"], "nao_lidas": 2, "janela_ate": "2026-10-04T13:05:12.481Z", "ultima_mensagem_em": "2026-10-04T13:05:12.481Z", "ultima_previa": "Oi, queria remarcar minha consulta", "ultima_direcao": "texto", "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "resolvida_em": "2026-10-04T13:05:12.481Z", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" }, "origem_anuncio": { "anuncio_id": "texto", "titulo": "Consulta de retorno", "canal": "whatsapp", "clicado_em": "2026-10-04T13:05:12.481Z" } } } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /conversas/{id}/mensagens — Mensagens de uma conversa - URL: `GET https://chat.forsi.com.br/api/v1/conversas/{id}/mensagens` - Acesso: escopo `mensagens:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Mensagem` (paginada por cursor) - Página: https://docs.forsi.com.br/api/conversas/listar-mensagens (Markdown: https://docs.forsi.com.br/api/conversas/listar-mensagens.md) Da mais nova para a mais antiga. Notas internas da equipe não aparecem. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/conversas/495a7cae-128c-4b66-adbc-807b3959c129/mensagens?limite=50" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Mensagem` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Contatos Contatos são as pessoas que falam com a sua organização. O telefone é único por organização (com ou sem o nono dígito): repetir dá `409 telefone_repetido`. - Busque por `busca` (parte do nome ou do telefone, ou o e-mail exato), `telefone` ou `email`. - `campos` guarda campos personalizados (objeto livre, até 16 mil caracteres): convênio, plano, código no seu sistema... - No `PATCH`, mande só o que muda; `null` apaga o valor. - Apagar um contato apaga junto as conversas dele. Não tem volta. **Códigos de erro comuns nesta seção**: `contato_inexistente`, `telefone_invalido`, `telefone_repetido`, `cursor_invalido`. ### GET /contatos — Listar e buscar contatos - URL: `GET https://chat.forsi.com.br/api/v1/contatos` - Acesso: escopo `contatos:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Contato` (paginada por cursor) - Página: https://docs.forsi.com.br/api/contatos/listar-contatos (Markdown: https://docs.forsi.com.br/api/contatos/listar-contatos.md) Busca por telefone acha com e sem o 9º dígito. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `busca` | string | não | Parte do nome ou do telefone, ou o e-mail exato. até 100 caracteres | | `telefone` | string | não | Com DDI e DDD. Acha o contato com ou sem o 9º dígito. mín. 8 caracteres até 30 caracteres ex.: +55 31 98888-7777 | | `email` | string | não | até 254 caracteres | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/contatos?telefone=31988887777" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "nome_whatsapp": "Maria", "telefone": "+5531988887777", "email": "maria@exemplo.com.br", "instagram": "maria.souza", "campos": {"convenio":"Unimed"}, "bloqueado": false, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "primeiro_anuncio": { "anuncio_id": "texto", "titulo": "Consulta de retorno", "canal": "whatsapp", "clicado_em": "2026-10-04T13:05:12.481Z" } } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /contatos — Criar contato - URL: `POST https://chat.forsi.com.br/api/v1/contatos` - Acesso: escopo `contatos:escrever` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Contato` - Página: https://docs.forsi.com.br/api/contatos/criar-contato (Markdown: https://docs.forsi.com.br/api/contatos/criar-contato.md) Telefone repetido (com ou sem o 9) dá 409. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string ou null | não | | | `telefone` | string ou null | não | | | `email` | string ou null | não | | | `instagram` | string ou null | não | | | `campos` | objeto livre | não | Campos personalizados (objeto livre, até 16 mil caracteres). | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/contatos" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "Maria Souza", "telefone": "+55 31 98888-7777", "email": "maria@exemplo.com.br", "campos": {"convenio":"Unimed"} }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "nome_whatsapp": "Maria", "telefone": "+5531988887777", "email": "maria@exemplo.com.br", "instagram": "maria.souza", "campos": {"convenio":"Unimed"}, "bloqueado": false, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z", "primeiro_anuncio": { "anuncio_id": "texto", "titulo": "Consulta de retorno", "canal": "whatsapp", "clicado_em": "2026-10-04T13:05:12.481Z" } } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /contatos/{id} — Ver um contato - URL: `GET https://chat.forsi.com.br/api/v1/contatos/{id}` - Acesso: escopo `contatos:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Contato` - Página: https://docs.forsi.com.br/api/contatos/ver-contato (Markdown: https://docs.forsi.com.br/api/contatos/ver-contato.md) Contato mesclado responde com o que ficou. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/contatos/08054a7f-a752-481e-9915-cf4cf944df39" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Contato` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /contatos/{id} — Atualizar contato - URL: `PATCH https://chat.forsi.com.br/api/v1/contatos/{id}` - Acesso: escopo `contatos:escrever` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Contato` - Página: https://docs.forsi.com.br/api/contatos/atualizar-contato (Markdown: https://docs.forsi.com.br/api/contatos/atualizar-contato.md) Só os campos enviados mudam; null apaga o campo. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string ou null | não | | | `telefone` | string ou null | não | | | `email` | string ou null | não | | | `instagram` | string ou null | não | | | `campos` | objeto livre | não | Campos personalizados (objeto livre, até 16 mil caracteres). | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/contatos/08054a7f-a752-481e-9915-cf4cf944df39" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{ "email": "maria.souza@exemplo.com.br", "campos": {"convenio":"Particular"} }' ``` **Resposta `200`**: mesmo formato do exemplo de `Contato` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /contatos/{id} — Apagar contato - URL: `DELETE https://chat.forsi.com.br/api/v1/contatos/{id}` - Acesso: escopo `contatos:escrever` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Apagado` - Página: https://docs.forsi.com.br/api/contatos/apagar-contato (Markdown: https://docs.forsi.com.br/api/contatos/apagar-contato.md) Apaga o contato e as conversas dele. Não tem volta. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/contatos/08054a7f-a752-481e-9915-cf4cf944df39" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Apagado` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Modelos Modelos (templates) são mensagens aprovadas pela Meta. Na API oficial do WhatsApp, são a única forma de falar com quem não mandou mensagem nas últimas 24 horas. - [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos) lista os modelos de cada caixa com nome, idioma, categoria, estado e texto. Filtre por `estado=APPROVED` para ver só os que podem sair. - Para enviar, use [`POST /mensagens`](https://docs.forsi.com.br/api/mensagens/enviar-mensagem) com `tipo: "modelo"` e o `modelo` (id, ou nome + idioma) com as variáveis em `corpo`, o `cabecalho` e os `botoes`, quando houver. - Modelos só saem por caixas conectadas à API oficial (`422 modelo_sem_api_oficial`). **Códigos de erro comuns nesta seção**: `caixa_inexistente`. ### GET /modelos — Listar modelos do WhatsApp - URL: `GET https://chat.forsi.com.br/api/v1/modelos` - Acesso: escopo `modelos:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Modelo` - Página: https://docs.forsi.com.br/api/modelos/listar-modelos (Markdown: https://docs.forsi.com.br/api/modelos/listar-modelos.md) Modelos da API oficial sincronizados com a Meta, com as variáveis de cada um. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `estado` | string | não | APPROVED, PENDING, REJECTED... até 30 caracteres | | `caixa_id` | uuid | não | | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/modelos?estado=APPROVED" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "2eabdd6d-7cb0-4c12-9da3-8f9441056099", "nome": "lembrete_consulta", "idioma": "pt_BR", "categoria": "UTILITY", "estado": "APPROVED", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "formato_parametros": "positional", "variaveis": ["texto"], "texto": "Olá {{1}}, sua consulta é amanhã às {{2}}.", "componentes": [], "atualizado_em": "2026-10-04T13:05:12.481Z" } ] } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Funil e negócios Funis organizam negócios em etapas (colunas). Cada funil tem ao menos uma etapa aberta; etapas podem ser de ganho ou de perda. O módulo Funil precisa estar no plano. - Permissões: `funil:ler`, `funil:escrever` (criar, editar, mover, ganhar, perder, anotar) e `funil:apagar` (apagar negócios, notas, etapas e funis, sem volta), separada de propósito. - Negócio sem funil vai para o primeiro funil; sem etapa, para a primeira etapa aberta. Sem título, usa o nome do contato. - Valores em centavos (`valor_centavos`); `previsao` é a data prevista de fechamento (AAAA-MM-DD). - Mover para uma etapa de ganho ou de perda tem o mesmo efeito de ganhar ou perder. `reabrir` volta para a primeira etapa aberta. - Apagar uma etapa move os negócios dela para `destino_id` (ou para a primeira etapa aberta). - Limites: 20 funis por organização, 30 etapas por funil, 500 notas por negócio. - Eventos de webhook: `negocio.criado`, `negocio.movido`, `negocio.ganho` e `negocio.perdido`. ```json title="Para uma etapa de perda, com motivo" { "etapa_id": "8bd304cb-bbac-4aba-b321-d894b0fe5669", "motivo": "Achou mais barato em outro lugar" } ``` **Códigos de erro comuns nesta seção**: `funil_inexistente`, `etapa_inexistente`, `negocio_inexistente`, `nota_inexistente`, `etapa_fora_do_funil`, `etapa_ganha_e_perdida`, `estado_invalido`, `valor_invalido`, `limite_de_funis`, `limite_de_etapas`, `limite_de_notas`, `contato_inexistente`, `conversa_inexistente`. ### GET /funis — Listar funis - URL: `GET https://chat.forsi.com.br/api/v1/funis` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com lista de `Funil` - Página: https://docs.forsi.com.br/api/funil/listar-funis (Markdown: https://docs.forsi.com.br/api/funil/listar-funis.md) Cada funil vem com as etapas, na ordem do quadro. **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/funis" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "nome": "Implantes", "posicao": 0, "etapas": [{"id":"929692a6-7c6e-4f64-988f-e3a786bb5349"}], "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } ] } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /funis — Criar funil - URL: `POST https://chat.forsi.com.br/api/v1/funis` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Funil` - Página: https://docs.forsi.com.br/api/funil/criar-funil (Markdown: https://docs.forsi.com.br/api/funil/criar-funil.md) Até 20 funis e 30 etapas por funil. Sem `etapas`, usa o modelo do segmento. Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | até 60 caracteres | | `etapas` | lista de object | não | Sem etapas, o funil nasce com o modelo do segmento da organização. Precisa de pelo menos uma etapa que não seja de ganho nem de perda. mín. 1 item até 30 itens | | `etapas[].nome` | string | sim | até 40 caracteres | | `etapas[].cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `etapas[].ganha` | boolean | não | Etapa de ganho: negócio que chega aqui vira ganho. | | `etapas[].perdida` | boolean | não | Etapa de perda: negócio que chega aqui vira perdido. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/funis" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "Implantes", "etapas": [ {"nome":"Avaliação","cor":"blue"}, {"nome":"Orçamento enviado","cor":"amber"}, {"nome":"Fechado","cor":"green","ganha":true}, {"nome":"Desistiu","cor":"rose","perdida":true} ] }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "nome": "Implantes", "posicao": 0, "etapas": [{"id":"929692a6-7c6e-4f64-988f-e3a786bb5349"}], "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /funis/{id} — Ver um funil - URL: `GET https://chat.forsi.com.br/api/v1/funis/{id}` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Funil` - Página: https://docs.forsi.com.br/api/funil/ver-funil (Markdown: https://docs.forsi.com.br/api/funil/ver-funil.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/funis/182aeb59-9ecb-47b2-a745-dfa6f21ea385" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Funil` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /funis/{id} — Renomear funil - URL: `PATCH https://chat.forsi.com.br/api/v1/funis/{id}` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Funil` - Página: https://docs.forsi.com.br/api/funil/atualizar-funil (Markdown: https://docs.forsi.com.br/api/funil/atualizar-funil.md) Só o nome muda; etapas se mexem em /etapas. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | até 60 caracteres | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/funis/182aeb59-9ecb-47b2-a745-dfa6f21ea385" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"nome":"Implantes 2026"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Funil` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /funis/{id} — Apagar funil - URL: `DELETE https://chat.forsi.com.br/api/v1/funis/{id}` - Acesso: escopo `funil:apagar` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `ApagadoFunil` - Página: https://docs.forsi.com.br/api/funil/apagar-funil (Markdown: https://docs.forsi.com.br/api/funil/apagar-funil.md) Apaga o funil, as etapas e TODOS os negócios dele. Não tem volta. O último funil não pode ser apagado. Escopo próprio, separado de `funil:escrever`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/funis/182aeb59-9ecb-47b2-a745-dfa6f21ea385" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":{"apagado":false,"negocios_apagados":0,"negocios_movidos":3}} ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /etapas — Criar etapa - URL: `POST https://chat.forsi.com.br/api/v1/etapas` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Etapa` - Página: https://docs.forsi.com.br/api/funil/criar-etapa (Markdown: https://docs.forsi.com.br/api/funil/criar-etapa.md) Informe `funil_id`. Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `funil_id` | uuid | sim | | | `nome` | string | sim | até 40 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `ganha` | boolean | não | | | `perdida` | boolean | não | | | `posicao` | integer | não | Posição no funil, a partir de 0. Sem isso vai para o fim. máx. 30 | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/etapas" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "funil_id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "nome": "Retorno agendado", "cor": "violet", "posicao": 2 }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "929692a6-7c6e-4f64-988f-e3a786bb5349", "funil_id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "nome": "Orçamento enviado", "cor": "slate", "posicao": 0, "ganha": false, "perdida": false, "negocios": 3, "criado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /etapas/{id} — Ver uma etapa - URL: `GET https://chat.forsi.com.br/api/v1/etapas/{id}` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Etapa` - Página: https://docs.forsi.com.br/api/funil/ver-etapa (Markdown: https://docs.forsi.com.br/api/funil/ver-etapa.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/etapas/929692a6-7c6e-4f64-988f-e3a786bb5349" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Etapa` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /etapas/{id} — Atualizar etapa - URL: `PATCH https://chat.forsi.com.br/api/v1/etapas/{id}` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Etapa` - Página: https://docs.forsi.com.br/api/funil/atualizar-etapa (Markdown: https://docs.forsi.com.br/api/funil/atualizar-etapa.md) Nome, cor, tipo (ganho/perda) e posição. O funil precisa manter pelo menos uma etapa aberta. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | não | até 40 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `ganha` | boolean | não | | | `perdida` | boolean | não | | | `posicao` | integer | não | mín. 0 máx. 30 | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/etapas/929692a6-7c6e-4f64-988f-e3a786bb5349" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"nome":"Orçamento aprovado","cor":"emerald"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Etapa` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /etapas/{id} — Apagar etapa - URL: `DELETE https://chat.forsi.com.br/api/v1/etapas/{id}` - Acesso: escopo `funil:apagar` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `ApagadoFunil` - Página: https://docs.forsi.com.br/api/funil/apagar-etapa (Markdown: https://docs.forsi.com.br/api/funil/apagar-etapa.md) Os negócios da etapa mudam para `destino_id` (ou para a primeira etapa aberta) e voltam a "aberto". Escopo próprio, separado de `funil:escrever`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `destino_id` | uuid | não | Etapa do mesmo funil que recebe os negócios. Sem isso vão para a primeira etapa aberta. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/etapas/929692a6-7c6e-4f64-988f-e3a786bb5349" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"destino_id":"8bd304cb-bbac-4aba-b321-d894b0fe5669"}' ``` **Resposta `200`**: mesmo formato do exemplo de `ApagadoFunil` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /negocios — Listar negócios - URL: `GET https://chat.forsi.com.br/api/v1/negocios` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com lista de `Negocio` (paginada por cursor) - Página: https://docs.forsi.com.br/api/funil/listar-negocios (Markdown: https://docs.forsi.com.br/api/funil/listar-negocios.md) Do mais novo para o mais antigo, paginado por cursor. Filtre por funil, etapa, contato, responsável, estado ou título. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `funil_id` | uuid | não | | | `etapa_id` | uuid | não | | | `contato_id` | uuid | não | | | `responsavel_id` | uuid | não | | | `estado` | "aberto" \| "ganho" \| "perdido" | não | | | `busca` | string | não | Parte do título. até 100 caracteres | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/negocios?funil_id=182aeb59-9ecb-47b2-a745-dfa6f21ea385&estado=aberto&limite=50" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "b04efa09-d26e-496a-a843-fb12f9be4d4d", "funil_id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "etapa_id": "929692a6-7c6e-4f64-988f-e3a786bb5349", "etapa": "Orçamento enviado", "estado": "aberto", "titulo": "Implante superior - Maria", "valor_centavos": 450000, "responsavel_id": "42b8028f-d7ce-4706-9567-44ac3bff4219", "previsao": "2026-10-30", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" }, "ganho_em": "2026-10-04T13:05:12.481Z", "perdido_em": "2026-10-04T13:05:12.481Z", "motivo_perda": "Preço", "posicao": 1024, "origem": "usuario", "notas": 1, "criado_em": "2026-10-04T13:05:12.481Z", "movido_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios — Criar negócio - URL: `POST https://chat.forsi.com.br/api/v1/negocios` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/criar-negocio (Markdown: https://docs.forsi.com.br/api/funil/criar-negocio.md) Mesmo efeito de criar pelo quadro: aparece na tela em tempo real, entra no histórico da conversa e dispara `negocio.criado`. Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `titulo` | string | não | Sem título, usa o nome do contato. até 160 caracteres | | `funil_id` | uuid | não | Sem funil nem etapa, vai para o primeiro funil. | | `etapa_id` | uuid | não | Sem etapa, entra na primeira etapa aberta do funil. | | `contato_id` | uuid | não | | | `conversa_id` | uuid | não | | | `responsavel_id` | uuid | não | Id de um membro da equipe (veja GET /me). | | `valor_centavos` | integer | não | Valor em centavos (R$ 1.500,00 = 150000). mín. 0 máx. 100000000000 | | `previsao` | data AAAA-MM-DD | não | Data prevista de fechamento (AAAA-MM-DD). | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "titulo": "Implante superior - Maria", "funil_id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "valor_centavos": 450000, "previsao": "2026-10-30" }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "b04efa09-d26e-496a-a843-fb12f9be4d4d", "funil_id": "182aeb59-9ecb-47b2-a745-dfa6f21ea385", "etapa_id": "929692a6-7c6e-4f64-988f-e3a786bb5349", "etapa": "Orçamento enviado", "estado": "aberto", "titulo": "Implante superior - Maria", "valor_centavos": 450000, "responsavel_id": "42b8028f-d7ce-4706-9567-44ac3bff4219", "previsao": "2026-10-30", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777" }, "ganho_em": "2026-10-04T13:05:12.481Z", "perdido_em": "2026-10-04T13:05:12.481Z", "motivo_perda": "Preço", "posicao": 1024, "origem": "usuario", "notas": 1, "criado_em": "2026-10-04T13:05:12.481Z", "movido_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /negocios/{id} — Ver um negócio - URL: `GET https://chat.forsi.com.br/api/v1/negocios/{id}` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/ver-negocio (Markdown: https://docs.forsi.com.br/api/funil/ver-negocio.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /negocios/{id} — Atualizar negócio - URL: `PATCH https://chat.forsi.com.br/api/v1/negocios/{id}` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/atualizar-negocio (Markdown: https://docs.forsi.com.br/api/funil/atualizar-negocio.md) Título, valor, responsável, previsão, contato. Para mudar de etapa use /mover, /ganhar ou /perder. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `titulo` | string | não | até 160 caracteres | | `valor_centavos` | integer | não | Valor em centavos (R$ 1.500,00 = 150000). mín. 0 máx. 100000000000 | | `responsavel_id` | uuid ou null | não | | | `contato_id` | uuid ou null | não | | | `previsao` | data AAAA-MM-DD ou null | não | | | `motivo_perda` | string ou null | não | Só vale em negócio perdido. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"valor_centavos":480000,"previsao":"2026-11-15"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /negocios/{id} — Apagar negócio - URL: `DELETE https://chat.forsi.com.br/api/v1/negocios/{id}` - Acesso: escopo `funil:apagar` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `ApagadoFunil` - Página: https://docs.forsi.com.br/api/funil/apagar-negocio (Markdown: https://docs.forsi.com.br/api/funil/apagar-negocio.md) Apaga o negócio e as notas dele. Não tem volta. Escopo próprio, separado de `funil:escrever`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `ApagadoFunil` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios/{id}/mover — Mover para outra etapa - URL: `POST https://chat.forsi.com.br/api/v1/negocios/{id}/mover` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/mover-negocio (Markdown: https://docs.forsi.com.br/api/funil/mover-negocio.md) O mesmo que arrastar no quadro. Cair numa etapa de ganho ou de perda marca o negócio como ganho ou perdido; dispara `negocio.movido` (e `negocio.ganho` / `negocio.perdido`). **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `etapa_id` | uuid | sim | | | `posicao` | number | não | Ordem dentro da etapa (menor = mais acima). Sem isso vai para o fim da coluna. | | `motivo` | string | não | Motivo da perda, se a etapa for de perda. até 300 caracteres | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/mover" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"etapa_id":"8bd304cb-bbac-4aba-b321-d894b0fe5669"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios/{id}/ganhar — Marcar como ganho - URL: `POST https://chat.forsi.com.br/api/v1/negocios/{id}/ganhar` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/ganhar-negocio (Markdown: https://docs.forsi.com.br/api/funil/ganhar-negocio.md) Move para a primeira etapa de ganho do funil. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/ganhar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{}' ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios/{id}/perder — Marcar como perdido - URL: `POST https://chat.forsi.com.br/api/v1/negocios/{id}/perder` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/perder-negocio (Markdown: https://docs.forsi.com.br/api/funil/perder-negocio.md) Move para a primeira etapa de perda do funil. `motivo` é opcional. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `motivo` | string | não | até 300 caracteres | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/perder" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"motivo":"Achou mais barato em outro lugar"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios/{id}/reabrir — Reabrir negócio - URL: `POST https://chat.forsi.com.br/api/v1/negocios/{id}/reabrir` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Negocio` - Página: https://docs.forsi.com.br/api/funil/reabrir-negocio (Markdown: https://docs.forsi.com.br/api/funil/reabrir-negocio.md) Volta para a primeira etapa aberta do funil. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/reabrir" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{}' ``` **Resposta `200`**: mesmo formato do exemplo de `Negocio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /negocios/{id}/notas — Notas de um negócio - URL: `GET https://chat.forsi.com.br/api/v1/negocios/{id}/notas` - Acesso: escopo `funil:ler` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com lista de `NotaNegocio` (paginada por cursor) - Página: https://docs.forsi.com.br/api/funil/listar-notas-negocio (Markdown: https://docs.forsi.com.br/api/funil/listar-notas-negocio.md) Da mais nova para a mais antiga. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | Valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/notas?limite=20" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "68375c45-c8b3-4632-9968-1bd256569205", "negocio_id": "b04efa09-d26e-496a-a843-fb12f9be4d4d", "texto": "Pediu para ligar depois das 18h.", "autor_id": "7aded196-b699-4ef0-b171-2a17b934d051", "autor": "Ana Lima", "criado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /negocios/{id}/notas — Anotar no negócio - URL: `POST https://chat.forsi.com.br/api/v1/negocios/{id}/notas` - Acesso: escopo `funil:escrever` · módulos do plano: API + Funil · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `NotaNegocio` - Página: https://docs.forsi.com.br/api/funil/criar-nota-negocio (Markdown: https://docs.forsi.com.br/api/funil/criar-nota-negocio.md) Até 500 notas por negócio. Mande `Idempotency-Key` para poder repetir com segurança. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `texto` | string | sim | até 4000 caracteres | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/negocios/b04efa09-d26e-496a-a843-fb12f9be4d4d/notas" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"texto":"Pediu para ligar depois das 18h."}' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "68375c45-c8b3-4632-9968-1bd256569205", "negocio_id": "b04efa09-d26e-496a-a843-fb12f9be4d4d", "texto": "Pediu para ligar depois das 18h.", "autor_id": "7aded196-b699-4ef0-b171-2a17b934d051", "autor": "Ana Lima", "criado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /negocios/notas/{id} — Apagar uma nota - URL: `DELETE https://chat.forsi.com.br/api/v1/negocios/notas/{id}` - Acesso: escopo `funil:apagar` · módulos do plano: API + Funil - Sucesso: `200`, `{"dados": ...}` com `ApagadoFunil` - Página: https://docs.forsi.com.br/api/funil/apagar-nota-negocio (Markdown: https://docs.forsi.com.br/api/funil/apagar-nota-negocio.md) O `id` é o da nota. Escopo próprio, separado de `funil:escrever`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/negocios/notas/b8853773-02d6-452a-81d6-86ee539009ed" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `ApagadoFunil` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Agenda Leitura usa `agenda:ler` e escrita usa `agenda:escrever`. A API segue as mesmas regras do app e do agente de IA: horário de trabalho, bloqueios, intervalo e antecedência mínima. O módulo Agenda precisa estar no plano; se o plano perder o módulo, a chave deixa de funcionar na hora. - Datas e horas sempre com o fuso, como `2026-10-12T14:00:00-03:00` (ou `Z`). Sem o deslocamento a API recusa, nunca chuta o fuso. As respostas trazem `inicio` e `fim` em UTC e `inicio_local` e `fim_local` no fuso da agenda. - Veja os horários livres em [`GET /disponibilidade`](https://docs.forsi.com.br/api/agenda/disponibilidade) (janela de até 62 dias) e marque em [`POST /compromissos`](https://docs.forsi.com.br/api/agenda/criar-compromisso). Duas chamadas ao mesmo tempo para o mesmo horário: uma marca e a outra recebe `409 horario_ocupado`, mesmo com muitas chamadas em paralelo. - Sem `agenda_id`, o compromisso vai para a primeira agenda com o horário livre. O contato vem de `contato_id`, `conversa_id` ou `telefone` (só um deles). Pelo telefone, o contato é criado se ainda não existir. - Duração: mande `fim` ou `duracao_min`; sem nenhum, vale a duração padrão da agenda. - Remarcar, cancelar e confirmar têm rota própria. Compromisso cancelado não volta: marque um novo. Cancelar duas vezes é seguro. `PATCH` marca presença (`estado`: `concluido` ou `faltou`) e edita título, local, link e notas. - Eventos de webhook: `agendamento.criado`, `agendamento.remarcado` (traz também `anterior`), `agendamento.cancelado` e `agendamento.confirmado`. Disparam para qualquer origem: app, agente de IA ou API. - Fora da API de propósito: apagar agenda, apagar compromisso (só cancelar), encaixe fora do horário, operações em massa e trocar o profissional de uma agenda. Apagar bloqueio existe, um por chamada. - Não existe cadastro de serviços: cada agenda tem a sua duração padrão (`duracao_min`). ```json title="Com contato que já existe e fim explícito" { "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "inicio": "2026-10-12T14:00:00-03:00", "fim": "2026-10-12T14:45:00-03:00", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "titulo": "Avaliação", "local": "Rua das Flores, 120 - sala 4" } ``` **Códigos de erro comuns nesta seção**: `horario_invalido`, `janela_grande`, `duracao_invalida`, `campo_longo`, `campo_invalido`, `link_invalido`, `contato_ambiguo`, `conversa_de_outro_contato`, `evento_cancelado`, `evento_finalizado`, `horario_ocupado`, `horario_bloqueado`, `fora_do_horario`, `antecedencia`, `compromisso_estado_invalido`, `nada_para_atualizar`, `fuso_invalido`, `cor_invalida`, `lembretes_invalidos`, `usuario_inexistente`, `agenda_inexistente`, `evento_inexistente`, `bloqueio_inexistente`, `agenda_repetida`, `limite_agendas`, `limite_bloqueios`, `bloqueio_tipo_invalido`, `repeticao_invalida`. ### GET /agendas — Listar agendas - URL: `GET https://chat.forsi.com.br/api/v1/agendas` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com lista de `Agenda` (paginada por cursor) - Página: https://docs.forsi.com.br/api/agenda/listar-agendas (Markdown: https://docs.forsi.com.br/api/agenda/listar-agendas.md) As agendas (uma por profissional ou sala), com horário de trabalho, duração padrão e fuso. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | | `ativa` | "true" \| "false" | não | Só as ativas (`true`) ou só as inativas (`false`). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/agendas?ativa=true" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "nome": "Dra. Ana Lima", "cor": "blue", "fuso": "America/Sao_Paulo", "ativa": true, "duracao_min": 30, "intervalo_min": 10, "antecedencia_min": 120, "horario": {"1":[["09:00","12:00"],["14:00","18:00"]]}, "lembretes": [{"antes_min":1440}], "mensagem_lembrete": "Oi {nome}, lembrando da sua consulta {data} às {hora}.", "profissional": {"id":"27a50fb4-0f76-4c9a-92d0-c2d984d356b9","nome":"Maria Souza"}, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /agendas — Criar agenda - URL: `POST https://chat.forsi.com.br/api/v1/agendas` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Agenda` - Página: https://docs.forsi.com.br/api/agenda/criar-agenda (Markdown: https://docs.forsi.com.br/api/agenda/criar-agenda.md) Até 100 agendas por organização. Mande `Idempotency-Key` para poder repetir com segurança. Apagar agenda não existe na API. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | até 80 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `fuso` | string | não | Fuso IANA, ex.: America/Sao_Paulo. Padrão: o da organização. até 64 caracteres | | `horario` | objeto livre | não | Horário de trabalho: dia da semana (0 domingo a 6 sábado) para faixas [início, fim], ex.: {"1": [["09:00","12:00"],["14:00","18:00"]]}. | | `duracao_min` | integer | não | Duração padrão de cada compromisso, em minutos (5 a 720). | | `intervalo_min` | integer | não | Folga entre compromissos, em minutos (0 a 240). | | `antecedencia_min` | integer | não | Antecedência mínima para marcar, em minutos. mín. 0 máx. 43200 | | `lembretes` | lista de object | não | Lembretes ao contato: até 5, cada um com `antes_min` (minutos antes do compromisso). | | `lembretes[].antes_min` | integer | sim | mín. 1 máx. 43200 | | `mensagem_lembrete` | string | não | até 1000 caracteres | | `ativa` | boolean | não | | | `usuario_id` | uuid | não | Profissional dono da agenda (precisa ser da equipe). Só na criação; não dá para trocar depois pela API. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/agendas" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "Dra. Ana Lima", "cor": "blue", "fuso": "America/Sao_Paulo", "duracao_min": 30, "intervalo_min": 10, "antecedencia_min": 120, "horario": { "1": [["09:00","12:00"],["14:00","18:00"]], "3": [["09:00","12:00"],["14:00","18:00"]], "5": [["09:00","13:00"]] }, "lembretes": [{"antes_min":1440},{"antes_min":120}] }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "nome": "Dra. Ana Lima", "cor": "blue", "fuso": "America/Sao_Paulo", "ativa": true, "duracao_min": 30, "intervalo_min": 10, "antecedencia_min": 120, "horario": {"1":[["09:00","12:00"],["14:00","18:00"]]}, "lembretes": [{"antes_min":1440}], "mensagem_lembrete": "Oi {nome}, lembrando da sua consulta {data} às {hora}.", "profissional": {"id":"27a50fb4-0f76-4c9a-92d0-c2d984d356b9","nome":"Maria Souza"}, "criado_em": "2026-10-04T13:05:12.481Z", "atualizado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /agendas/{id} — Ver uma agenda - URL: `GET https://chat.forsi.com.br/api/v1/agendas/{id}` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Agenda` - Página: https://docs.forsi.com.br/api/agenda/ver-agenda (Markdown: https://docs.forsi.com.br/api/agenda/ver-agenda.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/agendas/f7233bb7-10b6-49d2-aa3b-8c2e7d88064d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Agenda` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /agendas/{id} — Atualizar agenda - URL: `PATCH https://chat.forsi.com.br/api/v1/agendas/{id}` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Agenda` - Página: https://docs.forsi.com.br/api/agenda/atualizar-agenda (Markdown: https://docs.forsi.com.br/api/agenda/atualizar-agenda.md) Horário, duração, intervalo, antecedência, fuso, lembretes, cor, nome e `ativa`. Para tirar uma agenda do ar, use `ativa: false`. Trocar o profissional só pela tela. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | não | até 80 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `fuso` | string | não | Fuso IANA, ex.: America/Sao_Paulo. Padrão: o da organização. até 64 caracteres | | `horario` | objeto livre | não | Horário de trabalho: dia da semana (0 domingo a 6 sábado) para faixas [início, fim], ex.: {"1": [["09:00","12:00"],["14:00","18:00"]]}. | | `duracao_min` | integer | não | Duração padrão de cada compromisso, em minutos (5 a 720). | | `intervalo_min` | integer | não | Folga entre compromissos, em minutos (0 a 240). | | `antecedencia_min` | integer | não | Antecedência mínima para marcar, em minutos. mín. 0 máx. 43200 | | `lembretes` | lista de object | não | Lembretes ao contato: até 5, cada um com `antes_min` (minutos antes do compromisso). | | `lembretes[].antes_min` | integer | sim | mín. 1 máx. 43200 | | `mensagem_lembrete` | string | não | até 1000 caracteres | | `ativa` | boolean | não | | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/agendas/f7233bb7-10b6-49d2-aa3b-8c2e7d88064d" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"duracao_min":45,"antecedencia_min":60}' ``` **Resposta `200`**: mesmo formato do exemplo de `Agenda` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /disponibilidade — Horários livres - URL: `GET https://chat.forsi.com.br/api/v1/disponibilidade` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Disponibilidade` - Página: https://docs.forsi.com.br/api/agenda/disponibilidade (Markdown: https://docs.forsi.com.br/api/agenda/disponibilidade.md) Os mesmos horários que o agente de IA oferece: respeita o horário de trabalho, bloqueios, compromissos, intervalo e antecedência mínima. Cada horário traz `inicio`/`fim` em UTC e `inicio_local`/`fim_local` no fuso da agenda. Até 400 horários por consulta. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data e hora ISO 8601 | não | Começo da janela (com fuso). | | `ate` | data e hora ISO 8601 | não | Fim da janela (com fuso). A janela vai até 62 dias. | | `agenda_id` | uuid | não | Só esta agenda. Sem ela, todas as agendas ativas. | | `duracao_min` | integer | não | Duração do compromisso, em minutos. Padrão: a da agenda. mín. 5 máx. 720 | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/disponibilidade?agenda_id=f7233bb7-10b6-49d2-aa3b-8c2e7d88064d&de=2026-10-12T00:00:00-03:00&ate=2026-10-17T00:00:00-03:00&duracao_min=30" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":{"fuso":"America/Sao_Paulo","duracao_min":30,"slots":[{}],"truncado":false}} ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /compromissos — Listar compromissos - URL: `GET https://chat.forsi.com.br/api/v1/compromissos` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com lista de `Compromisso` (paginada por cursor) - Página: https://docs.forsi.com.br/api/agenda/listar-compromissos (Markdown: https://docs.forsi.com.br/api/agenda/listar-compromissos.md) Em ordem de horário, paginado por cursor. Filtre por agenda, contato, estado e período. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | | `agenda_id` | uuid | não | | | `contato_id` | uuid | não | | | `estado` | "marcado" \| "confirmado" \| "concluido" \| "faltou" \| "cancelado" | não | | | `de` | data e hora ISO 8601 | não | Só compromissos que terminam depois disto. | | `ate` | data e hora ISO 8601 | não | Só compromissos que começam antes disto. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/compromissos?de=2026-10-12T00:00:00-03:00&ate=2026-10-19T00:00:00-03:00&estado=marcado" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "e0c6d7e8-baef-444c-acaf-8ea96e737165", "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "agenda": "Dra. Ana Lima", "cor": "blue", "titulo": "Consulta de retorno", "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "fuso": "America/Sao_Paulo", "inicio_local": "2026-10-12T14:00:00-03:00", "fim_local": "2026-10-12T14:30:00-03:00", "duracao_min": 30, "estado": "marcado", "local": "Rua das Flores, 120 - sala 4", "link": "https://meet.google.com/abc-defg-hij", "notas": "Trazer os exames.", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777", "avatar_url": "https://chat.forsi.com.br/avatar.png" }, "lembretes": [ { "antes_min": 1440, "estado": "texto", "enviar_em": "2026-10-04T13:05:12.481Z", "enviado_em": "2026-10-04T13:05:12.481Z" } ], "criado_por_tipo": "texto", "criado_em": "2026-10-04T13:05:12.481Z", "confirmado_em": "2026-10-04T13:05:12.481Z", "cancelado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /compromissos — Marcar compromisso - URL: `POST https://chat.forsi.com.br/api/v1/compromissos` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/criar-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/criar-compromisso.md) Mesmas regras do app e do agente: horário de trabalho, bloqueios, intervalo e antecedência mínima. Horário ocupado devolve 409, mesmo com várias chamadas ao mesmo tempo. O contato vem de `contato_id`, `conversa_id` ou `telefone` (um só). Não existe `forcar`: encaixe fora das regras só pela tela. Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `inicio` | data e hora ISO 8601 | sim | Começo do compromisso (com fuso). | | `fim` | data e hora ISO 8601 | não | Fim do compromisso (com fuso, até 12 horas depois do início). | | `duracao_min` | integer | não | Duração em minutos (5 a 720). | | `agenda_id` | uuid | não | Sem ela, vale a primeira agenda com o horário livre (como o agente faz). | | `titulo` | string | não | até 160 caracteres | | `local` | string | não | até 200 caracteres | | `link` | string | não | Link da chamada ou reunião (https). até 500 caracteres | | `notas` | string | não | até 2000 caracteres | | `contato_id` | uuid | não | | | `conversa_id` | uuid | não | | | `telefone` | string | não | Acha o contato pelo telefone (ou cria um). Não combine com `contato_id` nem `conversa_id`. mín. 8 caracteres até 30 caracteres | | `nome` | string | não | Nome do contato novo, quando criado pelo `telefone`. até 120 caracteres | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/compromissos" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "inicio": "2026-10-12T14:00:00-03:00", "duracao_min": 30, "telefone": "+55 31 98888-7777", "nome": "Maria Souza", "titulo": "Consulta de retorno" }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "e0c6d7e8-baef-444c-acaf-8ea96e737165", "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "agenda": "Dra. Ana Lima", "cor": "blue", "titulo": "Consulta de retorno", "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "fuso": "America/Sao_Paulo", "inicio_local": "2026-10-12T14:00:00-03:00", "fim_local": "2026-10-12T14:30:00-03:00", "duracao_min": 30, "estado": "marcado", "local": "Rua das Flores, 120 - sala 4", "link": "https://meet.google.com/abc-defg-hij", "notas": "Trazer os exames.", "contato_id": "08054a7f-a752-481e-9915-cf4cf944df39", "conversa_id": "495a7cae-128c-4b66-adbc-807b3959c129", "contato": { "id": "08054a7f-a752-481e-9915-cf4cf944df39", "nome": "Maria Souza", "telefone": "+5531988887777", "avatar_url": "https://chat.forsi.com.br/avatar.png" }, "lembretes": [ { "antes_min": 1440, "estado": "texto", "enviar_em": "2026-10-04T13:05:12.481Z", "enviado_em": "2026-10-04T13:05:12.481Z" } ], "criado_por_tipo": "texto", "criado_em": "2026-10-04T13:05:12.481Z", "confirmado_em": "2026-10-04T13:05:12.481Z", "cancelado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /compromissos/{id} — Ver um compromisso - URL: `GET https://chat.forsi.com.br/api/v1/compromissos/{id}` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/ver-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/ver-compromisso.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/compromissos/e0c6d7e8-baef-444c-acaf-8ea96e737165" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Compromisso` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /compromissos/{id} — Atualizar compromisso - URL: `PATCH https://chat.forsi.com.br/api/v1/compromissos/{id}` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/atualizar-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/atualizar-compromisso.md) Título, notas, local, link, contato e `estado` (só `concluido` ou `faltou`, para marcar comparecimento). Horário: use remarcar. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `titulo` | string ou null | não | | | `local` | string ou null | não | | | `link` | string ou null | não | | | `notas` | string ou null | não | | | `contato_id` | uuid ou null | não | | | `estado` | "concluido" \| "faltou" | não | Só para encerrar: `concluido` (compareceu) ou `faltou`. Confirmar e cancelar têm rota própria. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/compromissos/e0c6d7e8-baef-444c-acaf-8ea96e737165" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"notas":"Trazer os exames de sangue."}' ``` **Resposta `200`**: mesmo formato do exemplo de `Compromisso` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /compromissos/{id}/remarcar — Remarcar compromisso - URL: `POST https://chat.forsi.com.br/api/v1/compromissos/{id}/remarcar` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/remarcar-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/remarcar-compromisso.md) Só compromissos marcados ou confirmados. Volta para `marcado` e dispara `agendamento.remarcado`. Cancelado ou encerrado devolve 409. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `inicio` | data e hora ISO 8601 | sim | Novo começo (com fuso). | | `fim` | data e hora ISO 8601 | não | Novo fim (com fuso). | | `duracao_min` | integer | não | Duração em minutos (5 a 720). | | `agenda_id` | uuid | não | Mudar de agenda junto com o horário. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/compromissos/e0c6d7e8-baef-444c-acaf-8ea96e737165/remarcar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"inicio":"2026-10-13T09:30:00-03:00"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Compromisso` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /compromissos/{id}/cancelar — Cancelar compromisso - URL: `POST https://chat.forsi.com.br/api/v1/compromissos/{id}/cancelar` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/cancelar-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/cancelar-compromisso.md) Libera o horário. Repetir o cancelamento é seguro. Não apaga o registro. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/compromissos/e0c6d7e8-baef-444c-acaf-8ea96e737165/cancelar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{}' ``` **Resposta `200`**: mesmo formato do exemplo de `Compromisso` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /compromissos/{id}/confirmar — Confirmar compromisso - URL: `POST https://chat.forsi.com.br/api/v1/compromissos/{id}/confirmar` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Compromisso` - Página: https://docs.forsi.com.br/api/agenda/confirmar-compromisso (Markdown: https://docs.forsi.com.br/api/agenda/confirmar-compromisso.md) O contato confirmou presença. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** Sem campos: mande `{}` ou nenhum corpo. **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/compromissos/e0c6d7e8-baef-444c-acaf-8ea96e737165/confirmar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{}' ``` **Resposta `200`**: mesmo formato do exemplo de `Compromisso` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /bloqueios — Listar bloqueios - URL: `GET https://chat.forsi.com.br/api/v1/bloqueios` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com lista de `Bloqueio` (paginada por cursor) - Página: https://docs.forsi.com.br/api/agenda/listar-bloqueios (Markdown: https://docs.forsi.com.br/api/agenda/listar-bloqueios.md) Folgas, almoços e feriados que fecham a agenda. Os que repetem aparecem uma vez, com a regra de repetição. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | | `agenda_id` | uuid | não | | | `de` | data e hora ISO 8601 | não | Só bloqueios que ainda valem a partir daqui. | | `ate` | data e hora ISO 8601 | não | Só bloqueios que começam antes disto. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/bloqueios?agenda_id=f7233bb7-10b6-49d2-aa3b-8c2e7d88064d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "a27653af-7bd5-4506-af22-674660419819", "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "titulo": "Almoço", "tipo": "folga", "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "fuso": "America/Sao_Paulo", "inicio_local": "2026-10-12T14:00:00-03:00", "fim_local": "2026-10-12T14:30:00-03:00", "repetir": "nunca", "repetir_ate": "2026-10-12", "criado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /bloqueios — Criar bloqueio - URL: `POST https://chat.forsi.com.br/api/v1/bloqueios` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Bloqueio` - Página: https://docs.forsi.com.br/api/agenda/criar-bloqueio (Markdown: https://docs.forsi.com.br/api/agenda/criar-bloqueio.md) Até 500 bloqueios por organização. Não cancela compromissos que já estão marcados nesse horário. Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `inicio` | data e hora ISO 8601 | sim | Começo do bloqueio (com fuso). | | `fim` | data e hora ISO 8601 | sim | Fim do bloqueio (com fuso, até 366 dias depois do início). | | `agenda_id` | uuid ou null | não | Agenda do bloqueio. Vazio = vale para todas as agendas. | | `titulo` | string | não | até 120 caracteres | | `tipo` | "folga" \| "almoco" \| "feriado" \| "outro" | não | | | `repetir` | "nunca" \| "diario" \| "semanal" \| "anual" | não | | | `repetir_ate` | data AAAA-MM-DD ou null | não | Último dia da repetição (AAAA-MM-DD). Só com `repetir` diferente de `nunca`. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/bloqueios" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "inicio": "2026-10-12T12:00:00-03:00", "fim": "2026-10-12T13:00:00-03:00", "titulo": "Almoço", "tipo": "almoco", "repetir": "diario" }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "a27653af-7bd5-4506-af22-674660419819", "agenda_id": "f7233bb7-10b6-49d2-aa3b-8c2e7d88064d", "titulo": "Almoço", "tipo": "folga", "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "fuso": "America/Sao_Paulo", "inicio_local": "2026-10-12T14:00:00-03:00", "fim_local": "2026-10-12T14:30:00-03:00", "repetir": "nunca", "repetir_ate": "2026-10-12", "criado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /bloqueios/{id} — Ver um bloqueio - URL: `GET https://chat.forsi.com.br/api/v1/bloqueios/{id}` - Acesso: escopo `agenda:ler` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Bloqueio` - Página: https://docs.forsi.com.br/api/agenda/ver-bloqueio (Markdown: https://docs.forsi.com.br/api/agenda/ver-bloqueio.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/bloqueios/a27653af-7bd5-4506-af22-674660419819" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Bloqueio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /bloqueios/{id} — Atualizar bloqueio - URL: `PATCH https://chat.forsi.com.br/api/v1/bloqueios/{id}` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Bloqueio` - Página: https://docs.forsi.com.br/api/agenda/atualizar-bloqueio (Markdown: https://docs.forsi.com.br/api/agenda/atualizar-bloqueio.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `inicio` | data e hora ISO 8601 | não | Começo do bloqueio (com fuso). | | `fim` | data e hora ISO 8601 | não | Fim do bloqueio (com fuso, até 366 dias depois do início). | | `agenda_id` | uuid ou null | não | Agenda do bloqueio. Vazio = vale para todas as agendas. | | `titulo` | string | não | até 120 caracteres | | `tipo` | "folga" \| "almoco" \| "feriado" \| "outro" | não | | | `repetir` | "nunca" \| "diario" \| "semanal" \| "anual" | não | | | `repetir_ate` | data AAAA-MM-DD ou null | não | Último dia da repetição (AAAA-MM-DD). Só com `repetir` diferente de `nunca`. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/bloqueios/a27653af-7bd5-4506-af22-674660419819" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"fim":"2026-10-12T13:30:00-03:00"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Bloqueio` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /bloqueios/{id} — Apagar bloqueio - URL: `DELETE https://chat.forsi.com.br/api/v1/bloqueios/{id}` - Acesso: escopo `agenda:escrever` · módulos do plano: API + Agenda - Sucesso: `200`, `{"dados": ...}` com `Apagado` - Página: https://docs.forsi.com.br/api/agenda/apagar-bloqueio (Markdown: https://docs.forsi.com.br/api/agenda/apagar-bloqueio.md) Apaga um bloqueio (um por chamada). Só o bloqueio: agendas e compromissos não se apagam pela API. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/bloqueios/a27653af-7bd5-4506-af22-674660419819" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Apagado` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Equipe e permissões São três permissões separadas: `equipe:ler` (pessoas, presença, convites abertos e equipes), `equipe:escrever` (equipes e atribuição de conversas) e `equipe:permissoes` (convidar, mudar permissões e remover). A última é sensível: o botão "Tudo" da tela de chaves não a marca, você marca de propósito. - A chave nunca tem mais poder que um gestor e só mexe em **atendentes**. Dono e gestor aparecem nas listas, mas mudar, remover ou convidar alguém como dono ou gestor dá `403 sem_permissao`, mesmo que a chave tenha sido criada por um dono. - O papel (atendente, gestor, dono) nunca muda pela API. Dá para mudar o papel de acesso, os WhatsApps visíveis, os módulos e a agenda de um atendente. - Convite: a resposta traz o `link` uma única vez e o e-mail sai como na tela (mande `enviar_email: false` para só pegar o link). Vale 7 dias e conta no limite de acessos do plano. Até 20 convites por dia por organização e 3 por dia para o mesmo e-mail. Reenviar troca o link: o antigo para de valer. - Não existe desativar: remover tira o acesso na hora e as conversas da pessoa ficam sem responsável. Para voltar, convide de novo. - Atribuir conversa tem o mesmo efeito da tela: fica no histórico da conversa com o nome da chave, a pessoa recebe o aviso, o agente de IA pausa naquela conversa e sai o webhook `conversa.atribuida`. - Eventos de webhook: `membro.convidado`, `membro.entrou`, `membro.alterado` (traz também `anterior`) e `membro.removido`. - Toda mudança fica na auditoria da organização com o nome da chave. Ids de outra organização dão `404`. - Fora da API de propósito: mudar papel, qualquer coisa com dono ou gestor, transferir a organização, plano e cobrança, horário e presença de cada pessoa (só leitura) e papéis de acesso (só pelo app). **Códigos de erro comuns nesta seção**: `sem_permissao`, `ja_e_membro`, `limite_de_acessos`, `limite_de_convites`, `membro_inexistente`, `convite_inexistente`, `equipe_inexistente`, `equipe_repetida`, `papel_inexistente`, `modulos_do_papel`, `modulo_invalido`, `conversa_inexistente`. ### GET /equipe/membros — Listar pessoas da equipe - URL: `GET https://chat.forsi.com.br/api/v1/equipe/membros` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Membro` (paginada por cursor) - Página: https://docs.forsi.com.br/api/equipe/listar-membros (Markdown: https://docs.forsi.com.br/api/equipe/listar-membros.md) Por ordem de entrada, paginado por cursor. Filtre por papel ou pelo e-mail exato. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `papel` | "dono" \| "gestor" \| "atendente" | não | | | `email` | string | não | E-mail exato de alguém DESTA organização. até 254 caracteres | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/membros?papel=atendente" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "0e584d55-e895-4372-8945-cb64cafc6efd", "nome": "Ana Lima", "email": "ana@exemplo.com.br", "avatar_url": "https://chat.forsi.com.br/avatar.png", "papel": "dono", "papel_de_acesso_id": "646adc88-610e-4604-96fe-af5175dca375", "permissoes": {"caixas":["7a9d53e9-9d1b-4122-923b-fac67958f445"],"modulos":["texto"],"agenda":"todas"}, "equipes": ["55174f08-dda3-49e6-bc5d-9309becdaf6d"], "disponivel": true, "disponivel_agora": true, "ausente_ate": "2026-10-04T13:05:12.481Z", "horario": {"dias":[1,2,3,4,5],"inicio":"08:00","fim":"18:00"}, "ultimo_acesso": "2026-10-04T13:05:12.481Z", "entrou_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /equipe/membros/{id} — Ver uma pessoa - URL: `GET https://chat.forsi.com.br/api/v1/equipe/membros/{id}` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Membro` - Página: https://docs.forsi.com.br/api/equipe/ver-membro (Markdown: https://docs.forsi.com.br/api/equipe/ver-membro.md) O `id` é o id da pessoa (usuário). **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/membros/0e584d55-e895-4372-8945-cb64cafc6efd" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "id": "0e584d55-e895-4372-8945-cb64cafc6efd", "nome": "Ana Lima", "email": "ana@exemplo.com.br", "avatar_url": "https://chat.forsi.com.br/avatar.png", "papel": "dono", "papel_de_acesso_id": "646adc88-610e-4604-96fe-af5175dca375", "permissoes": {"caixas":["7a9d53e9-9d1b-4122-923b-fac67958f445"],"modulos":["texto"],"agenda":"todas"}, "equipes": ["55174f08-dda3-49e6-bc5d-9309becdaf6d"], "disponivel": true, "disponivel_agora": true, "ausente_ate": "2026-10-04T13:05:12.481Z", "horario": {"dias":[1,2,3,4,5],"inicio":"08:00","fim":"18:00"}, "ultimo_acesso": "2026-10-04T13:05:12.481Z", "entrou_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /equipe/membros/{id} — Mudar permissões de um atendente - URL: `PATCH https://chat.forsi.com.br/api/v1/equipe/membros/{id}` - Acesso: escopo `equipe:permissoes` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Membro` - Página: https://docs.forsi.com.br/api/equipe/atualizar-membro (Markdown: https://docs.forsi.com.br/api/equipe/atualizar-membro.md) Papel de acesso, WhatsApps visíveis, módulos e agenda. Só os campos enviados mudam. Só vale para atendentes: dono e gestor (e contas de suporte da FORSI) dão 403 `sem_permissao` e só mudam pelo app. O papel (atendente, gestor, dono) nunca muda pela API. Dispara `membro.alterado`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `papel_de_acesso_id` | uuid ou null | não | Papel de acesso (Configurações → Equipe → Papéis). Com papel, os módulos e a agenda vêm dele. null tira o papel. | | `caixas` | lista de uuid | não | WhatsApps que a pessoa vê. Lista vazia = todos. até 200 itens | | `modulos` | lista de "conversas" \| "contatos" \| "automacoes" \| "agentes" \| "funil" \| "agenda" \| "followup" \| "relatorios" | não | Módulos liberados. Só sem papel de acesso. até 8 itens | | `agenda` | "todas" \| "propria" | não | Agenda: todas ou só a própria. Só sem papel de acesso. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/equipe/membros/0e584d55-e895-4372-8945-cb64cafc6efd" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"modulos":["conversas","contatos","agenda"],"agenda":"propria"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Membro` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /equipe/membros/{id} — Tirar um atendente da equipe - URL: `DELETE https://chat.forsi.com.br/api/v1/equipe/membros/{id}` - Acesso: escopo `equipe:permissoes` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `ApagadoEquipe` - Página: https://docs.forsi.com.br/api/equipe/remover-membro (Markdown: https://docs.forsi.com.br/api/equipe/remover-membro.md) A pessoa perde o acesso na hora e as conversas dela ficam sem responsável. Só vale para atendentes: dono e gestor (e contas de suporte da FORSI) dão 403 `sem_permissao` e só mudam pelo app. Para voltar, convide de novo. Dispara `membro.removido`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/equipe/membros/0e584d55-e895-4372-8945-cb64cafc6efd" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":{"id":"44ccbf83-e9cc-43ae-a406-c7d8c1550e81","apagado":false}} ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /equipe/presenca — Quem está disponível - URL: `GET https://chat.forsi.com.br/api/v1/equipe/presenca` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Presenca` (paginada por cursor) - Página: https://docs.forsi.com.br/api/equipe/listar-presenca (Markdown: https://docs.forsi.com.br/api/equipe/listar-presenca.md) Disponível agora = marcou disponível, não está ausente e está dentro do horário de trabalho (no fuso da organização). **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `equipe_id` | uuid | não | Só quem está nesta equipe. | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/presenca?equipe_id=55174f08-dda3-49e6-bc5d-9309becdaf6d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "9d77cca2-2aa9-45d6-aef4-707f6b6ee109", "nome": "Ana Lima", "papel": "dono", "disponivel": true, "disponivel_agora": true, "ausente_ate": "2026-10-04T13:05:12.481Z", "horario": {"dias":[1,2,3,4,5],"inicio":"08:00","fim":"18:00"}, "ultimo_acesso": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /equipe/convites — Listar convites abertos - URL: `GET https://chat.forsi.com.br/api/v1/equipe/convites` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Convite` (paginada por cursor) - Página: https://docs.forsi.com.br/api/equipe/listar-convites (Markdown: https://docs.forsi.com.br/api/equipe/listar-convites.md) Só os que ainda não foram aceitos nem venceram, do mais novo para o mais antigo. O link do convite nunca aparece aqui. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/convites" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "fac480ff-6cc4-47be-9a87-6270d519a659", "email": "nova.atendente@exemplo.com.br", "papel": "dono", "papel_de_acesso_id": "646adc88-610e-4604-96fe-af5175dca375", "convidado_por": "Ana Lima", "expira_em": "2026-10-04T13:05:12.481Z", "criado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /equipe/convites — Convidar atendente - URL: `POST https://chat.forsi.com.br/api/v1/equipe/convites` - Acesso: escopo `equipe:permissoes` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `ConviteComLink` - Página: https://docs.forsi.com.br/api/equipe/criar-convite (Markdown: https://docs.forsi.com.br/api/equipe/criar-convite.md) Manda o e-mail de convite (como a tela) e responde com o `link` UMA vez. Vale 7 dias. Só atendente. Conta no limite de acessos do plano. Até 20 convites por dia por organização e 3 por e-mail (429 `limite_de_convites`). Repetir com a mesma `Idempotency-Key` devolve o convite sem `link` e sem mandar outro e-mail. Dispara `membro.convidado`. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `email` | string | sim | até 254 caracteres | | `papel` | "atendente" | não | Sempre `atendente` pela API. | | `papel_de_acesso_id` | uuid | não | Papel de acesso aplicado quando a pessoa aceitar. | | `enviar_email` | boolean | não | false = não manda o e-mail; use o `link` da resposta. Padrão: manda. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/equipe/convites" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"email":"nova.atendente@exemplo.com.br","enviar_email":true}' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "fac480ff-6cc4-47be-9a87-6270d519a659", "email": "nova.atendente@exemplo.com.br", "papel": "dono", "papel_de_acesso_id": "646adc88-610e-4604-96fe-af5175dca375", "convidado_por": "Ana Lima", "expira_em": "2026-10-04T13:05:12.481Z", "criado_em": "2026-10-04T13:05:12.481Z", "link": "https://chat.forsi.com.br/convite/EXEMPLO" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /equipe/convites/{id} — Cancelar convite - URL: `DELETE https://chat.forsi.com.br/api/v1/equipe/convites/{id}` - Acesso: escopo `equipe:permissoes` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `ApagadoEquipe` - Página: https://docs.forsi.com.br/api/equipe/cancelar-convite (Markdown: https://docs.forsi.com.br/api/equipe/cancelar-convite.md) O link deixa de valer na hora. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/equipe/convites/fac480ff-6cc4-47be-9a87-6270d519a659" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `ApagadoEquipe` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /equipe/convites/{id}/reenviar — Reenviar convite - URL: `POST https://chat.forsi.com.br/api/v1/equipe/convites/{id}/reenviar` - Acesso: escopo `equipe:permissoes` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `ConviteComLink` - Página: https://docs.forsi.com.br/api/equipe/reenviar-convite (Markdown: https://docs.forsi.com.br/api/equipe/reenviar-convite.md) Gera um link novo (o antigo para de valer), renova o prazo de 7 dias e manda o e-mail de novo. Conta na mesma cota diária da criação. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON, opcional)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `enviar_email` | boolean | não | false = só gera o link novo. Padrão: manda o e-mail. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/equipe/convites/fac480ff-6cc4-47be-9a87-6270d519a659/reenviar" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"enviar_email":false}' ``` **Resposta `200`**: mesmo formato do exemplo de `ConviteComLink` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /equipe/equipes — Listar equipes (setores) - URL: `GET https://chat.forsi.com.br/api/v1/equipe/equipes` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com lista de `Equipe` (paginada por cursor) - Página: https://docs.forsi.com.br/api/equipe/listar-equipes (Markdown: https://docs.forsi.com.br/api/equipe/listar-equipes.md) Cada equipe vem com os ids das pessoas dela. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/equipes" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "55174f08-dda3-49e6-bc5d-9309becdaf6d", "nome": "Recepção", "cor": "blue", "distribuicao": "manual", "ordem": 12, "membros": ["0e584d55-e895-4372-8945-cb64cafc6efd"], "criado_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /equipe/equipes — Criar equipe - URL: `POST https://chat.forsi.com.br/api/v1/equipe/equipes` - Acesso: escopo `equipe:escrever` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `Equipe` - Página: https://docs.forsi.com.br/api/equipe/criar-equipe (Markdown: https://docs.forsi.com.br/api/equipe/criar-equipe.md) Nome único na organização (409 `equipe_repetida`). Mande `Idempotency-Key` para poder repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | até 40 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `distribuicao` | "manual" \| "rodizio" \| "menos_ocupado" | não | Como as conversas novas da equipe são distribuídas: manual, rodízio ou para quem tem menos conversas abertas. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/equipe/equipes" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"nome":"Recepção","cor":"emerald","distribuicao":"rodizio"}' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "55174f08-dda3-49e6-bc5d-9309becdaf6d", "nome": "Recepção", "cor": "blue", "distribuicao": "manual", "ordem": 12, "membros": ["0e584d55-e895-4372-8945-cb64cafc6efd"], "criado_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /equipe/equipes/{id} — Ver uma equipe - URL: `GET https://chat.forsi.com.br/api/v1/equipe/equipes/{id}` - Acesso: escopo `equipe:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Equipe` - Página: https://docs.forsi.com.br/api/equipe/ver-equipe (Markdown: https://docs.forsi.com.br/api/equipe/ver-equipe.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/equipe/equipes/55174f08-dda3-49e6-bc5d-9309becdaf6d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `Equipe` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /equipe/equipes/{id} — Atualizar equipe - URL: `PATCH https://chat.forsi.com.br/api/v1/equipe/equipes/{id}` - Acesso: escopo `equipe:escrever` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Equipe` - Página: https://docs.forsi.com.br/api/equipe/atualizar-equipe (Markdown: https://docs.forsi.com.br/api/equipe/atualizar-equipe.md) Nome, cor e distribuição. Só os campos enviados mudam. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | não | até 40 caracteres | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `distribuicao` | "manual" \| "rodizio" \| "menos_ocupado" | não | Como as conversas novas da equipe são distribuídas: manual, rodízio ou para quem tem menos conversas abertas. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/equipe/equipes/55174f08-dda3-49e6-bc5d-9309becdaf6d" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{"distribuicao":"menos_ocupado"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Equipe` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /equipe/equipes/{id} — Apagar equipe - URL: `DELETE https://chat.forsi.com.br/api/v1/equipe/equipes/{id}` - Acesso: escopo `equipe:escrever` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `ApagadoEquipe` - Página: https://docs.forsi.com.br/api/equipe/apagar-equipe (Markdown: https://docs.forsi.com.br/api/equipe/apagar-equipe.md) As pessoas continuam na organização; as conversas da equipe ficam sem equipe. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/equipe/equipes/55174f08-dda3-49e6-bc5d-9309becdaf6d" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `ApagadoEquipe` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /equipe/equipes/{id}/membros — Pôr e tirar pessoas da equipe - URL: `PATCH https://chat.forsi.com.br/api/v1/equipe/equipes/{id}/membros` - Acesso: escopo `equipe:escrever` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `Equipe` - Página: https://docs.forsi.com.br/api/equipe/atualizar-membros-equipe (Markdown: https://docs.forsi.com.br/api/equipe/atualizar-membros-equipe.md) Até 50 em cada lista. Qualquer pessoa da organização pode estar numa equipe: isso não muda o que ela pode ver. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `adicionar` | lista de uuid | não | Ids de pessoas (GET /equipe/membros) para pôr na equipe. até 50 itens | | `remover` | lista de uuid | não | Ids de pessoas para tirar da equipe. até 50 itens | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/equipe/equipes/55174f08-dda3-49e6-bc5d-9309becdaf6d/membros" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -d '{ "adicionar": ["0e584d55-e895-4372-8945-cb64cafc6efd"], "remover": ["8e5f909e-42a4-455e-801f-328bf2e7acf1"] }' ``` **Resposta `200`**: mesmo formato do exemplo de `Equipe` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /conversas/{id}/atribuir — Atribuir ou transferir conversa - URL: `POST https://chat.forsi.com.br/api/v1/conversas/{id}/atribuir` - Acesso: escopo `equipe:escrever` · módulo do plano: API · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `Conversa` - Página: https://docs.forsi.com.br/api/equipe/atribuir-conversa (Markdown: https://docs.forsi.com.br/api/equipe/atribuir-conversa.md) Muda o responsável e/ou a equipe; null tira. Mesmo efeito da tela: registra no histórico da conversa ("Integração “nome da chave” passou para..."), avisa a pessoa, pausa o agente de IA da conversa e dispara `conversa.atribuida`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `responsavel_id` | uuid ou null | não | Pessoa responsável (GET /equipe/membros). null tira o responsável. | | `equipe_id` | uuid ou null | não | Equipe da conversa (GET /equipe/equipes). null tira a equipe. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/conversas/495a7cae-128c-4b66-adbc-807b3959c129/atribuir" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"responsavel_id":"0e584d55-e895-4372-8945-cb64cafc6efd"}' ``` **Resposta `200`**: mesmo formato do exemplo de `Conversa` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Automações do Instagram As automações do Instagram respondem comentários nos seus posts: a regra olha as palavras do comentário, responde em público (se você quiser) e manda uma mensagem no direct de quem comentou. Duas permissões: `automacoes:ler` (regras, contas do Instagram e automações) e `automacoes:escrever` (criar, editar, ligar, desligar e apagar regras). O módulo Automações precisa estar no plano. - Comece por [`GET /instagram/contas`](https://docs.forsi.com.br/api/automacoes/listar-contas-instagram) (o `id` é o `conexao_id`). Para iniciar uma automação com a mensagem direta, pegue o `automacao_id` em [`GET /automacoes?gatilho=comentario`](https://docs.forsi.com.br/api/automacoes/listar-automacoes-de-comentario). - **Sempre informe `post_id`.** Sem ele (`null`) a regra vale para todas as publicações da conta e responde em qualquer post. - A primeira regra que combinar com o comentário é a que vale. Em `modo: contem` o comentário só precisa ter a palavra; em `igual` ele é só a palavra. - A regra nasce desligada, a menos que você mande `ativo: true`. Para pausar, use `PATCH` com `ativo: false`; `DELETE` não tem volta. - `PATCH` muda só os campos enviados, mas o resultado final precisa ser válido (por exemplo, não dá para ficar sem resposta pública e sem resposta privada). - `qualquer_comentario` e `botoes` só funcionam se estiverem liberados na organização; senão a resposta é `403 recurso_desligado`. - Cada botão é um texto (devolve o toque) ou `{ "titulo": "Agendar", "url": "https://..." }` (abre o link; só `https`, até 2000 caracteres, `422 regra_botao_url_invalida`). A resposta traz `botoes` (títulos) e `botoes_detalhe` (título e link). Se a Meta recusar os botões na resposta a um comentário, a mensagem sai uma vez só com texto e os links vão no fim dele: o link nunca se perde. - Limite de 200 regras por organização (`422 limite_de_regras`). - **Dois tipos de regra.** `tipo: "comum"` é o padrão (e o que toda regra antiga é): responde o comentário e manda a mensagem no direct. `tipo: "seguir"` entrega um conteúdo só para quem segue o seu perfil. O fluxo, todos os campos e os padrões estão no guia [Seguir para receber](https://docs.forsi.com.br/seguir-para-receber). Toda regra traz `tipo` na resposta; filtre a listagem com `?tipo=seguir` ou `?tipo=comum`. - Toda mudança fica na auditoria da organização com o nome da chave. Ids de outra organização dão `404`. - Fora da API de propósito: regras por expressão regular, criação e edição de automações (fluxos) e listar as publicações da conta. ```json title="Regra comum (tipo omitido = comum)" { "nome": "Comente QUERO", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["quero"], "responder_publico": "Te mandei no direct!", "resposta_privada": "Oi, {nome}! Aqui está o link: https://exemplo.com.br/oferta" } ``` **Códigos de erro comuns nesta seção**: `regra_inexistente`, `regra_conexao_invalida`, `regra_sem_palavras`, `regra_modo_invalido`, `regra_sem_acao`, `regra_texto_longo`, `regra_variantes_demais`, `regra_extra_sem_publico`, `regra_automacao_invalida`, `regra_automacao_sem_privada`, `regra_botao_longo`, `regra_botao_repetido`, `regra_botao_url_invalida`, `regra_botoes_demais`, `regra_botoes_invalidos`, `regra_botoes_sem_privada`, `recurso_desligado`, `seguir_invalido`, `limite_de_regras`. ### GET /instagram/contas — Listar contas do Instagram - URL: `GET https://chat.forsi.com.br/api/v1/instagram/contas` - Acesso: escopo `automacoes:ler` · módulos do plano: API + Automações - Sucesso: `200`, `{"dados": ...}` com lista de `ContaInstagram` - Página: https://docs.forsi.com.br/api/automacoes/listar-contas-instagram (Markdown: https://docs.forsi.com.br/api/automacoes/listar-contas-instagram.md) Contas conectadas por login do Instagram, as que aceitam regras de comentário. O `id` é o `conexao_id` das regras. **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/instagram/contas" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "e20de52a-e801-4676-9cd3-03e78b280315", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445", "usuario": "texto", "nome": "Maria Souza" } ] } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /automacoes — Listar automações para ligar a uma regra - URL: `GET https://chat.forsi.com.br/api/v1/automacoes` - Acesso: escopo `automacoes:ler` · módulos do plano: API + Automações - Sucesso: `200`, `{"dados": ...}` com lista de `AutomacaoResumo` - Página: https://docs.forsi.com.br/api/automacoes/listar-automacoes-de-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/listar-automacoes-de-comentario.md) Com `gatilho=comentario`: as automações que uma regra de comentário pode iniciar (`automacao_id`). **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `gatilho` | "comentario" | não | Por enquanto só `comentario`: automações que podem ser ligadas a uma regra de comentário. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/automacoes?gatilho=comentario" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":[{"id":"fb75ed96-6799-4508-92b2-6bc7d57ff8a9","nome":"Maria Souza"}]} ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /regras-comentario — Listar regras de comentário - URL: `GET https://chat.forsi.com.br/api/v1/regras-comentario` - Acesso: escopo `automacoes:ler` · módulos do plano: API + Automações - Sucesso: `200`, `{"dados": ...}` com lista de `RegraComentario` (paginada por cursor) - Página: https://docs.forsi.com.br/api/automacoes/listar-regras-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/listar-regras-comentario.md) Regras que respondem comentários do Instagram, paginado por cursor. Filtre por conta, publicação, estado ou `tipo` (`comum` ou `seguir`). Regras `seguir` trazem o bloco `seguir` completo e `estatisticas`. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `conexao_id` | uuid | não | Só as regras desta conta do Instagram. | | `post_id` | string | não | Só as regras desta publicação. | | `ativo` | "true" \| "false" | não | true ou false. | | `tipo` | "comum" \| "seguir" | não | Só as regras deste tipo: `comum` ou `seguir`. | | `limite` | integer | não | Itens por página (1 a 100, padrão 50). | | `cursor` | string | não | O valor de `proximo` da página anterior. até 80 caracteres | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/regras-comentario?conexao_id=54b27747-86c2-455a-91a4-c0c02d0bd891&ativo=true" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": [ { "id": "cc23f373-2c83-42d6-83a7-c4e00ac10c39", "nome": "Maria Souza", "ativo": true, "conexao_id": "11908b04-f817-4068-afa6-896d3e1b7265", "post_id": "texto", "palavras": ["texto"], "modo": "contem", "qualquer_comentario": false, "automacao_id": "b6b1fba7-e2a3-4922-a365-70584a78e091", "automacao": "texto", "responder_publico": "texto", "resposta_privada": "texto", "respostas_publicas": ["texto"], "ocultar_apos": false, "botoes": ["texto"], "botoes_detalhe": [{"titulo":"Consulta de retorno","url":"https://seu-sistema.com.br/webhooks/forsi"}], "tipo": "comum", "seguir": { "botao_receber": "texto", "texto_nao_segue": "texto", "botao_estou_seguindo": "texto", "texto_ainda_nao_segue": "texto", "texto_ja_segue": "texto", "texto_seguiu_depois": "texto", "conteudo": { "texto": "Oi, queria remarcar minha consulta", "botao": {"titulo":"Consulta de retorno","url":"https://seu-sistema.com.br/webhooks/forsi"} }, "max_tentativas": 12, "texto_limite": "texto", "se_nao_verificar": "enviar" }, "estatisticas": {"pediram":12,"entregues":42,"ja_seguiam":12,"seguiram_depois":12,"aguardando":12}, "atualizado_em": "2026-10-04T13:05:12.481Z", "execucoes_7d": 12, "ultima_em": "2026-10-04T13:05:12.481Z" } ], "proximo": "1791205512481_3791c08d-72dd-4002-89ff-1e14a11c07e5" } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### POST /regras-comentario — Criar regra de comentário - URL: `POST https://chat.forsi.com.br/api/v1/regras-comentario` - Acesso: escopo `automacoes:escrever` · módulos do plano: API + Automações · aceita `Idempotency-Key` - Sucesso: `201`, `{"dados": ...}` com `RegraComentario` - Página: https://docs.forsi.com.br/api/automacoes/criar-regra-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/criar-regra-comentario.md) Nasce desligada, a menos que mande `ativo: true`. Informe `post_id`: sem ele a regra vale para todas as publicações. A primeira regra que combinar com o comentário vale. Até 200 regras por organização (422 `limite_de_regras`). Com `tipo: "seguir"` ("Seguir para receber") a pessoa precisa seguir a conta para receber o conteúdo: mande `resposta_privada` e `seguir.conteudo.texto`; o resto de `seguir` tem padrão (422 `seguir_invalido` se algum campo passar do limite). Sem `tipo`, a regra é `comum`. `qualquer_comentario`, `botoes` (inclusive botões de link) e `tipo: "seguir"` dão 403 `recurso_desligado` se não estiverem liberados. Mande `Idempotency-Key` para repetir com segurança. **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | sim | Nome da regra, só para você identificar. até 80 caracteres | | `conexao_id` | uuid | sim | Conta do Instagram (GET /instagram/contas). | | `post_id` | string ou null | não | Id da publicação. null = vale para qualquer publicação (não recomendado: responde em todos os posts). | | `palavras` | lista de string | não | Até 30 palavras. Vazio só com `qualquer_comentario: true`. | | `modo` | "contem" \| "igual" | não | `contem`: o comentário tem a palavra. `igual`: o comentário é só a palavra. | | `qualquer_comentario` | boolean | não | true = dispara em qualquer comentário, sem palavras. Precisa estar liberado na organização. | | `responder_publico` | string ou null | não | Resposta pública ao comentário (até 2200 caracteres). | | `respostas_publicas` | lista de string | não | Variações da resposta pública (até 5), sorteadas a cada comentário. Precisa de `responder_publico`. | | `ocultar_apos` | boolean | não | Oculta o comentário depois de responder. Precisa de `responder_publico`. | | `resposta_privada` | string ou null | não | Mensagem direta enviada a quem comentou (até 1000 caracteres). | | `automacao_id` | uuid ou null | não | Automação a iniciar com a resposta privada (GET /automacoes?gatilho=comentario). Precisa de `resposta_privada`. | | `botoes` | lista de string ou object | não | Até 3 botões na mensagem direta. Cada botão é um texto (devolve o toque) ou `{ titulo, url? }`; com `url` (https) abre o link. Se a Meta recusar os botões nessa resposta, a mensagem sai só com texto e os links entram no fim do texto. Precisa de `resposta_privada` e de estar liberado na organização. | | `ativo` | boolean | não | Liga ou desliga a regra. Ao criar, o padrão é false. | | `tipo` | "comum" \| "seguir" | não | `comum` (padrão): responde e manda a mensagem. `seguir`: o conteúdo só é entregue a quem segue a conta; veja `seguir`. No tipo `seguir` não use `automacao_id` nem `botoes`. | | `seguir` | object | não | Configuração do tipo `seguir`. Campos omitidos usam o padrão. Só `conteudo.texto` é obrigatório. Precisa de `resposta_privada` (até 640 caracteres) e estar liberado na organização (403 `recurso_desligado`). | | `seguir.botao_receber` | string | não | Texto do botão que vai junto da mensagem do comentário. 1 a 20 caracteres, sem variáveis (o banco recusa "{"). Padrão: "Quero receber". | | `seguir.texto_nao_segue` | string | não | Mensagem para quem pediu e ainda não segue a conta. 1 a 640 caracteres. Padrão: "Para receber, é só seguir o perfil @{conta} e depois tocar em "Estou seguindo".". Aceita {nome}, {usuario} e {conta}. | | `seguir.botao_estou_seguindo` | string | não | Texto do botão para avisar que passou a seguir. 1 a 20 caracteres, sem variáveis (o banco recusa "{"). Padrão: "Estou seguindo". | | `seguir.texto_ainda_nao_segue` | string | não | Mensagem quando a pessoa toca em "Estou seguindo" e ainda não segue. 1 a 640 caracteres. Padrão: "Ainda não apareceu que você segue @{conta}. Siga o perfil e toque de novo em "Estou seguindo".". Aceita {nome}, {usuario} e {conta}. | | `seguir.texto_ja_segue` | string | não | Mensagem antes do conteúdo para quem já seguia. 0 a 640 caracteres. Padrão: "Que bom ter você por aqui! Aqui está o que você pediu:". Vazio ("") = não envia este texto, só o conteúdo. Aceita {nome}, {usuario} e {conta}. | | `seguir.texto_seguiu_depois` | string | não | Mensagem antes do conteúdo para quem acabou de seguir. 0 a 640 caracteres. Padrão: "Obrigado por seguir! Aqui está o que você pediu:". Vazio ("") = não envia este texto, só o conteúdo. Aceita {nome}, {usuario} e {conta}. | | `seguir.conteudo` | object | sim | O que é entregue quando a pessoa segue: texto e, se quiser, um botão de link. Obrigatório para o tipo `seguir`. | | `seguir.max_tentativas` | integer ou null | não | Quantas vezes conferir se a pessoa passou a seguir (1 a 50). Padrão: null = confere até seguir, no máximo 50 vezes. | | `seguir.texto_limite` | string ou null | não | | | `seguir.se_nao_verificar` | "enviar" \| "pedir_de_novo" | não | Se a Meta não deixar conferir: `enviar` (padrão) entrega o conteúdo assim mesmo; `pedir_de_novo` pede para seguir outra vez. | **Exemplo (curl)** ```bash curl -X POST "https://chat.forsi.com.br/api/v1/regras-comentario" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "Quero o preço", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["preço","valor"], "modo": "contem", "responder_publico": "Te mandei os detalhes no direct!", "resposta_privada": "Oi! Aqui estão os valores e como agendar.", "botoes": [ "Falar com atendente", {"titulo":"Agendar","url":"https://exemplo.com.br/agendar"} ], "ativo": true }' ``` **Resposta `201` (exemplo)** ```json { "dados": { "id": "cc23f373-2c83-42d6-83a7-c4e00ac10c39", "nome": "Maria Souza", "ativo": true, "conexao_id": "11908b04-f817-4068-afa6-896d3e1b7265", "post_id": "texto", "palavras": ["texto"], "modo": "contem", "qualquer_comentario": false, "automacao_id": "b6b1fba7-e2a3-4922-a365-70584a78e091", "automacao": "texto", "responder_publico": "texto", "resposta_privada": "texto", "respostas_publicas": ["texto"], "ocultar_apos": false, "botoes": ["texto"], "botoes_detalhe": [{"titulo":"Consulta de retorno","url":"https://seu-sistema.com.br/webhooks/forsi"}], "tipo": "comum", "seguir": { "botao_receber": "texto", "texto_nao_segue": "texto", "botao_estou_seguindo": "texto", "texto_ainda_nao_segue": "texto", "texto_ja_segue": "texto", "texto_seguiu_depois": "texto", "conteudo": { "texto": "Oi, queria remarcar minha consulta", "botao": {"titulo":"Consulta de retorno","url":"https://seu-sistema.com.br/webhooks/forsi"} }, "max_tentativas": 12, "texto_limite": "texto", "se_nao_verificar": "enviar" }, "estatisticas": {"pediram":12,"entregues":42,"ja_seguiam":12,"seguiram_depois":12,"aguardando":12}, "atualizado_em": "2026-10-04T13:05:12.481Z", "execucoes_7d": 12, "ultima_em": "2026-10-04T13:05:12.481Z" } } ``` **Erros**: `401`, `403`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /regras-comentario/{id} — Ver uma regra de comentário - URL: `GET https://chat.forsi.com.br/api/v1/regras-comentario/{id}` - Acesso: escopo `automacoes:ler` · módulos do plano: API + Automações - Sucesso: `200`, `{"dados": ...}` com `RegraComentario` - Página: https://docs.forsi.com.br/api/automacoes/ver-regra-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/ver-regra-comentario.md) **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/regras-comentario/7f0a344c-4fb9-4180-8d9b-46d51ab38635" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200`**: mesmo formato do exemplo de `RegraComentario` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### PATCH /regras-comentario/{id} — Editar regra de comentário - URL: `PATCH https://chat.forsi.com.br/api/v1/regras-comentario/{id}` - Acesso: escopo `automacoes:escrever` · módulos do plano: API + Automações · aceita `Idempotency-Key` - Sucesso: `200`, `{"dados": ...}` com `RegraComentario` - Página: https://docs.forsi.com.br/api/automacoes/editar-regra-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/editar-regra-comentario.md) Só os campos enviados mudam. `ativo` liga ou desliga. Mudar `tipo` para `comum` apaga o bloco `seguir`. Em `seguir`, só os campos enviados mudam e os omitidos mantêm o valor atual (dentro de `conteudo` também; `conteudo.botao: null` tira o botão). O resultado final tem de continuar válido (por exemplo, não dá para ficar sem resposta nenhuma). **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Corpo (JSON)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `nome` | string | não | Nome da regra, só para você identificar. até 80 caracteres | | `conexao_id` | uuid | não | Conta do Instagram (GET /instagram/contas). | | `post_id` | string ou null | não | Id da publicação. null = vale para qualquer publicação (não recomendado: responde em todos os posts). | | `palavras` | lista de string | não | Até 30 palavras. Vazio só com `qualquer_comentario: true`. | | `modo` | "contem" \| "igual" | não | `contem`: o comentário tem a palavra. `igual`: o comentário é só a palavra. | | `qualquer_comentario` | boolean | não | true = dispara em qualquer comentário, sem palavras. Precisa estar liberado na organização. | | `responder_publico` | string ou null | não | Resposta pública ao comentário (até 2200 caracteres). | | `respostas_publicas` | lista de string | não | Variações da resposta pública (até 5), sorteadas a cada comentário. Precisa de `responder_publico`. | | `ocultar_apos` | boolean | não | Oculta o comentário depois de responder. Precisa de `responder_publico`. | | `resposta_privada` | string ou null | não | Mensagem direta enviada a quem comentou (até 1000 caracteres). | | `automacao_id` | uuid ou null | não | Automação a iniciar com a resposta privada (GET /automacoes?gatilho=comentario). Precisa de `resposta_privada`. | | `botoes` | lista de string ou object | não | Até 3 botões na mensagem direta. Cada botão é um texto (devolve o toque) ou `{ titulo, url? }`; com `url` (https) abre o link. Se a Meta recusar os botões nessa resposta, a mensagem sai só com texto e os links entram no fim do texto. Precisa de `resposta_privada` e de estar liberado na organização. | | `ativo` | boolean | não | Liga ou desliga a regra. Ao criar, o padrão é false. | | `tipo` | "comum" \| "seguir" | não | `comum` (padrão): responde e manda a mensagem. `seguir`: o conteúdo só é entregue a quem segue a conta; veja `seguir`. No tipo `seguir` não use `automacao_id` nem `botoes`. | | `seguir` | object | não | Mesmo formato de `seguir`, com todos os campos opcionais (inclusive dentro de `conteudo`). Só os campos enviados mudam; os omitidos mantêm o valor atual. `max_tentativas: null` e `texto_limite: null` voltam ao padrão (sem limite). Se a regra era `comum`, o que faltar vem dos padrões e `conteudo.texto` é obrigatório. | | `seguir.botao_receber` | string | não | Texto do botão que vai junto da mensagem do comentário. 1 a 20 caracteres, sem variáveis (o banco recusa "{"). Padrão: "Quero receber". | | `seguir.texto_nao_segue` | string | não | Mensagem para quem pediu e ainda não segue a conta. 1 a 640 caracteres. Padrão: "Para receber, é só seguir o perfil @{conta} e depois tocar em "Estou seguindo".". Aceita {nome}, {usuario} e {conta}. | | `seguir.botao_estou_seguindo` | string | não | Texto do botão para avisar que passou a seguir. 1 a 20 caracteres, sem variáveis (o banco recusa "{"). Padrão: "Estou seguindo". | | `seguir.texto_ainda_nao_segue` | string | não | Mensagem quando a pessoa toca em "Estou seguindo" e ainda não segue. 1 a 640 caracteres. Padrão: "Ainda não apareceu que você segue @{conta}. Siga o perfil e toque de novo em "Estou seguindo".". Aceita {nome}, {usuario} e {conta}. | | `seguir.texto_ja_segue` | string | não | Mensagem antes do conteúdo para quem já seguia. 0 a 640 caracteres. Padrão: "Que bom ter você por aqui! Aqui está o que você pediu:". Vazio ("") = não envia este texto, só o conteúdo. Aceita {nome}, {usuario} e {conta}. | | `seguir.texto_seguiu_depois` | string | não | Mensagem antes do conteúdo para quem acabou de seguir. 0 a 640 caracteres. Padrão: "Obrigado por seguir! Aqui está o que você pediu:". Vazio ("") = não envia este texto, só o conteúdo. Aceita {nome}, {usuario} e {conta}. | | `seguir.conteudo` | object | não | Só o que mudar: `texto`, `botao` ou os dois. `botao: null` tira o botão. | | `seguir.max_tentativas` | integer ou null | não | Quantas vezes conferir se a pessoa passou a seguir (1 a 50). Padrão: null = confere até seguir, no máximo 50 vezes. | | `seguir.texto_limite` | string ou null | não | | | `seguir.se_nao_verificar` | "enviar" \| "pedir_de_novo" | não | Se a Meta não deixar conferir: `enviar` (padrão) entrega o conteúdo assim mesmo; `pedir_de_novo` pede para seguir outra vez. | **Exemplo (curl)** ```bash curl -X PATCH "https://chat.forsi.com.br/api/v1/regras-comentario/7f0a344c-4fb9-4180-8d9b-46d51ab38635" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{"palavras":["preço","valor","quanto custa"],"ativo":false}' ``` **Resposta `200`**: mesmo formato do exemplo de `RegraComentario` acima (campos em "Objetos das respostas"). **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### DELETE /regras-comentario/{id} — Apagar regra de comentário - URL: `DELETE https://chat.forsi.com.br/api/v1/regras-comentario/{id}` - Acesso: escopo `automacoes:escrever` · módulos do plano: API + Automações - Sucesso: `200`, `{"dados": ...}` com `ApagadoRegra` - Página: https://docs.forsi.com.br/api/automacoes/apagar-regra-comentario (Markdown: https://docs.forsi.com.br/api/automacoes/apagar-regra-comentario.md) Sem volta. Para só pausar, use PATCH com `ativo: false`. **Parâmetros no caminho** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | sim | No caminho da URL. | **Exemplo (curl)** ```bash curl -X DELETE "https://chat.forsi.com.br/api/v1/regras-comentario/7f0a344c-4fb9-4180-8d9b-46d51ab38635" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json {"dados":{"apagado":false}} ``` **Erros**: `401`, `403`, `404`, `409`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Seguir para receber O Seguir para receber entrega um conteúdo no direct do Instagram só para quem segue o seu perfil. A pessoa comenta uma palavra-chave no post e recebe uma mensagem com um botão. Ao tocar, a FORSI confere se ela segue a conta: quem segue recebe o conteúdo na hora; quem não segue recebe um pedido para seguir e um botão "Estou seguindo" para conferir de novo. Na API, é uma regra de comentário com `tipo: "seguir"`, criada por [`POST /regras-comentario`](https://docs.forsi.com.br/api/automacoes/criar-regra-comentario). Precisa dos escopos `automacoes:ler` e `automacoes:escrever`, do módulo Automações no plano e do recurso liberado na organização (é o mesmo dos botões; sem ele, `403 recurso_desligado`). ### Como funciona > **Figura: O fluxo do Seguir para receber.** A pessoa comenta a palavra-chave no post. A FORSI responde o comentário (se você configurou) e manda no direct a sua mensagem com o botão "Quero receber". Quando a pessoa toca, a FORSI confere se ela segue a conta. Se já segue, recebe texto_ja_segue e o conteúdo. Se não segue, recebe texto_nao_segue com o botão "Estou seguindo". A cada toque nesse botão a FORSI confere de novo: se seguiu, recebe texto_seguiu_depois e o conteúdo; se ainda não, recebe texto_ainda_nao_segue e pode tocar outra vez, até max_tentativas (então vai texto_limite). Cada pessoa recebe o conteúdo uma vez só. - Cada pessoa recebe o conteúdo uma vez só, mesmo que comente de novo. - Quem já seguia recebe `texto_ja_segue` e o conteúdo. Quem seguiu depois do pedido recebe `texto_seguiu_depois` e o conteúdo. - O botão "Estou seguindo" pode ser tocado quantas vezes for preciso, até `max_tentativas`. Quando as tentativas acabam, vai `texto_limite` (se você definiu) e a FORSI para de responder a essa pessoa. ### Passo a passo 1. **Pegue a conta.** [`GET /instagram/contas`](https://docs.forsi.com.br/api/automacoes/listar-contas-instagram) lista as contas conectadas. O `id` de cada uma é o `conexao_id` da regra. 2. **Escolha o post.** Mande o `post_id` da publicação. Sem ele (`null`), a regra vale para todos os posts da conta. 3. **Crie a regra** com `tipo: "seguir"`, as palavras-chave, `resposta_privada` (a mensagem que leva o botão, até 640 caracteres) e `seguir.conteudo.texto`. O resto do bloco `seguir` é opcional e tem padrão. Mande `ativo: true` para a regra já nascer ligada. 4. **Teste** comentando a palavra-chave no post com um perfil que não segue a conta, e depois com um que segue. 5. **Acompanhe** em [`GET /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/ver-regra-comentario): `estatisticas` traz `pediram` (receberam o botão), `entregues` (receberam o conteúdo), `ja_seguiam`, `seguiram_depois` e `aguardando`. ### Requisição completa Só o mínimo; o resto do bloco `seguir` usa os padrões da tabela abaixo: ```bash title="cURL" curl -X POST "https://chat.forsi.com.br/api/v1/regras-comentario" \ -H "Authorization: Bearer $FORSI_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4821" \ -d '{ "nome": "Guia grátis", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["guia"], "responder_publico": "Te chamei no direct!", "resposta_privada": "Oi, {nome}! Toque no botão para receber o guia.", "tipo": "seguir", "seguir": {"conteudo":{"texto":"Aqui está o seu guia!"}}, "ativo": true }' ``` ```js title="JavaScript" const resposta = await fetch('https://chat.forsi.com.br/api/v1/regras-comentario', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FORSI_CHAVE}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'pedido-4821', }, body: JSON.stringify({ "nome": "Guia grátis", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["guia"], "responder_publico": "Te chamei no direct!", "resposta_privada": "Oi, {nome}! Toque no botão para receber o guia.", "tipo": "seguir", "seguir": {"conteudo":{"texto":"Aqui está o seu guia!"}}, "ativo": true }), }); const json = await resposta.json(); if (!resposta.ok) throw new Error(`${resposta.status} ${json.erro.codigo}: ${json.erro.mensagem}`); console.log(json.dados); ``` ```python title="Python" import os import requests resposta = requests.post( "https://chat.forsi.com.br/api/v1/regras-comentario", headers={ "Authorization": f"Bearer {os.environ['FORSI_CHAVE']}", "Idempotency-Key": "pedido-4821", }, json={ "nome": "Guia grátis", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["guia"], "responder_publico": "Te chamei no direct!", "resposta_privada": "Oi, {nome}! Toque no botão para receber o guia.", "tipo": "seguir", "seguir": { "conteudo": { "texto": "Aqui está o seu guia!", }, }, "ativo": True, }, timeout=30, ) corpo = resposta.json() if not resposta.ok: raise RuntimeError(f"{resposta.status_code} {corpo['erro']['codigo']}: {corpo['erro']['mensagem']}") print(corpo["dados"]) ``` A resposta é `201` com a regra criada e o bloco `seguir` completo, já com os padrões. O mesmo pedido com todos os campos: ```json title="Seguir para receber (todos os campos)" { "nome": "Guia grátis", "conexao_id": "54b27747-86c2-455a-91a4-c0c02d0bd891", "post_id": "17912345678901234", "palavras": ["guia"], "responder_publico": "Te chamei no direct!", "resposta_privada": "Oi, {nome}! Toque no botão para receber o guia.", "tipo": "seguir", "seguir": { "botao_receber": "Quero receber", "texto_nao_segue": "Para receber, é só seguir o perfil @{conta} e depois tocar em \"Estou seguindo\".", "botao_estou_seguindo": "Estou seguindo", "texto_ainda_nao_segue": "Ainda não apareceu que você segue @{conta}. Siga o perfil e toque de novo em \"Estou seguindo\".", "texto_ja_segue": "Que bom ter você por aqui! Aqui está o que você pediu:", "texto_seguiu_depois": "Obrigado por seguir! Aqui está o que você pediu:", "conteudo": { "texto": "Seu guia completo está aqui.", "botao": {"titulo":"Abrir o guia","url":"https://exemplo.com.br/guia"} }, "max_tentativas": 5, "texto_limite": "Quando seguir, é só comentar de novo.", "se_nao_verificar": "enviar" }, "ativo": true } ``` Nesse tipo não use `automacao_id` nem `botoes`: o botão vem de `seguir.botao_receber`. A resposta pública (`responder_publico`), as variações e esconder o comentário funcionam normalmente. ### Campos do bloco seguir | Campo | Para que serve | Limite | Padrão | | --- | --- | --- | --- | | `botao_receber` | Botão da primeira mensagem. | 1 a 20 caracteres, sem variáveis | "Quero receber" | | `texto_nao_segue` | Pedido para seguir, para quem ainda não segue. | 1 a 640 | "Para receber, é só seguir o perfil @{conta} e depois tocar em "Estou seguindo"." | | `botao_estou_seguindo` | Botão para avisar que seguiu. | 1 a 20, sem variáveis | "Estou seguindo" | | `texto_ainda_nao_segue` | Quando a pessoa toca em "Estou seguindo" e ainda não segue. | 1 a 640 | "Ainda não apareceu que você segue @{conta}. Siga o perfil e toque de novo em "Estou seguindo"." | | `texto_ja_segue` | Vai antes do conteúdo, para quem já seguia. | 0 a 640 ("" manda só o conteúdo) | "Que bom ter você por aqui! Aqui está o que você pediu:" | | `texto_seguiu_depois` | Vai antes do conteúdo, para quem acabou de seguir. | 0 a 640 ("" manda só o conteúdo) | "Obrigado por seguir! Aqui está o que você pediu:" | | `conteudo.texto` | O conteúdo entregue. **Obrigatório.** | 1 a 640 | nenhum | | `conteudo.botao` | Botão com link no conteúdo: `titulo` (1 a 20) e `url` (https). | opcional | `null` (sem botão) | | `max_tentativas` | Quantas vezes conferir quando a pessoa toca em "Estou seguindo". | 1 a 50, ou `null` | `null`: confere até a pessoa seguir, no máximo 50 vezes | | `texto_limite` | Vai uma vez quando as tentativas acabam. | 1 a 640, ou `null` | `null` (não manda nada) | | `se_nao_verificar` | O que fazer se o Instagram não deixar conferir. | `enviar` ou `pedir_de_novo` | `enviar`: entrega o conteúdo assim mesmo | Os textos aceitam `{nome}`, `{usuario}` e `{conta}`: o nome e o @ de quem comentou e o @ da sua conta. Os dois botões não trocam variáveis. Cada texto vai até 900 bytes (emoji e acento contam mais que uma letra), porque a mensagem direta do Instagram vai até 1000 bytes. ### Editar uma regra No [`PATCH /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/editar-regra-comentario), sem `tipo` a regra mantém o tipo. Em `seguir`, só os campos enviados mudam: os que você omitir mantêm o valor atual, inclusive dentro de `conteudo` (mande só `texto` ou só `botao`; `botao: null` tira o botão). `max_tentativas: null` e `texto_limite: null` voltam ao padrão. `tipo: "comum"` apaga o bloco `seguir`. Ao trocar uma regra comum para `seguir`, o que faltar vem dos padrões e `conteudo.texto` é obrigatório. ```json title="Mudar só um texto do Seguir para receber" { "seguir": {"conteudo":{"texto":"Novo texto do conteúdo."},"max_tentativas":3} } ``` ```json title="Voltar para regra comum (apaga o bloco seguir)" {"tipo":"comum"} ``` ### Respostas Toda regra traz `tipo`. Nas regras `seguir`, o bloco `seguir` vem completo, com os padrões. Campo sem valor não aparece: `conteudo.botao`, `max_tentativas` e `texto_limite` somem enquanto forem `null`, e `seguir` some na regra comum. [`GET /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/ver-regra-comentario) e a [listagem](https://docs.forsi.com.br/api/automacoes/listar-regras-comentario) trazem `estatisticas`. Filtre a listagem com `?tipo=seguir`. ### Erros - `403 recurso_desligado`: o Seguir para receber não está liberado na organização (é o mesmo recurso dos botões). - `422 seguir_invalido`: um campo de `seguir` está vazio, passa do limite, tem `{` num dos dois botões, tem caractere de controle ou invisível, ou passa de 900 bytes. A mensagem diz qual campo. - `422 limite_de_regras`: a organização já tem 200 regras. ### Perguntas frequentes #### A FORSI confere mesmo se a pessoa segue? Sim, a cada toque no botão. Se o Instagram não deixar conferir, vale `se_nao_verificar`: `enviar` (padrão) entrega o conteúdo assim mesmo e `pedir_de_novo` pede para seguir outra vez. #### Quem comentar duas vezes recebe duas vezes? Não. Cada pessoa recebe o conteúdo uma vez só. #### Posso usar variáveis nos botões? Não. `botao_receber` e `botao_estou_seguindo` não trocam variáveis, e um `{` neles dá `422 seguir_invalido`. Nos textos, use `{nome}`, `{usuario}` e `{conta}`. #### Dá para transformar uma regra comum em Seguir para receber? Dá, com `PATCH` e `tipo: "seguir"`. O que faltar vem dos padrões, mas `seguir.conteudo.texto` é obrigatório. Para voltar, mande `tipo: "comum"`: o bloco `seguir` é apagado. #### O que acontece se eu não mandar post_id? A regra vale para todos os posts da conta e responde a palavra-chave em qualquer publicação. Se o conteúdo é de um post só, mande sempre o `post_id`. #### O que a pessoa vê se nunca seguir? A cada toque em "Estou seguindo" ela recebe `texto_ainda_nao_segue`. Com `max_tentativas`, quando as tentativas acabam vai `texto_limite` (se houver) e a FORSI para de responder a ela. Sem `max_tentativas`, a FORSI confere até 50 vezes. ## Relatórios Os relatórios são só de leitura e usam o escopo `relatorios:ler`. Dão os mesmos números da tela Relatórios, no fuso da sua organização, e nunca mostram valores em dinheiro: o uso de IA vem só em créditos. - `de` e `ate` (AAAA-MM-DD): o período, de até 366 dias. Sem eles, valem os últimos 30 dias. - `granularidade`: `dia` (padrão), `semana` (começa na segunda) ou `mes`. Vale para volume, tempo de resposta e créditos. - `caixa_id`: só os números de uma caixa. - `formato=csv`: planilha em UTF-8 com BOM, cabeçalho e colunas sempre na mesma ordem. Textos que começariam com `=`, `+`, `-` ou `@` saem com um apóstrofo na frente, para a planilha não os ler como fórmula. - Cache: a resposta traz `ETag`. Mande-o em `If-None-Match` e, se nada mudou, a resposta é `304` sem corpo. Os números mudam a cada mensagem: não guarde por mais de 1 minuto. - Limite próprio: 12 consultas por minuto por chave e 30 por organização (`429 limite_relatorios`). Se a consulta demorar demais, vem `503 relatorio_demorou`: escolha um período menor ou agrupe por semana ou mês. - O tempo de resposta traz a porcentagem respondida em até 1, 5, 15 e 60 minutos. **Códigos de erro comuns nesta seção**: `periodo_longo`, `periodo_invalido`, `granularidade_invalida`, `caixa_invalida`, `limite_relatorios`, `relatorio_demorou`. ### GET /relatorios/volume — Volume de conversas e mensagens - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/volume` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioVolume`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-volume (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-volume.md) Conversas novas e resolvidas e mensagens recebidas/enviadas no período, em uma linha por dia, semana ou mês, mais o movimento por hora do dia. Os mesmos números da tela Relatórios. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `granularidade` | "dia" \| "semana" \| "mes" | não | Tamanho de cada linha da série: `dia` (padrão), `semana` (começa na segunda) ou `mes`. | | `caixa_id` | uuid | não | Só os números desta caixa (WhatsApp, Instagram...). Veja as caixas em GET /me. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/volume?de=2026-09-01&ate=2026-09-30&granularidade=semana" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": { "conversas_novas": 12, "conversas_resolvidas": 12, "mensagens_recebidas": 12, "mensagens_enviadas": 12, "mensagens_do_agente": 12, "notas_internas": 12 }, "por_hora": [{"hora":9,"mensagens_recebidas":12,"mensagens_enviadas":12}], "linhas": [ { "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "conversas_novas": 12, "conversas_resolvidas": 12, "mensagens_recebidas": 12, "mensagens_enviadas": 12, "mensagens_do_agente": 12, "notas_internas": 12 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /relatorios/tempo-de-resposta — Tempo de resposta e de resolução - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/tempo-de-resposta` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioTempo`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-tempo-de-resposta (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-tempo-de-resposta.md) Primeira resposta e resolução das conversas: quantidade, mediana, 90% das conversas, faixas e a porcentagem respondida em até 1, 5, 15 e 60 minutos. Tempos em segundos. Por semana ou mês, a mediana é calculada de novo em cada período (não é média de medianas). **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `granularidade` | "dia" \| "semana" \| "mes" | não | Tamanho de cada linha da série: `dia` (padrão), `semana` (começa na segunda) ou `mes`. | | `caixa_id` | uuid | não | Só os números desta caixa (WhatsApp, Instagram...). Veja as caixas em GET /me. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/tempo-de-resposta?de=2026-09-01&ate=2026-09-30" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": { "primeiras_respostas": 12, "sem_resposta": 12, "mediana_primeira_resposta_s": 240, "p90_primeira_resposta_s": 240, "respondidas_em_ate_1_min_pct": 87.5, "respondidas_em_ate_5_min_pct": 87.5, "respondidas_em_ate_15_min_pct": 87.5, "respondidas_em_ate_1_h_pct": 87.5, "resolvidas": 12, "mediana_resolucao_s": 240, "p90_resolucao_s": 240 }, "faixas_primeira_resposta": [{"faixa":"1 a 5 min","quantidade":12}], "faixas_resolucao": [{"faixa":"1 a 5 min","quantidade":12}], "linhas": [ { "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "primeiras_respostas": 12, "mediana_primeira_resposta_s": 240, "resolvidas": 12, "mediana_resolucao_s": 240 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /relatorios/atendentes — Desempenho por atendente - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/atendentes` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioAtendentes`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-atendentes (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-atendentes.md) Uma linha por pessoa da equipe com conversas atendidas, mensagens enviadas, resolvidas, abertas agora e tempo de primeira resposta no período todo. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `caixa_id` | uuid | não | Só os números desta caixa (WhatsApp, Instagram...). Veja as caixas em GET /me. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/atendentes?de=2026-09-01&ate=2026-09-30&formato=csv" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": {"atendentes":12}, "linhas": [ { "id": "64ba31d1-11f4-41f2-9d68-33eef70d3495", "nome": "Maria Souza", "papel": "texto", "conversas_atendidas": 12, "mensagens_enviadas": 12, "conversas_resolvidas": 12, "abertas_agora": 12, "primeiras_respostas": 12, "mediana_primeira_resposta_s": 240 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /relatorios/caixas — Desempenho por caixa (canal) - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/caixas` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioCaixas`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-caixas (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-caixas.md) Uma linha por caixa (WhatsApp, Instagram etc.) com conversas novas e resolvidas e mensagens no período todo. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `caixa_id` | uuid | não | Só os números desta caixa (WhatsApp, Instagram...). Veja as caixas em GET /me. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/caixas?de=2026-09-01&ate=2026-09-30" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": {"caixas":12}, "linhas": [ { "id": "64ba31d1-11f4-41f2-9d68-33eef70d3495", "nome": "Maria Souza", "canal": "whatsapp", "telefone": "+5531988887777", "conversas_novas": 12, "conversas_resolvidas": 12, "conversas_com_mensagens": 12, "mensagens_recebidas": 12, "mensagens_enviadas": 12 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /relatorios/agente-de-ia — Atendimento do agente de IA - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/agente-de-ia` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioAgenteDeIa`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-agente-de-ia (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-agente-de-ia.md) Quantas conversas o agente resolveu sozinho, quantas passou para a equipe e os motivos da passagem. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `caixa_id` | uuid | não | Só os números desta caixa (WhatsApp, Instagram...). Veja as caixas em GET /me. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/agente-de-ia?de=2026-09-01&ate=2026-09-30" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": { "conversas": 12, "sem_agente": 12, "com_agente": 12, "passadas_para_a_equipe": 12, "resolvidas_pelo_agente": 12, "em_andamento": 12, "taxa_resolucao": 0.62 }, "motivos_da_passagem": [{"motivo":"texto","quantidade":12}], "linhas": [ { "id": "64ba31d1-11f4-41f2-9d68-33eef70d3495", "nome": "Maria Souza", "conversas_com_agente": 12, "passadas_para_a_equipe": 12, "resolvidas_pelo_agente": 12, "em_andamento": 12 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ### GET /relatorios/creditos — Créditos de IA usados - URL: `GET https://chat.forsi.com.br/api/v1/relatorios/creditos` - Acesso: escopo `relatorios:ler` · módulo do plano: API - Sucesso: `200`, `{"dados": ...}` com `RelatorioCreditos`; ou CSV com `formato=csv` - Página: https://docs.forsi.com.br/api/relatorios/relatorio-creditos (Markdown: https://docs.forsi.com.br/api/relatorios/relatorio-creditos.md) Créditos gastos no período (por dia, semana ou mês e por tipo de uso) e o saldo atual. Só créditos: nada em dinheiro. **Parâmetros de consulta (query string)** | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `de` | data AAAA-MM-DD | não | Primeiro dia (AAAA-MM-DD, no fuso da organização). Padrão: 29 dias antes de `ate`. | | `ate` | data AAAA-MM-DD | não | Último dia, inclusive. Padrão: hoje. O período vai até 366 dias. | | `granularidade` | "dia" \| "semana" \| "mes" | não | Tamanho de cada linha da série: `dia` (padrão), `semana` (começa na segunda) ou `mes`. | | `formato` | "json" \| "csv" | não | `json` (padrão) ou `csv` (UTF-8 com BOM, separado por vírgula, pronto para planilha). | **Exemplo (curl)** ```bash curl "https://chat.forsi.com.br/api/v1/relatorios/creditos?granularidade=mes&de=2026-01-01&ate=2026-09-30" \ -H "Authorization: Bearer $FORSI_CHAVE" ``` **Resposta `200` (exemplo)** ```json { "dados": { "periodo": { "de": "2026-10-12", "ate": "2026-10-12", "dias": 30, "fuso": "America/Sao_Paulo", "granularidade": "dia", "caixa_id": "7a9d53e9-9d1b-4122-923b-fac67958f445" }, "resumo": { "creditos_usados": 12, "creditos_disponiveis": 12, "creditos_do_plano": 12, "creditos_extras": 12, "renovam_em": "2026-10-04T13:05:12.481Z" }, "por_motivo": [{"motivo":"resposta do agente","creditos_usados":12,"vezes":12}], "linhas": [ { "inicio": "2026-10-12T17:00:00.000Z", "fim": "2026-10-12T17:30:00.000Z", "creditos_usados": 12 } ] } } ``` **Erros**: `401`, `403`, `422`, `429` (formato e códigos em "Erros"; códigos desta seção no início dela). ## Escopos e planos Cada chave tem só os escopos marcados na criação. Chamada fora deles dá `403 escopo_insuficiente` (o escopo que faltou vem em `detalhe`). [`GET /me`](https://docs.forsi.com.br/api/conta/me) aceita qualquer chave válida. | Escopo | O que libera | Rotas | | --- | --- | --- | | `mensagens:enviar` | Enviar mensagens (texto, mídia e modelos) | [`POST /mensagens`](https://docs.forsi.com.br/api/mensagens/enviar-mensagem) | | `mensagens:ler` | Ler mensagens das conversas | [`GET /mensagens`](https://docs.forsi.com.br/api/mensagens/listar-mensagens-org), [`GET /mensagens/{id}`](https://docs.forsi.com.br/api/mensagens/ver-mensagem), [`GET /mensagens/{id}/midia`](https://docs.forsi.com.br/api/mensagens/baixar-midia), [`GET /conversas/{id}/mensagens`](https://docs.forsi.com.br/api/conversas/listar-mensagens) | | `contatos:ler` | Ler e buscar contatos | [`GET /contatos`](https://docs.forsi.com.br/api/contatos/listar-contatos), [`GET /contatos/{id}`](https://docs.forsi.com.br/api/contatos/ver-contato) | | `contatos:escrever` | Criar, editar e apagar contatos | [`POST /contatos`](https://docs.forsi.com.br/api/contatos/criar-contato), [`PATCH /contatos/{id}`](https://docs.forsi.com.br/api/contatos/atualizar-contato), [`DELETE /contatos/{id}`](https://docs.forsi.com.br/api/contatos/apagar-contato) | | `conversas:ler` | Listar e ler conversas | [`GET /conversas`](https://docs.forsi.com.br/api/conversas/listar-conversas), [`GET /conversas/{id}`](https://docs.forsi.com.br/api/conversas/ver-conversa) | | `modelos:ler` | Listar modelos aprovados do WhatsApp | [`GET /modelos`](https://docs.forsi.com.br/api/modelos/listar-modelos) | | `webhooks:gerir` | Cadastrar e gerir webhooks | [`GET /webhooks`](https://docs.forsi.com.br/api/webhooks/listar-webhooks), [`POST /webhooks`](https://docs.forsi.com.br/api/webhooks/criar-webhook), [`GET /webhooks/{id}`](https://docs.forsi.com.br/api/webhooks/ver-webhook), [`PATCH /webhooks/{id}`](https://docs.forsi.com.br/api/webhooks/atualizar-webhook), [`DELETE /webhooks/{id}`](https://docs.forsi.com.br/api/webhooks/apagar-webhook), [`POST /webhooks/{id}/rotacionar`](https://docs.forsi.com.br/api/webhooks/rotacionar-webhook), [`POST /webhooks/{id}/testar`](https://docs.forsi.com.br/api/webhooks/testar-webhook), [`GET /webhooks/{id}/entregas`](https://docs.forsi.com.br/api/webhooks/listar-entregas), [`POST /webhooks/entregas/{id}/reenviar`](https://docs.forsi.com.br/api/webhooks/reenviar-entrega) | | `funil:ler` | Ler funis, etapas, negócios e notas | [`GET /funis`](https://docs.forsi.com.br/api/funil/listar-funis), [`GET /funis/{id}`](https://docs.forsi.com.br/api/funil/ver-funil), [`GET /etapas/{id}`](https://docs.forsi.com.br/api/funil/ver-etapa), [`GET /negocios`](https://docs.forsi.com.br/api/funil/listar-negocios), [`GET /negocios/{id}`](https://docs.forsi.com.br/api/funil/ver-negocio), [`GET /negocios/{id}/notas`](https://docs.forsi.com.br/api/funil/listar-notas-negocio) | | `funil:escrever` | Criar e editar funis, etapas e negócios; mover, ganhar, perder e anotar | [`POST /funis`](https://docs.forsi.com.br/api/funil/criar-funil), [`PATCH /funis/{id}`](https://docs.forsi.com.br/api/funil/atualizar-funil), [`POST /etapas`](https://docs.forsi.com.br/api/funil/criar-etapa), [`PATCH /etapas/{id}`](https://docs.forsi.com.br/api/funil/atualizar-etapa), [`POST /negocios`](https://docs.forsi.com.br/api/funil/criar-negocio), [`PATCH /negocios/{id}`](https://docs.forsi.com.br/api/funil/atualizar-negocio), [`POST /negocios/{id}/mover`](https://docs.forsi.com.br/api/funil/mover-negocio), [`POST /negocios/{id}/ganhar`](https://docs.forsi.com.br/api/funil/ganhar-negocio), [`POST /negocios/{id}/perder`](https://docs.forsi.com.br/api/funil/perder-negocio), [`POST /negocios/{id}/reabrir`](https://docs.forsi.com.br/api/funil/reabrir-negocio), [`POST /negocios/{id}/notas`](https://docs.forsi.com.br/api/funil/criar-nota-negocio) | | `funil:apagar` | Apagar negócios, notas, etapas e funis (sem volta) | [`DELETE /funis/{id}`](https://docs.forsi.com.br/api/funil/apagar-funil), [`DELETE /etapas/{id}`](https://docs.forsi.com.br/api/funil/apagar-etapa), [`DELETE /negocios/{id}`](https://docs.forsi.com.br/api/funil/apagar-negocio), [`DELETE /negocios/notas/{id}`](https://docs.forsi.com.br/api/funil/apagar-nota-negocio) | | `relatorios:ler` | Ler relatórios: volume, tempo de resposta, equipe, caixas, agente de IA e créditos usados | [`GET /relatorios/volume`](https://docs.forsi.com.br/api/relatorios/relatorio-volume), [`GET /relatorios/tempo-de-resposta`](https://docs.forsi.com.br/api/relatorios/relatorio-tempo-de-resposta), [`GET /relatorios/atendentes`](https://docs.forsi.com.br/api/relatorios/relatorio-atendentes), [`GET /relatorios/caixas`](https://docs.forsi.com.br/api/relatorios/relatorio-caixas), [`GET /relatorios/agente-de-ia`](https://docs.forsi.com.br/api/relatorios/relatorio-agente-de-ia), [`GET /relatorios/creditos`](https://docs.forsi.com.br/api/relatorios/relatorio-creditos) | | `agenda:ler` | Ler agendas, horários livres, compromissos e bloqueios | [`GET /agendas`](https://docs.forsi.com.br/api/agenda/listar-agendas), [`GET /agendas/{id}`](https://docs.forsi.com.br/api/agenda/ver-agenda), [`GET /disponibilidade`](https://docs.forsi.com.br/api/agenda/disponibilidade), [`GET /compromissos`](https://docs.forsi.com.br/api/agenda/listar-compromissos), [`GET /compromissos/{id}`](https://docs.forsi.com.br/api/agenda/ver-compromisso), [`GET /bloqueios`](https://docs.forsi.com.br/api/agenda/listar-bloqueios), [`GET /bloqueios/{id}`](https://docs.forsi.com.br/api/agenda/ver-bloqueio) | | `agenda:escrever` | Criar e editar agendas e bloqueios; marcar, remarcar, confirmar, cancelar e dar presença em compromissos (não apaga nada) | [`POST /agendas`](https://docs.forsi.com.br/api/agenda/criar-agenda), [`PATCH /agendas/{id}`](https://docs.forsi.com.br/api/agenda/atualizar-agenda), [`POST /compromissos`](https://docs.forsi.com.br/api/agenda/criar-compromisso), [`PATCH /compromissos/{id}`](https://docs.forsi.com.br/api/agenda/atualizar-compromisso), [`POST /compromissos/{id}/remarcar`](https://docs.forsi.com.br/api/agenda/remarcar-compromisso), [`POST /compromissos/{id}/cancelar`](https://docs.forsi.com.br/api/agenda/cancelar-compromisso), [`POST /compromissos/{id}/confirmar`](https://docs.forsi.com.br/api/agenda/confirmar-compromisso), [`POST /bloqueios`](https://docs.forsi.com.br/api/agenda/criar-bloqueio), [`PATCH /bloqueios/{id}`](https://docs.forsi.com.br/api/agenda/atualizar-bloqueio), [`DELETE /bloqueios/{id}`](https://docs.forsi.com.br/api/agenda/apagar-bloqueio) | | `equipe:ler` | Ler pessoas da equipe, presença, convites abertos e equipes (setores) | [`GET /equipe/membros`](https://docs.forsi.com.br/api/equipe/listar-membros), [`GET /equipe/membros/{id}`](https://docs.forsi.com.br/api/equipe/ver-membro), [`GET /equipe/presenca`](https://docs.forsi.com.br/api/equipe/listar-presenca), [`GET /equipe/convites`](https://docs.forsi.com.br/api/equipe/listar-convites), [`GET /equipe/equipes`](https://docs.forsi.com.br/api/equipe/listar-equipes), [`GET /equipe/equipes/{id}`](https://docs.forsi.com.br/api/equipe/ver-equipe) | | `equipe:escrever` | Criar, editar e apagar equipes (setores), pôr e tirar pessoas delas e atribuir conversas | [`POST /equipe/equipes`](https://docs.forsi.com.br/api/equipe/criar-equipe), [`PATCH /equipe/equipes/{id}`](https://docs.forsi.com.br/api/equipe/atualizar-equipe), [`DELETE /equipe/equipes/{id}`](https://docs.forsi.com.br/api/equipe/apagar-equipe), [`PATCH /equipe/equipes/{id}/membros`](https://docs.forsi.com.br/api/equipe/atualizar-membros-equipe), [`POST /conversas/{id}/atribuir`](https://docs.forsi.com.br/api/equipe/atribuir-conversa) | | `equipe:permissoes` (sensível) | Convidar atendentes, reenviar e cancelar convites, mudar permissões e remover atendentes (nunca mexe em dono ou gestor) | [`PATCH /equipe/membros/{id}`](https://docs.forsi.com.br/api/equipe/atualizar-membro), [`DELETE /equipe/membros/{id}`](https://docs.forsi.com.br/api/equipe/remover-membro), [`POST /equipe/convites`](https://docs.forsi.com.br/api/equipe/criar-convite), [`DELETE /equipe/convites/{id}`](https://docs.forsi.com.br/api/equipe/cancelar-convite), [`POST /equipe/convites/{id}/reenviar`](https://docs.forsi.com.br/api/equipe/reenviar-convite) | | `automacoes:ler` | Ler regras de comentário do Instagram, contas conectadas e automações | [`GET /instagram/contas`](https://docs.forsi.com.br/api/automacoes/listar-contas-instagram), [`GET /automacoes`](https://docs.forsi.com.br/api/automacoes/listar-automacoes-de-comentario), [`GET /regras-comentario`](https://docs.forsi.com.br/api/automacoes/listar-regras-comentario), [`GET /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/ver-regra-comentario) | | `automacoes:escrever` (sensível) | Criar, editar, ligar, desligar e apagar regras de comentário do Instagram | [`POST /regras-comentario`](https://docs.forsi.com.br/api/automacoes/criar-regra-comentario), [`PATCH /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/editar-regra-comentario), [`DELETE /regras-comentario/{id}`](https://docs.forsi.com.br/api/automacoes/apagar-regra-comentario) | ### Plano - Tudo exige o módulo **API** no plano da organização. Sem ele, qualquer chamada dá `403 modulo_fora_do_plano` e os webhooks param. - Funil e negócios exigem também o módulo **Funil**; agenda exige o módulo **Agenda**. A checagem é a cada chamada: downgrade corta na hora. - Assinatura suspensa: `403 assinatura_bloqueada`. - Os limites de uso valem para o plano atual (veja [Limites de uso](https://docs.forsi.com.br/limites)). ## Boas práticas de segurança - **A chave é segredo de servidor.** Guarde em variável de ambiente ou cofre de segredos, nunca no código, no repositório, no site, no app de celular ou no navegador. Chamadas com `Origin` são recusadas de propósito. - **Uma chave por integração**, com o mínimo de escopos. Assim dá para revogar uma sem parar as outras. Evite `equipe:permissoes` e `funil:apagar` se não precisar. - **Restrinja por IP** e use **validade** quando o seu servidor tiver IP fixo ou a integração for temporária. - **Troque a chave com calma**: Trocar a chave mantém a antiga valendo por até 7 dias. - **Vazou?** Revogue na hora em Configurações → API e webhooks e crie outra. Revogar corta na hora. - **Nunca registre a chave inteira** em log, ticket ou print. Para o suporte, mande o `requisicao_id`. - **Webhooks**: confira a assinatura em todo evento, recuse timestamp com mais de 5 minutos, compare em tempo constante, use o `id` do evento para ignorar repetidos e responda rápido (trabalho pesado vai para a sua fila). - **Segredo do webhook** também é segredo de servidor. Troque com [`POST /webhooks/{id}/rotacionar`](https://docs.forsi.com.br/api/webhooks/rotacionar-webhook) se suspeitar de vazamento. - **Repita com Idempotency-Key**: no envio de mensagens e nas criações, para nunca duplicar. - **Dados pessoais**: guarde só o que a sua integração precisa e apague quando não precisar mais (LGPD). - **Mídia**: os links de `midia.url` precisam ser https públicos; não use links com credenciais na URL. ## Changelog ### 1.0.0 — outubro de 2026 Primeira versão pública da API v1. - Mensagens (texto, mídia e modelos), conversas, contatos, modelos aprovados e conta (`GET /me`). - Webhooks de saída com assinatura HMAC-SHA256, tentativas automáticas, entregas, reenvio e troca de segredo. - Funil e negócios: funis, etapas, negócios, mover, ganhar, perder, reabrir e notas. - Agenda: agendas, horários livres, compromissos (marcar, remarcar, confirmar, cancelar, presença) e bloqueios. - Equipe e permissões: pessoas, presença, convites, equipes (setores) e atribuição de conversas. - Relatórios: volume, tempo de resposta, equipe, caixas, agente de IA e créditos, em JSON ou CSV. - Automações do Instagram: contas conectadas, automações de comentário e regras de comentário (criar, editar, ligar, desligar e apagar). - Documentação em [docs.forsi.com.br](https://docs.forsi.com.br/), com uma página por guia, por recurso e por endpoint, e a versão `.md` de cada página. - Documentação para IA: `/llms.txt`, `/llms-full.txt` e os endereços de sempre, `https://chat.forsi.com.br/api/docs/llm.md` e `https://chat.forsi.com.br/api/docs/llm-resumo.md`. ### Versões O endereço traz a versão (`/api/v1`). Na v1 entram só mudanças compatíveis: rota nova, campo novo na resposta, evento novo, código de erro novo. O seu código deve ignorar campos e eventos que não conhece. Mudança que quebre integração não entra na v1 sem aviso nesta página. ## Objetos das respostas Campos de cada objeto devolvido em `dados`. Campos novos podem aparecer: ignore os que não conhecer. ### Eu | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `organizacao` | object | não | | | `organizacao.id` | uuid | não | | | `organizacao.nome` | string | não | | | `chave` | object | não | | | `chave.id` | uuid | não | | | `chave.nome` | string | não | | | `chave.prefixo` | string | não | | | `chave.escopos` | lista de string | não | | | `chave.expira_em` | data e hora ISO 8601 ou null | não | | | `limites` | object | não | | | `limites.por_minuto` | integer | não | | | `limites.org_por_minuto` | integer | não | | | `limites.envios_por_minuto` | integer | não | | | `limites.envios_org_por_minuto` | integer | não | | | `trava_spam_meta` | object | não | Trava de spam da Meta da organização (Configurações > API). | | `trava_spam_meta.ligada` | boolean | não | | | `trava_spam_meta.horas` | integer | não | Duração da restrição quando a Meta não manda data. | | `caixas` | lista de object | não | | | `caixas[].id` | uuid | não | | | `caixas[].nome` | string | não | | | `caixas[].canal` | string | não | | | `caixas[].telefone` | string ou null | não | | | `caixas[].conexoes` | lista de object | não | | ### EnvioAceito | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `mensagem_id` | uuid | não | | | `conversa_id` | uuid | não | | | `contato_id` | uuid ou null | não | | | `caixa_id` | uuid | não | | | `conversa_nova` | boolean | não | | | `estado` | "pendente" | não | | ### Mensagem | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `conversa_id` | uuid | não | | | `direcao` | "entrada" \| "saida" | não | | | `tipo` | string | não | ex.: texto | | `texto` | string | não | | | `estado` | "pendente" \| "enviando" \| "enviada" \| "entregue" \| "lida" \| "tocada" \| "falhou" \| "recebida" \| "nao_confirmado" | não | nao_confirmado: a conexão caiu durante o envio e o WhatsApp não confirmou; a mensagem pode ter chegado. Não é reenviada sozinha. | | `erro` | string | não | | | `autor_tipo` | "contato" \| "usuario" \| "agente" \| "sistema" \| "celular" | não | | | `responde_a_id` | uuid | não | | | `id_whatsapp` | string | não | | | `enviada_em` | data e hora ISO 8601 | não | | | `criado_em` | data e hora ISO 8601 | não | | | `editada_em` | data e hora ISO 8601 | não | | | `apagada_em` | data e hora ISO 8601 | não | | | `pela_api` | object | não | | | `pela_api.chave_id` | uuid | não | | | `modelo` | string | não | Nome do modelo, quando tipo = modelo. | | `midia` | lista de object | não | | | `midia[].id` | uuid | não | | | `midia[].mime` | string | não | | | `midia[].nome_arquivo` | string | não | | | `midia[].tamanho` | integer | não | | | `midia[].duracao_s` | number | não | | ### Conversa | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `caixa_id` | uuid | não | | | `grupo` | boolean | não | | | `titulo` | string ou null | não | | | `estado` | "aberta" \| "pendente" \| "adiada" \| "resolvida" | não | | | `responsavel_id` | uuid ou null | não | | | `equipe_id` | uuid ou null | não | | | `agente_id` | uuid ou null | não | | | `etiquetas` | lista de uuid | não | | | `nao_lidas` | integer | não | | | `janela_ate` | data e hora ISO 8601 ou null | não | Até quando a janela de 24 h da API oficial está aberta. | | `ultima_mensagem_em` | data e hora ISO 8601 ou null | não | | | `ultima_previa` | string ou null | não | | | `ultima_direcao` | string ou null | não | | | `criado_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | | `resolvida_em` | data e hora ISO 8601 ou null | não | | | `contato` | object ou null | não | | | `contato.id` | uuid | não | | | `contato.nome` | string ou null | não | | | `contato.telefone` | string ou null | não | | | `origem_anuncio` | object ou null | não | Primeiro anúncio que levou a pessoa ao WhatsApp, Instagram ou Messenger. | | `origem_anuncio.anuncio_id` | string ou null | não | | | `origem_anuncio.titulo` | string ou null | não | | | `origem_anuncio.canal` | string | não | | | `origem_anuncio.clicado_em` | data e hora ISO 8601 | não | | ### Contato | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string ou null | não | | | `nome_whatsapp` | string ou null | não | | | `telefone` | string ou null | não | | | `email` | string ou null | não | | | `instagram` | string ou null | não | | | `campos` | objeto livre | não | | | `bloqueado` | boolean | não | | | `criado_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | | `primeiro_anuncio` | object ou null | não | Primeiro anúncio que levou a pessoa ao WhatsApp, Instagram ou Messenger. | | `primeiro_anuncio.anuncio_id` | string ou null | não | | | `primeiro_anuncio.titulo` | string ou null | não | | | `primeiro_anuncio.canal` | string | não | | | `primeiro_anuncio.clicado_em` | data e hora ISO 8601 | não | | ### Apagado | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `apagado` | boolean | não | | | `conversas_apagadas` | integer | não | | ### Modelo | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `idioma` | string | não | | | `categoria` | string | não | | | `estado` | string | não | ex.: APPROVED | | `caixa_id` | uuid | não | | | `formato_parametros` | string | não | | | `variaveis` | lista de string | não | | | `texto` | string | não | | | `componentes` | array | não | | | `atualizado_em` | data e hora ISO 8601 | não | | ### Webhook | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `url` | string | não | | | `eventos` | lista de "mensagem.recebida" \| "mensagem.enviada" \| "mensagem.status" \| "conversa.criada" \| "conversa.atribuida" \| "conversa.encerrada" \| "contato.criado" \| "negocio.criado" \| "negocio.movido" \| "negocio.ganho" \| "negocio.perdido" \| "agendamento.criado" \| "agendamento.remarcado" \| "agendamento.cancelado" \| "agendamento.confirmado" \| "membro.convidado" \| "membro.entrou" \| "membro.alterado" \| "membro.removido" | não | | | `ativo` | boolean | não | | | `segredo_dica` | string | não | ex.: …a1B2c3 | | `segredo_anterior_ate` | data e hora ISO 8601 ou null | não | | | `falhas_seguidas` | integer | não | | | `falhando_desde` | data e hora ISO 8601 ou null | não | | | `desativado_em` | data e hora ISO 8601 ou null | não | | | `motivo_desativacao` | string ou null | não | | | `ultima_entrega_em` | data e hora ISO 8601 ou null | não | | | `ultimo_status` | integer ou null | não | | | `criado_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | | `entregas_24h` | object | não | | | `entregas_24h.entregues` | integer | não | | | `entregas_24h.falhas` | integer | não | | | `entregas_24h.pendentes` | integer | não | | ### TesteAceito | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `entrega_id` | uuid | não | | ### Entrega | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `webhook_id` | uuid | não | | | `evento` | string | não | | | `evento_id` | uuid | não | | | `estado` | "pendente" \| "falhou" \| "entregue" \| "morta" \| "cancelada" | não | | | `tentativas` | integer | não | | | `status_http` | integer ou null | não | | | `resposta` | string ou null | não | | | `erro` | string ou null | não | | | `duracao_ms` | integer ou null | não | | | `criado_em` | data e hora ISO 8601 | não | | | `ultima_tentativa_em` | data e hora ISO 8601 ou null | não | | | `proxima_em` | data e hora ISO 8601 ou null | não | | | `entregue_em` | data e hora ISO 8601 ou null | não | | | `payload` | Evento | não | | ### Funil | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `posicao` | integer | não | | | `etapas` | lista de Etapa | não | | | `criado_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | ### ApagadoFunil | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `apagado` | boolean | não | | | `negocios_apagados` | integer | não | | | `negocios_movidos` | integer | não | | ### Etapa | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `funil_id` | uuid | não | | | `nome` | string | não | | | `cor` | "slate" \| "blue" \| "violet" \| "amber" \| "emerald" \| "green" \| "rose" \| "cyan" \| "orange" | não | | | `posicao` | integer | não | Posição no funil, a partir de 0. | | `ganha` | boolean | não | | | `perdida` | boolean | não | | | `negocios` | integer | não | Quantos negócios estão na etapa. | | `criado_em` | data e hora ISO 8601 | não | | ### Negocio | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `funil_id` | uuid | não | | | `etapa_id` | uuid | não | | | `etapa` | string | não | Nome da etapa. | | `estado` | "aberto" \| "ganho" \| "perdido" | não | | | `titulo` | string | não | | | `valor_centavos` | integer | não | Em centavos. | | `responsavel_id` | uuid ou null | não | | | `previsao` | data AAAA-MM-DD ou null | não | | | `contato_id` | uuid ou null | não | | | `conversa_id` | uuid ou null | não | | | `contato` | object ou null | não | | | `contato.id` | uuid | não | | | `contato.nome` | string ou null | não | | | `contato.telefone` | string ou null | não | | | `ganho_em` | data e hora ISO 8601 ou null | não | | | `perdido_em` | data e hora ISO 8601 ou null | não | | | `motivo_perda` | string ou null | não | | | `posicao` | number | não | Ordem na coluna (menor = mais acima). | | `origem` | "usuario" \| "agente" \| "sistema" \| "api" | não | Quem criou. | | `notas` | integer | não | | | `criado_em` | data e hora ISO 8601 | não | | | `movido_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | ### NotaNegocio | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `negocio_id` | uuid | não | | | `texto` | string | não | | | `autor_id` | uuid ou null | não | | | `autor` | string ou null | não | | | `criado_em` | data e hora ISO 8601 | não | | ### RelatorioVolume | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.conversas_novas` | integer | não | | | `resumo.conversas_resolvidas` | integer | não | | | `resumo.mensagens_recebidas` | integer | não | | | `resumo.mensagens_enviadas` | integer | não | | | `resumo.mensagens_do_agente` | integer | não | Das enviadas, as que o agente de IA mandou. | | `resumo.notas_internas` | integer | não | | | `por_hora` | lista de object | não | 24 itens, hora do dia no fuso da organização. | | `por_hora[].hora` | integer | não | | | `por_hora[].mensagens_recebidas` | integer | não | | | `por_hora[].mensagens_enviadas` | integer | não | | | `linhas` | lista de object | não | Uma linha por dia, semana ou mês. | | `linhas[].inicio` | data AAAA-MM-DD | não | | | `linhas[].fim` | data AAAA-MM-DD | não | Último dia da linha (nas pontas do período, a linha é cortada). | | `linhas[].conversas_novas` | integer | não | | | `linhas[].conversas_resolvidas` | integer | não | | | `linhas[].mensagens_recebidas` | integer | não | | | `linhas[].mensagens_enviadas` | integer | não | | | `linhas[].mensagens_do_agente` | integer | não | | | `linhas[].notas_internas` | integer | não | | ### RelatorioTempo | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.primeiras_respostas` | integer | não | | | `resumo.sem_resposta` | integer | não | | | `resumo.mediana_primeira_resposta_s` | integer ou null | não | Mediana da primeira resposta (segundos) | | `resumo.p90_primeira_resposta_s` | integer ou null | não | 90% das conversas foram respondidas em até (segundos) | | `resumo.respondidas_em_ate_1_min_pct` | number ou null | não | | | `resumo.respondidas_em_ate_5_min_pct` | number ou null | não | | | `resumo.respondidas_em_ate_15_min_pct` | number ou null | não | | | `resumo.respondidas_em_ate_1_h_pct` | number ou null | não | Porcentagem (0 a 100) das conversas com primeira resposta em até 1 hora. | | `resumo.resolvidas` | integer | não | | | `resumo.mediana_resolucao_s` | integer ou null | não | Mediana até resolver (segundos) | | `resumo.p90_resolucao_s` | integer ou null | não | 90% das conversas foram resolvidas em até (segundos) | | `faixas_primeira_resposta` | lista de object | não | | | `faixas_primeira_resposta[].faixa` | string | não | ex.: 1 a 5 min | | `faixas_primeira_resposta[].quantidade` | integer | não | | | `faixas_resolucao` | lista de object | não | | | `faixas_resolucao[].faixa` | string | não | ex.: 1 a 5 min | | `faixas_resolucao[].quantidade` | integer | não | | | `linhas` | lista de object | não | Uma linha por dia, semana ou mês. | | `linhas[].inicio` | data AAAA-MM-DD | não | | | `linhas[].fim` | data AAAA-MM-DD | não | Último dia da linha (nas pontas do período, a linha é cortada). | | `linhas[].primeiras_respostas` | integer | não | | | `linhas[].mediana_primeira_resposta_s` | integer ou null | não | Mediana da primeira resposta (segundos) | | `linhas[].resolvidas` | integer | não | | | `linhas[].mediana_resolucao_s` | integer ou null | não | Mediana até resolver (segundos) | ### RelatorioAtendentes | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.atendentes` | integer | não | | | `linhas` | lista de object | não | Uma linha por pessoa da equipe. | | `linhas[].id` | uuid | não | | | `linhas[].nome` | string | não | | | `linhas[].papel` | string | não | | | `linhas[].conversas_atendidas` | integer | não | | | `linhas[].mensagens_enviadas` | integer | não | | | `linhas[].conversas_resolvidas` | integer | não | | | `linhas[].abertas_agora` | integer | não | | | `linhas[].primeiras_respostas` | integer | não | | | `linhas[].mediana_primeira_resposta_s` | integer ou null | não | Mediana da primeira resposta (segundos) | ### RelatorioCaixas | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.caixas` | integer | não | | | `linhas` | lista de object | não | Uma linha por caixa. | | `linhas[].id` | uuid | não | | | `linhas[].nome` | string | não | | | `linhas[].canal` | string | não | | | `linhas[].telefone` | string ou null | não | | | `linhas[].conversas_novas` | integer | não | | | `linhas[].conversas_resolvidas` | integer | não | | | `linhas[].conversas_com_mensagens` | integer | não | | | `linhas[].mensagens_recebidas` | integer | não | | | `linhas[].mensagens_enviadas` | integer | não | | ### RelatorioAgenteDeIa | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.conversas` | integer | não | | | `resumo.sem_agente` | integer | não | | | `resumo.com_agente` | integer | não | | | `resumo.passadas_para_a_equipe` | integer | não | | | `resumo.resolvidas_pelo_agente` | integer | não | | | `resumo.em_andamento` | integer | não | | | `resumo.taxa_resolucao` | number ou null | não | Resolvidas pelo agente / conversas com agente (0 a 1). | | `motivos_da_passagem` | lista de object | não | | | `motivos_da_passagem[].motivo` | string | não | | | `motivos_da_passagem[].quantidade` | integer | não | | | `linhas` | lista de object | não | Uma linha por agente de IA. | | `linhas[].id` | uuid | não | | | `linhas[].nome` | string | não | | | `linhas[].conversas_com_agente` | integer | não | | | `linhas[].passadas_para_a_equipe` | integer | não | | | `linhas[].resolvidas_pelo_agente` | integer | não | | | `linhas[].em_andamento` | integer | não | | ### RelatorioCreditos | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `periodo` | object | não | O período que valeu na consulta (com os padrões aplicados). | | `periodo.de` | data AAAA-MM-DD | não | | | `periodo.ate` | data AAAA-MM-DD | não | | | `periodo.dias` | integer | não | | | `periodo.fuso` | string | não | ex.: America/Sao_Paulo | | `periodo.granularidade` | "dia" \| "semana" \| "mes" | não | Só nos relatórios em série. | | `periodo.caixa_id` | uuid | não | | | `resumo` | object | não | | | `resumo.creditos_usados` | integer | não | Créditos gastos no período. | | `resumo.creditos_disponiveis` | integer | não | | | `resumo.creditos_do_plano` | integer | não | | | `resumo.creditos_extras` | integer | não | | | `resumo.renovam_em` | data e hora ISO 8601 ou null | não | | | `por_motivo` | lista de object | não | | | `por_motivo[].motivo` | string | não | ex.: resposta do agente | | `por_motivo[].creditos_usados` | integer | não | | | `por_motivo[].vezes` | integer | não | | | `linhas` | lista de object | não | Uma linha por dia, semana ou mês. | | `linhas[].inicio` | data AAAA-MM-DD | não | | | `linhas[].fim` | data AAAA-MM-DD | não | Último dia da linha (nas pontas do período, a linha é cortada). | | `linhas[].creditos_usados` | integer | não | | ### Agenda | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `cor` | string | não | | | `fuso` | string | não | ex.: America/Sao_Paulo | | `ativa` | boolean | não | | | `duracao_min` | integer | não | | | `intervalo_min` | integer | não | | | `antecedencia_min` | integer | não | | | `horario` | objeto livre | não | Dia da semana (0 domingo a 6 sábado) para faixas [início, fim]. | | `lembretes` | lista de object | não | | | `lembretes[].antes_min` | integer | não | | | `mensagem_lembrete` | string | não | | | `profissional` | object | não | Quando a agenda é de uma pessoa da equipe. | | `profissional.id` | uuid | não | | | `profissional.nome` | string | não | | | `criado_em` | data e hora ISO 8601 | não | | | `atualizado_em` | data e hora ISO 8601 | não | | ### Disponibilidade | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `fuso` | string | não | | | `duracao_min` | integer | não | | | `slots` | lista de Slot | não | | | `truncado` | boolean | não | Verdadeiro quando passou de 400 horários: estreite a janela. | ### Compromisso | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `agenda_id` | uuid | não | | | `agenda` | string | não | | | `cor` | string | não | | | `titulo` | string | não | | | `inicio` | data e hora ISO 8601 | não | | | `fim` | data e hora ISO 8601 | não | | | `fuso` | string | não | | | `inicio_local` | string | não | Começo no fuso da agenda, com deslocamento. ex.: 2026-10-12T14:00:00-03:00 | | `fim_local` | string | não | Fim no fuso da agenda, com deslocamento. ex.: 2026-10-12T14:00:00-03:00 | | `duracao_min` | integer | não | | | `estado` | "marcado" \| "confirmado" \| "concluido" \| "faltou" \| "cancelado" | não | | | `local` | string | não | | | `link` | string | não | | | `notas` | string | não | | | `contato_id` | uuid | não | | | `conversa_id` | uuid | não | | | `contato` | object | não | | | `contato.id` | uuid | não | | | `contato.nome` | string | não | | | `contato.telefone` | string | não | | | `contato.avatar_url` | string | não | | | `lembretes` | lista de object | não | | | `lembretes[].antes_min` | integer | não | | | `lembretes[].estado` | string | não | | | `lembretes[].enviar_em` | data e hora ISO 8601 | não | | | `lembretes[].enviado_em` | data e hora ISO 8601 | não | | | `criado_por_tipo` | string | não | | | `criado_em` | data e hora ISO 8601 | não | | | `confirmado_em` | data e hora ISO 8601 | não | | | `cancelado_em` | data e hora ISO 8601 | não | | ### Bloqueio | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `agenda_id` | uuid ou null | não | | | `titulo` | string | não | | | `tipo` | "folga" \| "almoco" \| "feriado" \| "outro" | não | | | `inicio` | data e hora ISO 8601 | não | | | `fim` | data e hora ISO 8601 | não | | | `fuso` | string | não | | | `inicio_local` | string | não | Começo no fuso da agenda, com deslocamento. ex.: 2026-10-12T14:00:00-03:00 | | `fim_local` | string | não | Fim no fuso da agenda, com deslocamento. ex.: 2026-10-12T14:00:00-03:00 | | `repetir` | "nunca" \| "diario" \| "semanal" \| "anual" | não | | | `repetir_ate` | data AAAA-MM-DD | não | | | `criado_em` | data e hora ISO 8601 | não | | ### Membro | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `email` | string | não | | | `avatar_url` | string | não | | | `papel` | "dono" \| "gestor" \| "atendente" | não | | | `papel_de_acesso_id` | uuid | não | | | `permissoes` | object | não | | | `permissoes.caixas` | lista de uuid | não | WhatsApps visíveis. Vazio = todos. | | `permissoes.modulos` | lista de string | não | Módulos liberados. Vazio = todos. | | `permissoes.agenda` | "todas" \| "propria" | não | | | `equipes` | lista de uuid | não | | | `disponivel` | boolean | não | | | `disponivel_agora` | boolean | não | | | `ausente_ate` | data e hora ISO 8601 | não | | | `horario` | objeto livre | não | {"dias": [1,2,3,4,5], "inicio": "08:00", "fim": "18:00"} (0 = domingo). | | `ultimo_acesso` | data e hora ISO 8601 | não | | | `entrou_em` | data e hora ISO 8601 | não | | ### ApagadoEquipe | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `apagado` | boolean | não | | ### Presenca | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `papel` | "dono" \| "gestor" \| "atendente" | não | | | `disponivel` | boolean | não | | | `disponivel_agora` | boolean | não | | | `ausente_ate` | data e hora ISO 8601 | não | | | `horario` | objeto livre | não | | | `ultimo_acesso` | data e hora ISO 8601 | não | | ### Convite | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `email` | string | não | | | `papel` | "dono" \| "gestor" \| "atendente" | não | | | `papel_de_acesso_id` | uuid | não | | | `convidado_por` | string | não | Nome de quem convidou pela tela. Convites da API não têm. | | `expira_em` | data e hora ISO 8601 | não | | | `criado_em` | data e hora ISO 8601 | não | | ### Equipe | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `cor` | string | não | | | `distribuicao` | "manual" \| "rodizio" \| "menos_ocupado" | não | | | `ordem` | integer | não | | | `membros` | lista de uuid | não | | | `criado_em` | data e hora ISO 8601 | não | | ### ContaInstagram | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `caixa_id` | uuid ou null | não | | | `usuario` | string ou null | não | | | `nome` | string ou null | não | | ### AutomacaoResumo | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | ### RegraComentario | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `id` | uuid | não | | | `nome` | string | não | | | `ativo` | boolean | não | | | `conexao_id` | uuid | não | | | `post_id` | string ou null | não | | | `palavras` | lista de string | não | | | `modo` | "contem" \| "igual" | não | | | `qualquer_comentario` | boolean | não | | | `automacao_id` | uuid ou null | não | | | `automacao` | string ou null | não | Nome da automação. | | `responder_publico` | string ou null | não | | | `resposta_privada` | string ou null | não | | | `respostas_publicas` | lista de string | não | | | `ocultar_apos` | boolean | não | | | `botoes` | lista de string | não | | | `botoes_detalhe` | lista de object | não | Os mesmos botões com o link: `url` não aparece no botão que só devolve o toque. | | `botoes_detalhe[].titulo` | string | não | | | `botoes_detalhe[].url` | string | não | | | `tipo` | "comum" \| "seguir" | não | `comum` ou `seguir` ("Seguir para receber"). Regras antigas são `comum`. | | `seguir` | object ou null | não | Só no tipo `seguir`: completo, com os padrões aplicados. Não aparece no tipo `comum`; dentro dele, `conteudo.botao`, `max_tentativas` e `texto_limite` também não aparecem enquanto forem null. | | `seguir.botao_receber` | string | não | Botão da mensagem do comentário (1 a 20). Padrão: "Quero receber". | | `seguir.texto_nao_segue` | string | não | Para quem ainda não segue (1 a 640). | | `seguir.botao_estou_seguindo` | string | não | Botão para avisar que passou a seguir (1 a 20). Padrão: "Estou seguindo". | | `seguir.texto_ainda_nao_segue` | string | não | Quando toca em "Estou seguindo" e ainda não segue (1 a 640). | | `seguir.texto_ja_segue` | string | não | Antes do conteúdo, para quem já seguia (0 a 640; "" = não envia). | | `seguir.texto_seguiu_depois` | string | não | Antes do conteúdo, para quem acabou de seguir (0 a 640; "" = não envia). | | `seguir.conteudo` | object | não | O que é entregue. | | `seguir.max_tentativas` | integer ou null | não | null = confere até seguir (no máximo 50 vezes). mín. 1 | | `seguir.texto_limite` | string ou null | não | Enviada uma vez quando as tentativas acabam (1 a 640). | | `seguir.se_nao_verificar` | "enviar" \| "pedir_de_novo" | não | Quando a Meta não deixa conferir. Padrão: `enviar`. | | `estatisticas` | object ou null | não | Só no tipo `seguir`. Contagem desde que a regra existe. | | `estatisticas.pediram` | integer | não | Pessoas que receberam o botão. | | `estatisticas.entregues` | integer | não | Pessoas que receberam o conteúdo. | | `estatisticas.ja_seguiam` | integer | não | Já seguiam ao pedir. | | `estatisticas.seguiram_depois` | integer | não | Passaram a seguir depois de pedir. | | `estatisticas.aguardando` | integer | não | Ainda esperando tocar ou seguir. | | `atualizado_em` | data e hora ISO 8601 | não | | | `execucoes_7d` | integer | não | Disparos nos últimos 7 dias. | | `ultima_em` | data e hora ISO 8601 ou null | não | | ### ApagadoRegra | Campo | Tipo | Obrigatório | Notas | | --- | --- | --- | --- | | `apagado` | boolean | não | |