============================================================================== Atlas API v1.0.0 — referencia completa em texto plano ============================================================================== Este arquivo existe para ser colado inteiro dentro de um modelo de linguagem. Ele contem toda a API do Atlas: endpoints, parametros, campos, tipos, erros e regras de negocio. Nao ha markup, nao ha indentacao profunda e nada esta abreviado. Gerado a partir do openapi.yaml, que e a fonte da verdade. BASE: https://atlas.skill-hub.app/api/v1 API REST do Atlas, o CRM da Skill-Hub. Esta especificação descreve exatamente o que a API faz hoje em produção. Onde existe uma inconsistência entre recursos, ela está documentada em vez de escondida. --- AUTENTICAÇÃO --- Todo request precisa de dois cabeçalhos: Authorization: Bearer SEU_TOKEN Accept: application/json O Accept: application/json não é opcional. Sem ele, um request sem token ou com token inválido recebe um 302 para a tela de login em HTML, em vez de um 401 em JSON. O token é gerado em Configurações → Tokens de API e é exibido uma única vez. São 40 caracteres. Guarde no momento da criação. --- ESCOPOS DO TOKEN --- Cada token carrega um ou mais escopos, escolhidos na criação e não editáveis depois. Para mudar o escopo, revogue o token e crie outro. - read: leitura. Todos os GET. - write: criar, alterar e apagar dados: contatos, conversas (status e atribuição), faturas, propostas, agendamentos, produtos e webhooks. - send: disparos que saem para o cliente ou para terceiros e consomem crédito: enviar mensagem (POST /messages/send e POST /conversations/{id}/messages), enviar template (POST /wa-connector/send-template), enviar e-mail por uma caixa (POST /emails/send) e testar webhook (POST /webhooks/{id}/test). send é separado de propósito: um token só de leitura e escrita nunca dispara mensagem por acidente. write não implica send. Tokens criados antes dos escopos têm acesso total (*) e continuam valendo para tudo. Cada operação declara o escopo exigido em x-scope. Sem ele, a resposta é 403 com código de máquina: { "error": "Este token não tem o escopo 'send'. Crie um token com esse escopo em Configurações → Tokens de API.", "code": "insufficient_scope", "required_scope": "send" } --- NEGÓCIOS: OS DOIS INTERRUPTORES --- Mover, ganhar ou perder um negócio pela tela dispara automações (WhatsApp, tarefa, e-mail), grava métrica de conversão e envia eventos para a conta de anúncios do cliente (Meta CAPI). Pela API, nada disso acontece por padrão. POST /deals, /deals/{id}/move, /win e /lose aceitam dois interruptores, ambos false se omitidos: | Parâmetro | Liga | Exige | |---|---|---| | trigger_automations | automações da etapa (WhatsApp, tarefas, e-mail, notificações) e a métrica de conversão | escopo send | | fire_pixel_events | eventos da Meta Conversions API (Purchase no ganho, eventos mapeados por etapa) | escopo send | Sem os interruptores, um token write move o card, registra a atividade e nada sai da conta. Com qualquer um ligado, a chamada envia mensagem ou grava conversão em terceiro, e por isso exige também o escopo send; sem ele, 403 insufficient_scope com required_scope: send. Uma sincronização de 200 negócios sem as flags não dispara 200 automações nem 200 conversões falsas. PATCH /deals/{id} nunca muda etapa nem status: stage_id, status, pipeline_id e afins respondem 422 mandando usar /move, /win ou /lose. O caminho perigoso é um só. --- PRÉ-REQUISITOS DA CONTA --- Além do token, o acesso à API exige que a licença do ambiente tenha a API habilitada e que o addon "Acesso à API" esteja ativo no tenant. Sem isso, todo request responde 403. --- ESCOPO DE DADOS --- O tenant_id nunca é aceito no corpo de um request. Ele é derivado do token. Um recurso que pertença a outra conta responde 404, não 403. --- LIMITE DE REQUISIÇÕES --- 60 requisições por minuto, por token, em janela fixa de 60 segundos. Toda resposta traz X-RateLimit-Limit e X-RateLimit-Remaining. Ao estourar, a resposta é 429 com Retry-After: 60. --- PAGINAÇÃO --- A maior parte das listagens aceita page e per_page (padrão 50, teto 100) e responde com um envelope meta: { "data": [], "meta": { "current_page": 1, "last_page": 10, "per_page": 50, "total": 500 } } Três exceções, documentadas em cada endpoint: /webhooks e /wa-connector/* não paginam, e /conversations/{id}/messages usa cursor (before_id, after_id, limit) com has_more no lugar de meta. --- DATAS --- ISO 8601 com offset (2026-09-03T19:57:10-03:00), exceto connected_at e last_synced_at do WhatsApp Connector, que vêm em UTC com Z. Datas puras usam YYYY-MM-DD. --- VALORES MONETÁRIOS --- Devolvidos como número (float), não como string. --- INCONSISTÊNCIAS CONHECIDAS --- Documentadas aqui porque existem hoje, e não porque sejam desejáveis: - discount_type aceita percent em faturas e percentage em propostas. - A ordenação padrão (sort_dir) é desc em contatos, conversas, faturas e propostas, e asc em agendamentos e produtos. - O campo filename de uma mensagem sempre responde null. - Oito eventos de webhook são aceitos no cadastro mas nunca são disparados. Veja a seção de Webhooks. ============================================================================== ENDPOINTS ============================================================================== ############################################################################## # CONTATOS ############################################################################## GET /contacts resumo: Listar contatos escopo exigido: read parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] type (query): lead|customer|prospect|supplier - Igualdade exata. person_type (query): individual|company search (query): string - Busca parcial, sem diferenciar maiúsculas, em nome, nome fantasia, e-mail, telefone, celular e documento. group_id (query): integer assigned_to (query): integer source (query): string - Igualdade exata e sensível a maiúsculas. A base contém grafias diferentes para a mesma origem, então prefira listar e filtrar do seu lado quando precisar de precisão. is_active (query): boolean tags (query): array - Array ou lista separada por vírgula. As tags são combinadas com E, não com OU. sort_by (query): name|email|type|created_at|updated_at|last_contact_at [padrao created_at] sort_dir (query): asc|desc [padrao desc] - Qualquer valor diferente de asc é tratado como desc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /contacts resumo: Criar contato escopo exigido: write nota: Aplica os padrões type = lead e person_type = individual quando os campos não são enviados, e grava o usuário do token em created_by. nota: Campos de rastreamento (utm_*, captured_at) não são aceitos. corpo (application/json): name: string [obrigatorio] [max 255] trading_name: string? [max 255] email: string(email)? [max 255] phone: string? [max 50] mobile: string? [max 50] website: string? [max 255] company: string? [max 255] position: string? [max 255] document: string? [max 50] state_registration: string? [max 50] municipal_registration: string? [max 50] person_type: individual|company [padrao individual] type: lead|customer|prospect|supplier [padrao lead] source: string? [max 100] contact_group_id: integer? assigned_to: integer? parent_id: integer? address: string? [max 255] address_number: string? [max 20] address_complement: string? [max 100] neighborhood: string? [max 100] city: string? [max 100] state: string? [max 50] zip_code: string? [max 20] country: string? [max 100] birth_date: string(date)? tags: array notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... notification_settings: object? respostas: 201: Criado. Devolve a representação detalhada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 422: Validação, incluindo campos personalizados. Quando algum campo é inválido ou a chave não existe, nada do request é gravado. 429: Limite de 60 requisições por minuto estourado. GET /contacts/{contact} resumo: Obter contato escopo exigido: read nota: Devolve a representação detalhada, incluindo employees quando o contato for uma empresa com pessoas vinculadas. parametros: contact (path): integer [obrigatorio] respostas: 200: OK 401: Token ausente, inválido ou expirado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /contacts/{contact} resumo: Atualizar contato escopo exigido: write nota: Grava o usuário do token em updated_by. nota: Aceita dois campos que a criação não aceita, is_active e bot_protected. nota: PATCH faz o mesmo. nota: Campos personalizados são parciais: só as chaves enviadas mudam e null apaga o valor do campo. parametros: contact (path): integer [obrigatorio] corpo (application/json): name: string [obrigatorio] [max 255] trading_name: string? [max 255] email: string(email)? [max 255] phone: string? [max 50] mobile: string? [max 50] website: string? [max 255] company: string? [max 255] position: string? [max 255] document: string? [max 50] state_registration: string? [max 50] municipal_registration: string? [max 50] person_type: individual|company [padrao individual] type: lead|customer|prospect|supplier [padrao lead] source: string? [max 100] contact_group_id: integer? assigned_to: integer? parent_id: integer? address: string? [max 255] address_number: string? [max 20] address_complement: string? [max 100] neighborhood: string? [max 100] city: string? [max 100] state: string? [max 50] zip_code: string? [max 20] country: string? [max 100] birth_date: string(date)? tags: array notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... notification_settings: object? is_active: boolean? bot_protected: boolean? respostas: 200: Atualizado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Validação, incluindo campos personalizados. Quando algum campo é inválido ou a chave não existe, nada do request é gravado. PATCH /contacts/{contact} resumo: Atualizar contato (parcial) escopo exigido: write nota: Igual ao PUT. nota: Só os campos enviados mudam; em custom_fields, null apaga o valor do campo. parametros: contact (path): integer [obrigatorio] corpo (application/json): name: string [obrigatorio] [max 255] trading_name: string? [max 255] email: string(email)? [max 255] phone: string? [max 50] mobile: string? [max 50] website: string? [max 255] company: string? [max 255] position: string? [max 255] document: string? [max 50] state_registration: string? [max 50] municipal_registration: string? [max 50] person_type: individual|company [padrao individual] type: lead|customer|prospect|supplier [padrao lead] source: string? [max 100] contact_group_id: integer? assigned_to: integer? parent_id: integer? address: string? [max 255] address_number: string? [max 20] address_complement: string? [max 100] neighborhood: string? [max 100] city: string? [max 100] state: string? [max 50] zip_code: string? [max 20] country: string? [max 100] birth_date: string(date)? tags: array notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... notification_settings: object? is_active: boolean? bot_protected: boolean? respostas: 200: Atualizado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Validação, incluindo campos personalizados. Quando algum campo é inválido ou a chave não existe, nada do request é gravado. DELETE /contacts/{contact} resumo: Remover contato escopo exigido: write nota: Exclusão lógica. nota: O registro deixa de aparecer nas listagens mas não é apagado do banco. parametros: contact (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. GET /contacts/{contact}/deals resumo: Negócios do contato escopo exigido: read nota: Ordenado por data de criação, do mais novo para o mais antigo. nota: Sem filtros. parametros: contact (path): integer [obrigatorio] page (query): integer [padrao 1] per_page (query): integer [padrao 25] respostas: 200: Lista paginada. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. GET /contacts/{contact}/conversations resumo: Conversas do contato escopo exigido: read nota: Ordenado pela última mensagem, da mais recente para a mais antiga. nota: Sem filtros. parametros: contact (path): integer [obrigatorio] page (query): integer [padrao 1] per_page (query): integer [padrao 25] respostas: 200: Lista paginada. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. ############################################################################## # FUNIS E ETAPAS ############################################################################## GET /pipelines resumo: Listar funis escopo exigido: read nota: Funis ativos da conta, em ordem alfabética, cada um com suas etapas já ordenadas (inclusive as de ganho e de perda). nota: Sem paginação. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. GET /pipelines/{pipeline}/stages resumo: Listar etapas de um funil escopo exigido: read nota: Etapas do funil em sort_order. nota: Por padrão exclui as etapas de ganho e de perda; passe include_won_lost=true para incluí-las. nota: Funil de outra conta responde 404. parametros: pipeline (path): integer [obrigatorio] include_won_lost (query): boolean [padrao False] respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 429: Limite de 60 requisições por minuto estourado. ############################################################################## # NEGÓCIOS ############################################################################## GET /deals resumo: Listar negócios escopo exigido: read parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] pipeline_id (query): integer stage_id (query): integer assigned_to (query): integer contact_id (query): integer status (query): open|won|lost created_from (query): string(date) - Criação, a partir de (data ou data-hora). created_to (query): string(date) stage_changed_from (query): string(date) - Entrada na etapa atual, a partir de. stage_changed_to (query): string(date) value_min (query): number value_max (query): number search (query): string - Busca parcial no título do negócio e no nome do contato. sort_by (query): created_at|updated_at|value|title|expected_close_date|stage_changed_at [padrao created_at] sort_dir (query): asc|desc [padrao desc] - Qualquer valor diferente de asc é tratado como desc. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /deals resumo: Criar negócio escopo exigido: write nota: Cria o negócio na etapa informada, que precisa pertencer ao funil informado (GET /pipelines). nota: Contato, responsável, origem e campanha são verificados contra a sua conta. nota: Se conversation_id vier, é vinculado após verificação de posse; se não vier e o contato tiver exatamente uma conversa, ela é vinculada. nota: Na criação o único efeito colateral existente é a automação deal_created, controlada por trigger_automations (padrão false, exige send). nota: fire_pixel_events é aceito por consistência e não tem efeito aqui. corpo (application/json): title: string [obrigatorio] [max 255] pipeline_id: integer [obrigatorio] stage_id: integer [obrigatorio] - Etapa do funil informado. contact_id: integer? assigned_to: integer? value: number [padrao 0] currency: string [padrao BRL] [max 10] expected_close_date: string(date)? notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... source_id: integer? campaign_id: integer? conversation_id: integer? trigger_automations: boolean [padrao False] fire_pixel_events: boolean [padrao False] respostas: 201: Criado 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 422: Validação, incluindo campos personalizados. Quando algum campo é inválido ou a chave não existe, nada do request é gravado. 429: Limite de 60 requisições por minuto estourado. GET /deals/{deal} resumo: Ler negócio escopo exigido: read parametros: deal (path): integer [obrigatorio] respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 429: Limite de 60 requisições por minuto estourado. PATCH /deals/{deal} resumo: Alterar dados do negócio escopo exigido: write nota: Altera título, valor, moeda, responsável, contato, data prevista, notas, campos personalizados, origem, campanha e conversa vinculada. nota: Campos personalizados no PATCH são parciais: só as chaves enviadas mudam; as que não vierem ficam como estão; null apaga o valor daquele campo. nota: Se qualquer campo for inválido, a resposta é 422 e nada do request é gravado. nota: Não muda etapa nem status. nota: stage_id, status, pipeline_id, probability, closed_at, lost_at, loss_reason_id, won e lost respondem 422 mandando usar /move, /win ou /lose. parametros: deal (path): integer [obrigatorio] corpo (application/json): title: string [max 255] contact_id: integer? assigned_to: integer? value: number currency: string [max 10] expected_close_date: string(date)? notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... source_id: integer? campaign_id: integer? conversation_id: integer? respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Validação, campo personalizado inválido (ver CustomFieldValidationError), ou tentativa de mudar etapa/status por PATCH: {"message":"…","errors":{"stage_id":["Este campo não pode ser alterado por... 429: Limite de 60 requisições por minuto estourado. GET /deals/{deal}/documents resumo: Listar documentos escopo exigido: read nota: Lista os arquivos da aba Documentos do negócio, do mais novo para o mais antigo. nota: Cada item traz um link assinado novo em url, válido por 30 minutos (url_expires_at). parametros: deal (path): integer [obrigatorio] respostas: 200: Lista de documentos. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 429: Limite de 60 requisições por minuto estourado. POST /deals/{deal}/documents resumo: Anexar documento escopo exigido: write nota: Anexa um arquivo na aba Documentos do negócio. nota: multipart/form-data, campo file. nota: Mesmos limites da tela: até 50 MB; arquivos executáveis ou que o navegador interpreta (php, html, svg, js, exe…) são recusados. nota: Mesmo limite de requisições por minuto dos outros endpoints. nota: O arquivo fica em área privada. nota: A resposta traz url, um link assinado e temporário para baixar o arquivo sem token, válido por 30 minutos (url_expires_at). nota: Depois disso o link responde 403; para baixar de novo, chame GET /deals/{deal}/documents, que devolve links novos. nota: Não guarde a URL como endereço permanente do arquivo. parametros: deal (path): integer [obrigatorio] corpo alternativo: multipart/form-data (mesmo formato, mais o arquivo em "media") respostas: 201: Anexado. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. 429: Limite de 60 requisições por minuto estourado. POST /deals/{deal}/move resumo: Mover de etapa escopo exigido: write nota: Move o negócio para a etapa informada, que pode ser de outro funil da mesma conta. nota: Etapa de ganho ou de perda responde 422: use /win ou /lose. nota: Registra a atividade com o nome do token. nota: Sem os interruptores: só a etapa muda. nota: Com trigger_automations=true as automações da etapa disparam e a métrica de conversão é gravada; com fire_pixel_events=true o evento mapeado da etapa vai para a Meta CAPI. nota: Qualquer um ligado exige o escopo send. parametros: deal (path): integer [obrigatorio] corpo (application/json): stage_id: integer [obrigatorio] trigger_automations: boolean [padrao False] fire_pixel_events: boolean [padrao False] respostas: 200: Movido 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. 429: Limite de 60 requisições por minuto estourado. POST /deals/{deal}/win resumo: Marcar como ganho escopo exigido: write nota: Status won, etapa de ganho do funil, closed_at hoje e o contato vira cliente. nota: Negócio já ganho responde 422. nota: Sem os interruptores nada mais acontece. nota: Com trigger_automations=true a automação deal_won dispara e a venda entra na métrica de conversão; com fire_pixel_events=true a Meta CAPI recebe Purchase (ou o evento mapeado). nota: Purchase falso na conta de anúncios não se desfaz. nota: Qualquer um ligado exige o escopo send. parametros: deal (path): integer [obrigatorio] corpo (application/json): trigger_automations: boolean [padrao False] fire_pixel_events: boolean [padrao False] respostas: 200: Ganho 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Negócio já ganho. 429: Limite de 60 requisições por minuto estourado. POST /deals/{deal}/lose resumo: Marcar como perdido escopo exigido: write nota: Status lost, etapa de perda do funil, lost_at agora, motivo e notas de perda. nota: O motivo precisa ser da sua conta. nota: Negócio já perdido responde 422. nota: Interruptores como em /win (deal_lost e evento mapeado). parametros: deal (path): integer [obrigatorio] corpo (application/json): loss_reason_id: integer? loss_notes: string? [max 1000] trigger_automations: boolean [padrao False] fire_pixel_events: boolean [padrao False] respostas: 200: Perdido 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. 429: Limite de 60 requisições por minuto estourado. GET /deals/{deal}/activities resumo: Histórico do negócio escopo exigido: read nota: Atividades do mais recente para o mais antigo, com page e per_page (padrão 50, teto 100). parametros: deal (path): integer [obrigatorio] page (query): integer [padrao 1] per_page (query): integer [padrao 50] respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 429: Limite de 60 requisições por minuto estourado. ############################################################################## # CAMPOS PERSONALIZADOS ############################################################################## GET /custom-fields resumo: Listar campos personalizados escopo exigido: read nota: Definições dos campos personalizados da sua conta: é daqui que saem as chaves que custom_fields aceita em negócios e contatos. nota: Por padrão só os ativos. parametros: entity_type (query): deal|contact - Sem ele, vêm os dois. include_inactive (query): boolean [padrao False] respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 422: Falha de validação. Formato padrão do framework. 429: Limite de 60 requisições por minuto estourado. ############################################################################## # CONVERSAS ############################################################################## GET /conversations resumo: Listar conversas escopo exigido: read parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] status (query): open|pending|resolved channel_id (query): integer assigned_to (query): integer contact_id (query): integer search (query): string - Busca parcial em nome exibido, identificador remoto e prévia da última mensagem. is_group (query): boolean sort_by (query): last_message_at|created_at|unread_count [padrao last_message_at] sort_dir (query): asc|desc [padrao desc] - Qualquer valor diferente de asc é tratado como desc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. GET /conversations/{conversation} resumo: Obter conversa escopo exigido: read parametros: conversation (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /conversations/{conversation} resumo: Atualizar conversa escopo exigido: write nota: Enviar status: resolved executa a rotina de resolução da conversa, registrando quem resolveu. nota: Cuidado com dois comportamentos: - assigned_user_id só atribui se o usuário pertencer à sua conta. nota: Se não pertencer, o valor é ignorado em silêncio e a resposta é 200. nota: - assigned_department_id é gravado sem essa verificação. nota: Envie null em assigned_user_id para desatribuir. parametros: conversation (path): integer [obrigatorio] corpo (application/json): status: open|pending|resolved assigned_user_id: integer? assigned_department_id: integer? respostas: 200: Atualizada. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. GET /conversations/{conversation}/messages resumo: Listar mensagens escopo exigido: read nota: Paginação por cursor, e não por página. nota: A resposta traz has_more no lugar de meta. nota: As mensagens vêm da mais antiga para a mais nova. nota: Mensagens apagadas não são incluídas. parametros: conversation (path): integer [obrigatorio] before_id (query): integer - Devolve mensagens anteriores a este id. Use para carregar histórico. after_id (query): integer - Devolve mensagens posteriores a este id. Use para buscar novidades. limit (query): integer [padrao 50] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. POST /conversations/{conversation}/messages resumo: Enviar mensagem em uma conversa escopo exigido: send nota: Envia para o canal já vinculado à conversa. nota: É obrigatório ao menos um entre content, media e media_url. nota: Efeitos colaterais, que acontecem sempre: - A mensagem é marcada com metadata.via = "api". nota: - Se a conversa não tiver responsável, ela é atribuída ao usuário do token. nota: - O status da conversa passa para open. nota: Áudio é enviado como mensagem de voz. nota: Quando há mídia sem type, o padrão é document. parametros: conversation (path): integer [obrigatorio] corpo (application/json): content: string? [max 10000] type: text|image|document|video|audio media_url: string(uri)? filename: string? [max 255] corpo alternativo: multipart/form-data (mesmo formato, mais o arquivo em "media") respostas: 201: Enviada. 400: O canal recusou o envio. Ocorre com canal desconectado (WhatsApp channel is disconnected.), com erro devolvido pelo provedor, ou com falha genérica de envio. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação, ou nenhum conteúdo enviado. POST /messages/send resumo: Enviar mensagem por telefone escopo exigido: send nota: Envia sem conhecer a conversa. nota: Dois modos: - Com conversation_id: usa a conversa existente e o canal dela. nota: - Sem conversation_id: channel_id passa a ser obrigatório. nota: O telefone é reduzido a dígitos e a conversa é criada se ainda não existir. nota: Embora phone seja declarado como opcional, ele é obrigatório na prática no segundo modo. corpo (application/json): content: string? [max 10000] type: text|image|document|video|audio media_url: string(uri)? filename: string? [max 255] phone: string [max 50] - Obrigatório quando não há conversation_id. conversation_id: integer? channel_id: integer - Obrigatório quando não há conversation_id. corpo alternativo: multipart/form-data (mesmo formato, mais o arquivo em "media") respostas: 201: Enviada. 400: Canal desconectado ou erro do provedor. 404: Conversa ou canal não encontrado. As mensagens possíveis são Conversation not found., Channel not found for this conversation. e Channel not found. 422: Falha de validação. Formato padrão do framework. 500: Não foi possível criar a conversa. ############################################################################## # FATURAS ############################################################################## GET /invoices resumo: Listar faturas escopo exigido: read parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] status (query): draft|pending|sent|paid|cancelled contact_id (query): integer search (query): string - Busca parcial em número e título. due_from (query): string(date) due_to (query): string(date) is_recurring (query): boolean sort_by (query): created_at|due_date|total|number|status [padrao created_at] sort_dir (query): asc|desc [padrao desc] - Qualquer valor diferente de asc é tratado como desc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /invoices resumo: Criar fatura escopo exigido: write nota: Os totais são calculados pelo servidor a partir dos itens. nota: Não envie subtotal nem total. nota: Padrões aplicados: issue_date igual a hoje, currency igual a BRL, discount_type igual a fixed, status igual a draft, unit igual a un, e o gateway de pagamento padrão da conta. nota: Atenção: o contact_id enviado não é verificado contra a sua conta. corpo (application/json): contact_id: integer [obrigatorio] title: string? [max 255] description: string? [max 5000] issue_date: string(date)? due_date: string(date) [obrigatorio] currency: string? [padrao BRL] [max 10] discount_amount: number? discount_type: fixed|percent [padrao fixed] notes: string? [max 5000] terms: string? [max 5000] items: array [obrigatorio] []name: string []description: string? []quantity: number []unit_price: number []unit: string? []product_id: integer? []tax_rate: number? []discount_amount: number? []discount_type: fixed|percent respostas: 201: Criada. 422: Falha de validação. Formato padrão do framework. GET /invoices/{invoice} resumo: Obter fatura escopo exigido: read parametros: invoice (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /invoices/{invoice} resumo: Atualizar fatura escopo exigido: write nota: Não aceita itens e não recalcula os totais. nota: Para mudar os itens de uma fatura, crie uma nova. parametros: invoice (path): integer [obrigatorio] corpo (application/json): contact_id: integer title: string? [max 255] description: string? [max 5000] issue_date: string(date)? due_date: string(date) status: draft|pending|sent|paid|cancelled currency: string? [max 10] discount_amount: number? discount_type: fixed|percent notes: string? [max 5000] terms: string? [max 5000] respostas: 200: Atualizada. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. DELETE /invoices/{invoice} resumo: Remover fatura escopo exigido: write parametros: invoice (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. GET /invoices/{invoice}/payments resumo: Listar pagamentos da fatura escopo exigido: read parametros: invoice (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. POST /invoices/{invoice}/payments resumo: Registrar pagamento escopo exigido: write nota: Registra um pagamento e atualiza o status da fatura. nota: Uma fatura em status que não aceita pagamento responde 422 antes mesmo da validação dos campos. nota: Padrões: method igual a manual e paid_at igual a agora. parametros: invoice (path): integer [obrigatorio] corpo (application/json): amount: number [obrigatorio] method: pix|boleto|credit_card|debit_card|transfer|manual notes: string? [max 1000] paid_at: string(date-time)? respostas: 201: Registrado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Validação, ou a fatura não aceita pagamento no status atual. ############################################################################## # PROPOSTAS ############################################################################## GET /proposals resumo: Listar propostas escopo exigido: read parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] status (query): draft|sent|accepted|rejected|expired contact_id (query): integer deal_id (query): integer search (query): string - Busca parcial em número, título e assunto. sort_by (query): created_at|proposal_date|valid_until|total|number|status [padrao created_at] sort_dir (query): asc|desc [padrao desc] - Qualquer valor diferente de asc é tratado como desc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /proposals resumo: Criar proposta escopo exigido: write nota: Totais calculados pelo servidor. nota: Padrões: assigned_to igual ao usuário do token, proposal_date igual a hoje, valid_until igual a hoje mais 30 dias, currency igual a BRL e status igual a draft. nota: Atenção ao discount_type: aqui os valores são fixed e percentage, diferente das faturas, que usam percent. corpo (application/json): contact_id: integer [obrigatorio] deal_id: integer? assigned_to: integer? title: string? [max 255] subject: string? [max 255] proposal_date: string(date)? valid_until: string(date)? currency: string? [padrao BRL] [max 10] discount_amount: number? discount_type: fixed|percentage content: string? notes: string? [max 5000] terms: string? [max 5000] items: array [obrigatorio] []name: string []description: string? []quantity: number []unit_price: number []unit: string? []product_id: integer? []tax_rate: number? []discount_amount: number? []discount_type: fixed|percentage []is_optional: boolean? respostas: 201: Criada. 422: Falha de validação. Formato padrão do framework. GET /proposals/{proposal} resumo: Obter proposta escopo exigido: read parametros: proposal (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /proposals/{proposal} resumo: Atualizar proposta escopo exigido: write nota: Não aceita itens. parametros: proposal (path): integer [obrigatorio] corpo (application/json): contact_id: integer deal_id: integer? assigned_to: integer? title: string? [max 255] subject: string? [max 255] proposal_date: string(date)? valid_until: string(date)? status: draft|sent|accepted|rejected|expired currency: string? [max 10] discount_amount: number? discount_type: fixed|percentage content: string? notes: string? [max 5000] terms: string? [max 5000] respostas: 200: Atualizada. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. DELETE /proposals/{proposal} resumo: Remover proposta escopo exigido: write parametros: proposal (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. ############################################################################## # AGENDAMENTOS ############################################################################## GET /appointments resumo: Listar agendamentos escopo exigido: read nota: A ordenação padrão aqui é crescente, diferente dos outros recursos. parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] status (query): scheduled|confirmed|cancelled|completed type (query): meeting|call|task|event|other user_id (query): integer contact_id (query): integer deal_id (query): integer start_from (query): string(date-time) start_to (query): string(date-time) search (query): string - Busca parcial em título, descrição e local. sort_by (query): start_at|end_at|created_at|title|status [padrao start_at] sort_dir (query): asc|desc [padrao asc] - Qualquer valor diferente de desc é tratado como asc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /appointments resumo: Criar agendamento escopo exigido: write nota: Padrões: user_id igual ao usuário do token, type igual a meeting, status igual a scheduled e end_at igual a start_at mais uma hora. corpo (application/json): title: string [obrigatorio] [max 255] description: string? [max 5000] start_at: string(date-time) [obrigatorio] end_at: string(date-time)? - Precisa ser igual ou posterior a start_at. all_day: boolean? location: string? [max 255] type: meeting|call|task|event|other [padrao meeting] meeting_type: in_person|online status: scheduled|confirmed|cancelled|completed [padrao scheduled] color: string? [max 20] user_id: integer? contact_id: integer? deal_id: integer? notes: string? [max 5000] respostas: 201: Criado. 422: Falha de validação. Formato padrão do framework. GET /appointments/{appointment} resumo: Obter agendamento escopo exigido: read parametros: appointment (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /appointments/{appointment} resumo: Atualizar agendamento escopo exigido: write nota: A validação de que end_at vem depois de start_at existe na criação, mas não na atualização. nota: Garanta a coerência do seu lado. parametros: appointment (path): integer [obrigatorio] corpo (application/json): title: string [obrigatorio] [max 255] description: string? [max 5000] start_at: string(date-time) [obrigatorio] end_at: string(date-time)? - Precisa ser igual ou posterior a start_at. all_day: boolean? location: string? [max 255] type: meeting|call|task|event|other [padrao meeting] meeting_type: in_person|online status: scheduled|confirmed|cancelled|completed [padrao scheduled] color: string? [max 20] user_id: integer? contact_id: integer? deal_id: integer? notes: string? [max 5000] respostas: 200: Atualizado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. DELETE /appointments/{appointment} resumo: Remover agendamento escopo exigido: write parametros: appointment (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. ############################################################################## # PRODUTOS ############################################################################## GET /products resumo: Listar produtos escopo exigido: read nota: A ordenação padrão aqui é crescente por nome. parametros: page (query): integer [padrao 1] per_page (query): integer [padrao 50] type (query): product|service category_id (query): integer is_active (query): boolean has_variations (query): boolean search (query): string - Busca parcial em nome, SKU e descrição. sort_by (query): name|price|sku|created_at|stock_quantity|sort_order [padrao name] sort_dir (query): asc|desc [padrao asc] - Qualquer valor diferente de desc é tratado como asc. respostas: 200: Lista paginada. 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /products resumo: Criar produto escopo exigido: write nota: Padrões: type igual a product e is_active igual a verdadeiro. nota: Não existe endpoint para criar ou editar variações. nota: Elas são apenas listadas na leitura de um produto. corpo (application/json): name: string [obrigatorio] [max 255] sku: string? [max 100] description: string? [max 5000] short_description: string? [max 500] type: product|service [padrao product] category: string? [max 255] category_id: integer? price: number [obrigatorio] cost: number? currency: string? [max 10] unit: string? [max 50] tax_rate: number? track_stock: boolean? stock_quantity: number? min_stock: number? is_active: boolean? [padrao True] custom_fields: object? respostas: 201: Criado. 422: Falha de validação. Formato padrão do framework. GET /products/{product} resumo: Obter produto escopo exigido: read parametros: product (path): integer [obrigatorio] respostas: 200: OK 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /products/{product} resumo: Atualizar produto escopo exigido: write parametros: product (path): integer [obrigatorio] corpo (application/json): name: string [obrigatorio] [max 255] sku: string? [max 100] description: string? [max 5000] short_description: string? [max 500] type: product|service [padrao product] category: string? [max 255] category_id: integer? price: number [obrigatorio] cost: number? currency: string? [max 10] unit: string? [max 50] tax_rate: number? track_stock: boolean? stock_quantity: number? min_stock: number? is_active: boolean? [padrao True] custom_fields: object? respostas: 200: Atualizado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. DELETE /products/{product} resumo: Remover produto escopo exigido: write parametros: product (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. ############################################################################## # WHATSAPP CONNECTOR ############################################################################## GET /wa-connector/channels resumo: Listar canais oficiais escopo exigido: read nota: Devolve apenas canais da API Oficial da Meta que estejam ativos. nota: Sem paginação, com envelope próprio { data, total }. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. GET /wa-connector/templates resumo: Listar templates aprovados escopo exigido: read nota: Somente templates aprovados pela Meta, ordenados por nome. nota: Sem paginação, com envelope próprio { data, total }. parametros: category (query): UTILITY|MARKETING|AUTHENTICATION - Convertido para maiúsculas automaticamente. language (query): string channel_id (query): integer - Filtra pelos templates da conta de WhatsApp Business daquele canal. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /wa-connector/send-template resumo: Enviar template escopo exigido: send nota: Envia um template aprovado pela API Oficial. nota: É o caminho para iniciar conversa fora da janela de 24 horas. nota: O telefone é reduzido a dígitos. nota: O idioma padrão é pt_BR. nota: components segue o formato da Cloud API da Meta. nota: Registro no Atlas. nota: Depois que a Meta aceita o envio, o Atlas registra a mensagem como um envio feito pela tela: - Contato: procurado pelo telefone na conta (com e sem 55, com e sem o 9º dígito). nota: Se não existir, é criado como lead, com o nome de contact_name (ou o próprio número). nota: Contato existente sem nome (ou com o número no lugar do nome) recebe o contact_name; nome de verdade não é trocado. nota: - Conversa: a deste número no mesmo canal é reaproveitada, pela mesma regra que a entrada da Meta usa. nota: Por isso, quando o lead responde, a resposta cai nessa conversa. nota: Se não existir, é criada; se estiver resolvida, é reaberta. nota: Ela termina com status open. nota: - Mensagem: gravada como mensagem de saída, com o texto do template já com as variáveis (ou [Template: nome] e as variáveis, quando o template não está sincronizado no Atlas), o message_id da Meta (os status entregue/lido atualizam por ele) e metadata.via = "api". nota: A conversa aparece na hora no inbox. nota: A IA não é acionada por essa mensagem. nota: Se a Meta recusar, nada é criado no Atlas e a resposta é 400, como antes. nota: Atenção: o serviço não verifica se o template existe na conta antes de enviar. nota: Um nome errado só falha na resposta da Meta. corpo (application/json): channel_id: integer [obrigatorio] to: string [obrigatorio] [max 30] template_name: string [obrigatorio] [max 255] language: string [padrao pt_BR] [max 10] components: array - Formato da Cloud API da Meta. contact_name: string [max 255] - Opcional. Nome do contato ao criar, ou para completar um contato existente sem nome. respostas: 200: Enviado. 400: A Meta recusou o envio. A mensagem devolvida é a da própria Meta. Nada é criado no Atlas. 404: Canal não encontrado ou inativo para esta conta. 422: Validação, canal desconectado, ou canal sem token de acesso e identificador de telefone configurados. ############################################################################## # E-MAIL ############################################################################## GET /email-accounts resumo: Listar caixas de e-mail escopo exigido: read nota: Caixas que o dono do token pode usar para enviar: a caixa pessoal dele, as caixas do time em que ele participa e a caixa Geral (E-mail SMTP das configurações) quando ele tem a permissão de configurações de e-mail. nota: São as mesmas caixas da tela E-mail. nota: O campo account é a chave usada em POST /emails/send: o id numérico da caixa ou general. nota: Sem paginação, com envelope { data }. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /emails/send resumo: Enviar e-mail por uma caixa escopo exigido: send nota: Envia um e-mail por uma caixa do Atlas, do mesmo jeito que a tela Novo e-mail. nota: Funciona com caixas IMAP/SMTP e Microsoft. nota: A mensagem fica na pasta Enviados da caixa, com o usuário do token como remetente interno, e o resultado aparece em Configurações, Logs de Integração. nota: account é a chave devolvida por GET /email-accounts. nota: Sem ela, usa a caixa pessoal do dono do token; se ele não tiver, a primeira acessível. nota: Caixa inativa (teste de SMTP falhou ou desligada) responde 422. nota: Destinatários (to, cc, bcc) aceitam três formas: uma string com um e-mail, uma lista de strings, ou uma lista de objetos { email, name }. nota: Endereço inválido é descartado; sem nenhum destinatário válido em to, 422. nota: Corpo: body_html (apelido body) ou body_text. nota: Mandando só um, o outro é derivado. nota: Anexos vão em base64, até 10 arquivos e 10 MB somados. nota: O envio é síncrono: a resposta só volta depois que o servidor da caixa aceitou a mensagem. nota: Com anexo grande ou servidor lento isso pode levar alguns segundos. corpo (application/json): account: string [max 20] - Id numérico da caixa ou general. Padrão: caixa pessoal do dono do token. to: object [obrigatorio] - String, lista de strings ou lista de { email, name }. cc: object - Mesmo formato de to. bcc: object - Mesmo formato de to. Cópia oculta. subject: string [obrigatorio] [max 255] body_html: string [max 500000] - Corpo em HTML. Apelido: body. body_text: string [max 200000] - Corpo em texto puro. Obrigatório quando não há body_html. reply_to: string(email) - Endereço de resposta (Reply-To). in_reply_to_id: integer - Id de um e-mail recebido no Atlas, para responder na mesma conversa. attachments: array - Até 10 arquivos e 10 MB somados. []filename: string []content_base64: string []mime: string respostas: 201: Enviado. 403: Token sem o escopo send, ou caixa que o dono do token não pode usar (a caixa de outra pessoa responde 403 mesmo existindo). 422: Validação (sem destinatário válido, sem corpo, anexo inválido ou acima de 10 MB), caixa inativa ou nenhuma caixa disponível para o usuário. 502: O servidor da caixa (SMTP ou Microsoft) recusou ou não respondeu. A mensagem fica em Enviados com status failed e o detalhe em Logs de Integração. GET /emails/{id} resumo: Consultar e-mail enviado escopo exigido: read nota: Status e destinatários de um e-mail enviado pela API ou pela tela (só mensagens de saída da conta). parametros: id (path): integer [obrigatorio] respostas: 200: OK 404: E-mail inexistente, de entrada, ou de outra conta. ############################################################################## # WEBHOOKS ############################################################################## GET /webhooks resumo: Listar webhooks escopo exigido: read nota: Sem paginação e sem filtros. nota: O segredo não é exibido na listagem. respostas: 200: OK 401: Token ausente, inválido ou expirado. 403: Quatro causas, distinguíveis pelo corpo: - token sem o escopo exigido (code: insufficient_scope, com required_scope), ver a seção *Escopos do token*; - usuário sem conta vinculada; - licença sem API... 429: Limite de 60 requisições por minuto estourado. POST /webhooks resumo: Criar webhook escopo exigido: write nota: O segredo é gerado pelo servidor e devolvido apenas nesta resposta e nas leituras individuais. nota: Use * em events para receber todos. corpo (application/json): url: string(uri) [obrigatorio] [max 500] events: array<*|contact.created|contact.updated|contact.deleted|conversation.created|conversation.updated|message.received|message.sent|invoice.created|invoice.updated|invoice.paid|invoice.overdue|invoice.cancelled|product.created|product.updated|product.deleted> [obrigatorio] is_active: boolean? [padrao True] respostas: 201: Criado. Guarde o secret. 422: Falha de validação. Formato padrão do framework. GET /webhooks/{webhook} resumo: Obter webhook escopo exigido: read parametros: webhook (path): integer [obrigatorio] respostas: 200: OK. Inclui o segredo. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. PUT /webhooks/{webhook} resumo: Atualizar webhook escopo exigido: write parametros: webhook (path): integer [obrigatorio] corpo (application/json): url: string(uri) [max 500] events: array<*|contact.created|contact.updated|contact.deleted|conversation.created|conversation.updated|message.received|message.sent|invoice.created|invoice.updated|invoice.paid|invoice.overdue|invoice.cancelled|product.created|product.updated|product.deleted> is_active: boolean? respostas: 200: Atualizado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. 422: Falha de validação. Formato padrão do framework. DELETE /webhooks/{webhook} resumo: Remover webhook escopo exigido: write parametros: webhook (path): integer [obrigatorio] respostas: 200: Removido. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. POST /webhooks/{webhook}/test resumo: Disparar teste escopo exigido: send nota: Envia o evento webhook.test para a URL cadastrada, com o corpo {"message":"This is a test webhook event.","timestamp":"..."}. parametros: webhook (path): integer [obrigatorio] respostas: 200: Teste despachado. 404: Recurso inexistente ou pertencente a outra conta. As duas situações respondem igual, por decisão de segurança. ============================================================================== OBJETOS DE RESPOSTA ============================================================================== CustomFieldsInput: ({chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text, textarea: texto (respeita tamanho mínimo e máximo do campo) - select, radio: o valor ou o rótulo de uma opção, sem diferenciar maiúsculas - multiselect: lista de valores ou rótulos das opções - checkbox: true/false ou lista de valores das opções - number, rating: número inteiro - decimal, currency: número (1234.56) ou texto "1.234,56" - date: AAAA-MM-DD - datetime: ISO 8601 (2026-09-30T14:00:00-03:00) - url: começando com http:// ou https:// - email, phone, color (#RRGGBB) - file, image: não gravados pela API (enviar pela tela) Campos obrigatórios só são exigidos na criação (POST). No PATCH, só as chaves enviadas mudam e null apaga o valor do campo. Chave inexistente: 422 com unknown_keys (contas em modo compatível recebem warnings e a chave vai para legacy_custom_fields).) CustomFieldValues: (Valores do módulo de Campos Personalizados, os mesmos da tela, com as chaves do módulo. Só campos ativos e preenchidos. Tipos como em CustomFieldsInput (data em AAAA-MM-DD, data e hora em ISO 8601, número como número, múltipla seleção como lista).) LegacyCustomFields: (Somente leitura. Chaves antigas que não existem no módulo (contas em modo compatível e dados de importação). Não aparecem na tela nem para a IA.) CustomFieldWarnings: (Só em contas no modo compatível, quando alguma chave enviada não existe no módulo.) CustomFieldErrorBody: message: string errors: object custom_field_errors: array []field: string []label: string []type: string []received: object []reason: string []valid_options: array unknown_keys: array CustomFieldDefinition: entity_type: deal|contact key: string - Fixa depois de criada; renomear o rótulo não muda a chave. label: string field_type: text|textarea|number|decimal|date|datetime|select|multiselect|checkbox|radio|email|phone|url|file|image|color|rating|currency options: array []value: string []label: string is_required: boolean is_active: boolean order: integer DealDocument: id: integer name: string size: integer - Bytes. mime_type: string? url: string(uri) - Link assinado e temporário para baixar o arquivo, sem token. Vale 30 minutos; vencido ou alterado, responde 403. url_expires_at: string(date-time) - Quando o link de url deixa de valer. created_at: string(date-time) ErrorScope: (Erro de escopo de token. Único erro da API com código de máquina.) error: string [obrigatorio] - Mensagem legível. code: insufficient_scope [obrigatorio] required_scope: read|write|send [obrigatorio] Pipeline: id: integer name: string description: string? is_default: boolean is_active: boolean sort_order: integer stages: array - Etapas do funil em ordem, inclusive as de ganho e de perda. []id: integer []pipeline_id: integer []name: string []color: string? []probability: integer []rotting_days: integer? []is_won: boolean []is_lost: boolean []sort_order: integer PipelineStage: id: integer pipeline_id: integer name: string color: string? probability: integer - Probabilidade de fechamento atribuída à etapa, de 0 a 100. rotting_days: integer? - Dias parado na etapa a partir dos quais o negócio é sinalizado como esfriando. is_won: boolean - Etapa de ganho. is_lost: boolean - Etapa de perda. sort_order: integer DealRef: id: integer name: string Deal: id: integer title: string value: number(float) currency: string status: open|won|lost probability: integer - Herdada da etapa, 0 a 100. pipeline: object .id: integer .name: string stage: object? .id: integer .name: string .is_won: boolean .is_lost: boolean contact: object .id: integer .name: string assigned_to: object .id: integer .name: string conversation_id: integer? - Conversa vinculada ao negócio, quando houver. expected_close_date: string(date)? stage_changed_at: string(date-time)? - Última entrada na etapa atual. closed_at: string(date)? created_at: string(date-time) updated_at: string(date-time) custom_fields: object - Valores do módulo de Campos Personalizados, os mesmos da tela, com as chaves do módulo. Só campos ativos e preenchidos. Tipos como em CustomFieldsInput (data em AAAA-MM-DD, data e hora em ISO 8601,... DealDetailed: id: integer title: string value: number(float) currency: string status: open|won|lost probability: integer - Herdada da etapa, 0 a 100. pipeline: object .id: integer .name: string stage: object? .id: integer .name: string .is_won: boolean .is_lost: boolean contact: object .id: integer .name: string assigned_to: object .id: integer .name: string conversation_id: integer? - Conversa vinculada ao negócio, quando houver. expected_close_date: string(date)? stage_changed_at: string(date-time)? - Última entrada na etapa atual. closed_at: string(date)? created_at: string(date-time) updated_at: string(date-time) custom_fields: object - Valores do módulo de Campos Personalizados, os mesmos da tela, com as chaves do módulo. Só campos ativos e preenchidos. Tipos como em CustomFieldsInput (data em AAAA-MM-DD, data e hora em ISO 8601,... notes: string? legacy_custom_fields: object - Somente leitura. Chaves antigas que não existem no módulo (contas em modo compatível e dados de importação). Não aparecem na tela nem para a IA. source_id: integer? campaign_id: integer? loss_reason: object .id: integer .name: string loss_notes: string? lost_at: string(date-time)? DealEffects: (Interruptores efetivamente aplicados na chamada. Ambos false quando omitidos.) trigger_automations: boolean fire_pixel_events: boolean DealActivity: id: integer type: string - created, stage_change, status_change, note, call, email, whatsapp, meeting… title: string? description: string? user: object .id: integer .name: string metadata: object? - Em movimentações: old_stage_id, new_stage_id, actor_label, via, trigger_automations, fire_pixel_events. activity_at: string(date-time)? created_at: string(date-time) EmailAccount: account: string - Chave para POST /emails/send: id numérico da caixa ou general. name: string from_email: string? from_name: string? provider: imap_smtp|microsoft|smtp - smtp é a caixa Geral das configurações. is_general: boolean is_personal: boolean - Caixa pessoal do dono do token. is_active: boolean - Só caixa ativa envia. EmailRecipient: email: string(email) [obrigatorio] name: string? EmailAttachment: filename: string [obrigatorio] [max 255] content_base64: string [obrigatorio] - Conteúdo do arquivo em base64. mime: string [padrao application/octet-stream] [max 100] EmailSent: id: integer account: string message_id: string - Message-ID do cabeçalho. status: sent|failed|draft from_email: string? from_name: string? to: array []email: string(email) []name: string? cc: array []email: string(email) []name: string? bcc: array []email: string(email) []name: string? subject: string in_reply_to: string? - Message-ID do e-mail respondido. sent_at: string(date-time)? sent_by_user_id: integer? - Usuário do token que enviou. Contact: id: integer name: string trading_name: string? display_name: string - Calculado a partir do nome e do nome fantasia. person_type: individual|company type: lead|customer|prospect|supplier email: string? phone: string? mobile: string? document: string? source: string? - Texto livre. É um rótulo de origem, não um canal detectado automaticamente. tags: array is_active: boolean group: object? .id: integer .name: string assigned_to: object? .id: integer .name: string deals_count: integer - Preenchido na listagem. Na leitura individual vem zero. contracts_count: integer - Preenchido na listagem. Na leitura individual vem zero. created_at: string(date-time) updated_at: string(date-time) custom_fields: object - Valores do módulo de Campos Personalizados, os mesmos da tela, com as chaves do módulo. Só campos ativos e preenchidos. Tipos como em CustomFieldsInput (data em AAAA-MM-DD, data e hora em ISO 8601,... ContactDetailed: id: integer name: string trading_name: string? display_name: string - Calculado a partir do nome e do nome fantasia. person_type: individual|company type: lead|customer|prospect|supplier email: string? phone: string? mobile: string? document: string? source: string? - Texto livre. É um rótulo de origem, não um canal detectado automaticamente. tags: array is_active: boolean group: object? .id: integer .name: string assigned_to: object? .id: integer .name: string deals_count: integer - Preenchido na listagem. Na leitura individual vem zero. contracts_count: integer - Preenchido na listagem. Na leitura individual vem zero. created_at: string(date-time) updated_at: string(date-time) custom_fields: object - Valores do módulo de Campos Personalizados, os mesmos da tela, com as chaves do módulo. Só campos ativos e preenchidos. Tipos como em CustomFieldsInput (data em AAAA-MM-DD, data e hora em ISO 8601,... website: string? company: string? position: string? state_registration: string? municipal_registration: string? address: string? address_number: string? address_complement: string? neighborhood: string? city: string? state: string? zip_code: string? country: string? birth_date: string(date)? notes: string? legacy_custom_fields: object - Somente leitura. Chaves antigas que não existem no módulo (contas em modo compatível e dados de importação). Não aparecem na tela nem para a IA. notification_settings: object parent_id: integer? bot_protected: boolean last_contact_at: string(date-time)? employees: array - Pessoas vinculadas a este contato. Só é carregado na leitura individual. []id: integer []name: string []email: string? []phone: string? []mobile: string? []position: string? ContactCreate: name: string [obrigatorio] [max 255] trading_name: string? [max 255] email: string(email)? [max 255] phone: string? [max 50] mobile: string? [max 50] website: string? [max 255] company: string? [max 255] position: string? [max 255] document: string? [max 50] state_registration: string? [max 50] municipal_registration: string? [max 50] person_type: individual|company [padrao individual] type: lead|customer|prospect|supplier [padrao lead] source: string? [max 100] contact_group_id: integer? assigned_to: integer? parent_id: integer? address: string? [max 255] address_number: string? [max 20] address_complement: string? [max 100] neighborhood: string? [max 100] city: string? [max 100] state: string? [max 50] zip_code: string? [max 20] country: string? [max 100] birth_date: string(date)? tags: array notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... notification_settings: object? ContactUpdate: (Todos os campos são opcionais. is_active e bot_protected só existem aqui.) name: string [obrigatorio] [max 255] trading_name: string? [max 255] email: string(email)? [max 255] phone: string? [max 50] mobile: string? [max 50] website: string? [max 255] company: string? [max 255] position: string? [max 255] document: string? [max 50] state_registration: string? [max 50] municipal_registration: string? [max 50] person_type: individual|company [padrao individual] type: lead|customer|prospect|supplier [padrao lead] source: string? [max 100] contact_group_id: integer? assigned_to: integer? parent_id: integer? address: string? [max 255] address_number: string? [max 20] address_complement: string? [max 100] neighborhood: string? [max 100] city: string? [max 100] state: string? [max 50] zip_code: string? [max 20] country: string? [max 100] birth_date: string(date)? tags: array notes: string? [max 5000] custom_fields: object - {chave: valor} com as chaves do módulo de Campos Personalizados (GET /custom-fields). cf_chave também vale (dentro de custom_fields ou solto na raiz do corpo). Formato aceito por tipo: - text,... notification_settings: object? is_active: boolean? bot_protected: boolean? DealSummary: id: integer title: string value: number currency: string status: string stage: object? .id: integer .name: string assigned_to: object? .id: integer .name: string expected_close_date: string(date)? won_at: string(date-time)? lost_at: string(date-time)? created_at: string(date-time) Conversation: id: integer remote_jid: string - Identificador no WhatsApp, no formato 5511912345678@s.whatsapp.net. display_name: string? avatar_url: string? status: open|pending|resolved is_group: boolean unread_count: integer last_message_preview: string? last_message_at: string(date-time)? channel: object? .id: integer .name: string .provider: string contact: object? .id: integer .name: string .email: string? .phone: string? .mobile: string? assigned_to: object? .id: integer .name: string assigned_department: object? .id: integer .name: string created_at: string(date-time) updated_at: string(date-time) Message: id: integer conversation_id: integer direction: incoming|outgoing type: text|image|document|video|audio content: string? media_url: string? media_mimetype: string? filename: string? - Sempre responde null. O campo é montado a partir de um atributo que não existe na tabela. Use media_url para identificar o arquivo. status: string remote_message_id: string? sender_user: object? .id: integer .name: string metadata: object - Mensagens enviadas por esta API trazem via = "api". created_at: string(date-time) MessageSend: (É obrigatório ao menos um entre content, media e media_url.) content: string? [max 10000] type: text|image|document|video|audio media_url: string(uri)? filename: string? [max 255] MessageSendByPhone: content: string? [max 10000] type: text|image|document|video|audio media_url: string(uri)? filename: string? [max 255] phone: string [max 50] - Obrigatório quando não há conversation_id. conversation_id: integer? channel_id: integer - Obrigatório quando não há conversation_id. Invoice: id: integer number: string title: string? status: draft|pending|sent|paid|cancelled issue_date: string(date) due_date: string(date) subtotal: number discount_amount: number discount_type: fixed|percent tax_total: number total: number amount_paid: number amount_due: number currency: string is_recurring: boolean contact: object? .id: integer .name: string .email: string? created_by: object? .id: integer .name: string created_at: string(date-time) updated_at: string(date-time) InvoiceDetailed: id: integer number: string title: string? status: draft|pending|sent|paid|cancelled issue_date: string(date) due_date: string(date) subtotal: number discount_amount: number discount_type: fixed|percent tax_total: number total: number amount_paid: number amount_due: number currency: string is_recurring: boolean contact: object? .id: integer .name: string .email: string? created_by: object? .id: integer .name: string created_at: string(date-time) updated_at: string(date-time) description: string? notes: string? terms: string? public_url: string - Link público da fatura, para envio ao cliente. sent_at: string(date-time)? paid_at: string(date-time)? viewed_at: string(date-time)? items: array []id: integer []name: string []description: string? []quantity: number []unit: string? []unit_price: number []discount_amount: number []discount_type: fixed|percent []tax_rate: number []tax_amount: number []total: number []product_id: integer? payments: array - Preenchido na leitura individual. Na criação e na atualização vem vazio. []id: integer []amount: number []method: string []status: string []paid_at: string(date-time)? InvoiceItem: id: integer name: string description: string? quantity: number unit: string? unit_price: number discount_amount: number discount_type: fixed|percent tax_rate: number tax_amount: number total: number product_id: integer? InvoiceCreate: contact_id: integer [obrigatorio] title: string? [max 255] description: string? [max 5000] issue_date: string(date)? due_date: string(date) [obrigatorio] currency: string? [padrao BRL] [max 10] discount_amount: number? discount_type: fixed|percent [padrao fixed] notes: string? [max 5000] terms: string? [max 5000] items: array [obrigatorio] []name: string []description: string? []quantity: number []unit_price: number []unit: string? []product_id: integer? []tax_rate: number? []discount_amount: number? []discount_type: fixed|percent InvoiceUpdate: (Não aceita itens e não recalcula totais.) contact_id: integer title: string? [max 255] description: string? [max 5000] issue_date: string(date)? due_date: string(date) status: draft|pending|sent|paid|cancelled currency: string? [max 10] discount_amount: number? discount_type: fixed|percent notes: string? [max 5000] terms: string? [max 5000] PaymentSummary: id: integer amount: number method: string status: string paid_at: string(date-time)? Payment: id: integer amount: number method: string status: string paid_at: string(date-time)? fee: number net_amount: number gateway: string? transaction_id: string? notes: string? created_at: string(date-time) Proposal: id: integer number: string title: string? subject: string? status: draft|sent|accepted|rejected|expired proposal_date: string(date) valid_until: string(date)? subtotal: number discount_amount: number discount_type: fixed|percentage tax_amount: number total: number currency: string contact: object? .id: integer .name: string .email: string? assigned_to: object? .id: integer .name: string created_by: object? .id: integer .name: string created_at: string(date-time) updated_at: string(date-time) ProposalDetailed: id: integer number: string title: string? subject: string? status: draft|sent|accepted|rejected|expired proposal_date: string(date) valid_until: string(date)? subtotal: number discount_amount: number discount_type: fixed|percentage tax_amount: number total: number currency: string contact: object? .id: integer .name: string .email: string? assigned_to: object? .id: integer .name: string created_by: object? .id: integer .name: string created_at: string(date-time) updated_at: string(date-time) deal: object? .id: integer .title: string content: string? notes: string? terms: string? public_url: string - Link público da proposta, para envio e assinatura. sent_at: string(date-time)? viewed_at: string(date-time)? accepted_at: string(date-time)? rejected_at: string(date-time)? signed_at: string(date-time)? signer_name: string? items: array []id: integer []name: string []description: string? []quantity: number []unit: string? []unit_price: number []discount_amount: number []discount_type: fixed|percentage []tax_rate: number []tax_amount: number []total: number []is_optional: boolean []product_id: integer? ProposalItem: id: integer name: string description: string? quantity: number unit: string? unit_price: number discount_amount: number discount_type: fixed|percentage tax_rate: number tax_amount: number total: number is_optional: boolean product_id: integer? ProposalCreate: contact_id: integer [obrigatorio] deal_id: integer? assigned_to: integer? title: string? [max 255] subject: string? [max 255] proposal_date: string(date)? valid_until: string(date)? currency: string? [padrao BRL] [max 10] discount_amount: number? discount_type: fixed|percentage content: string? notes: string? [max 5000] terms: string? [max 5000] items: array [obrigatorio] []name: string []description: string? []quantity: number []unit_price: number []unit: string? []product_id: integer? []tax_rate: number? []discount_amount: number? []discount_type: fixed|percentage []is_optional: boolean? ProposalUpdate: (Não aceita itens.) contact_id: integer deal_id: integer? assigned_to: integer? title: string? [max 255] subject: string? [max 255] proposal_date: string(date)? valid_until: string(date)? status: draft|sent|accepted|rejected|expired currency: string? [max 10] discount_amount: number? discount_type: fixed|percentage content: string? notes: string? [max 5000] terms: string? [max 5000] Appointment: id: integer title: string type: meeting|call|task|event|other status: scheduled|confirmed|cancelled|completed start_at: string(date-time) end_at: string(date-time)? all_day: boolean location: string? meeting_type: in_person|online color: string? user: object? .id: integer .name: string contact: object? .id: integer .name: string deal: object? .id: integer .title: string created_at: string(date-time) updated_at: string(date-time) AppointmentDetailed: id: integer title: string type: meeting|call|task|event|other status: scheduled|confirmed|cancelled|completed start_at: string(date-time) end_at: string(date-time)? all_day: boolean location: string? meeting_type: in_person|online color: string? user: object? .id: integer .name: string contact: object? .id: integer .name: string deal: object? .id: integer .title: string created_at: string(date-time) updated_at: string(date-time) description: string? notes: string? google_meet_link: string? google_event_id: string? AppointmentCreate: title: string [obrigatorio] [max 255] description: string? [max 5000] start_at: string(date-time) [obrigatorio] end_at: string(date-time)? - Precisa ser igual ou posterior a start_at. all_day: boolean? location: string? [max 255] type: meeting|call|task|event|other [padrao meeting] meeting_type: in_person|online status: scheduled|confirmed|cancelled|completed [padrao scheduled] color: string? [max 20] user_id: integer? contact_id: integer? deal_id: integer? notes: string? [max 5000] AppointmentUpdate: (Todos os campos opcionais. A verificação de que end_at vem depois de start_at não é aplicada aqui.) title: string [obrigatorio] [max 255] description: string? [max 5000] start_at: string(date-time) [obrigatorio] end_at: string(date-time)? - Precisa ser igual ou posterior a start_at. all_day: boolean? location: string? [max 255] type: meeting|call|task|event|other [padrao meeting] meeting_type: in_person|online status: scheduled|confirmed|cancelled|completed [padrao scheduled] color: string? [max 20] user_id: integer? contact_id: integer? deal_id: integer? notes: string? [max 5000] Product: id: integer name: string sku: string? type: product|service price: number cost: number tax_rate: number stock_quantity: number currency: string unit: string? is_active: boolean track_stock: boolean has_variations: boolean category: object? .id: integer .name: string image: string? - Caminho relativo do arquivo, não uma URL completa. created_at: string(date-time) updated_at: string(date-time) ProductDetailed: id: integer name: string sku: string? type: product|service price: number cost: number tax_rate: number stock_quantity: number currency: string unit: string? is_active: boolean track_stock: boolean has_variations: boolean category: object? .id: integer .name: string image: string? - Caminho relativo do arquivo, não uma URL completa. created_at: string(date-time) updated_at: string(date-time) description: string? short_description: string? min_stock: number custom_fields: object margin: number - Margem calculada a partir de preço e custo. variations: array - Somente na leitura individual. Não há endpoint para criar ou editar variações. []id: integer []name: string []sku: string? []price: number []cost: number []stock_quantity: number []is_active: boolean []is_default: boolean ProductCreate: name: string [obrigatorio] [max 255] sku: string? [max 100] description: string? [max 5000] short_description: string? [max 500] type: product|service [padrao product] category: string? [max 255] category_id: integer? price: number [obrigatorio] cost: number? currency: string? [max 10] unit: string? [max 50] tax_rate: number? track_stock: boolean? stock_quantity: number? min_stock: number? is_active: boolean? [padrao True] custom_fields: object? ProductUpdate: (Todos os campos opcionais.) name: string [obrigatorio] [max 255] sku: string? [max 100] description: string? [max 5000] short_description: string? [max 500] type: product|service [padrao product] category: string? [max 255] category_id: integer? price: number [obrigatorio] cost: number? currency: string? [max 10] unit: string? [max 50] tax_rate: number? track_stock: boolean? stock_quantity: number? min_stock: number? is_active: boolean? [padrao True] custom_fields: object? WaChannel: id: integer name: string phone_number: string profile_name: string? profile_picture: string? status: string is_connected: boolean is_primary: boolean is_default: boolean waba_id: string? - Identificador da conta de WhatsApp Business na Meta. connected_at: string(date-time)? - Em UTC, com sufixo Z. WaTemplate: id: integer name: string language: string category: UTILITY|MARKETING|AUTHENTICATION category_label: string status: string header_type: string body_text: string - Corpo do template, com as variáveis no formato {{1}}, {{2}} e assim por diante. footer_text: string? buttons: array dynamic_url_buttons: array has_dynamic_url_buttons: boolean variables: array - Nomes das variáveis, por exemplo ["1","2"]. variables_count: integer waba_id: string? last_synced_at: string(date-time)? - Em UTC, com sufixo Z. Webhook: id: integer url: string(uri) events: array<*|contact.created|contact.updated|contact.deleted|conversation.created|conversation.updated|message.received|message.sent|invoice.created|invoice.updated|invoice.paid|invoice.overdue|invoice.cancelled|product.created|product.updated|product.deleted> is_active: boolean failure_count: integer - Falhas consecutivas. Em 50, o webhook é desativado automaticamente. last_triggered_at: string(date-time)? created_at: string(date-time) WebhookWithSecret: id: integer url: string(uri) events: array<*|contact.created|contact.updated|contact.deleted|conversation.created|conversation.updated|message.received|message.sent|invoice.created|invoice.updated|invoice.paid|invoice.overdue|invoice.cancelled|product.created|product.updated|product.deleted> is_active: boolean failure_count: integer - Falhas consecutivas. Em 50, o webhook é desativado automaticamente. last_triggered_at: string(date-time)? created_at: string(date-time) secret: string - 64 caracteres. Usado para calcular a assinatura HMAC-SHA256 de cada entrega. Não aparece na listagem. ============================================================================== EVENTOS DE WEBHOOK ============================================================================== Entrega: POST na URL cadastrada. Corpo: {"event":"","timestamp":"","data":{...}} Cabecalhos: Content-Type: application/json | User-Agent: Atlas/1.0 | X-Webhook-Event | X-Webhook-Id | X-Webhook-Signature Assinatura: HMAC-SHA256 em hexadecimal do corpo cru, usando o secret do webhook. Reentrega: timeout de 15s, 3 tentativas com espera de 10s, 30s e 60s, apenas para erro 5xx ou falha de rede. Um 4xx conta como falha e nao e repetido. Desativacao automatica: 50 falhas consecutivas. contact.created quando: Qualquer criacao de contato, pela API ou pela interface. data: id, name, email, phone, mobile, type, person_type, document, source, tags[] contact.updated quando: Qualquer atualizacao de contato, sem filtro de campo. data: mesmo payload de contact.created contact.deleted quando: Exclusao logica de contato. data: id, name conversation.created quando: Nova conversa. data: id, remote_jid, display_name, status, contact_id, channel_id, assigned_user_id conversation.updated quando: Somente quando status, responsavel, departamento ou contato mudam. data: mesmo payload de conversation.created message.received quando: Mensagem recebida. Exclui mensagens de sistema e ocultas. data: id, conversation_id, contact_id, direction, type, content, media_url, status, remote_message_id, created_at message.sent quando: Mensagem enviada, inclusive por bot, campanha ou automacao. data: mesmo payload de message.received NUNCA DISPARADOS HOJE (aceitos no cadastro, mas sem nenhum codigo que os emita): invoice.created, invoice.updated, invoice.paid, invoice.overdue, invoice.cancelled, product.created, product.updated, product.deleted Nao construa integracao que dependa deles. ============================================================================== ARMADILHAS CONHECIDAS ============================================================================== - Accept: application/json e obrigatorio. Sem ele, erro de autenticacao vira 302 em HTML. - Recurso de outra conta responde 404, nunca 403. Nao interprete 404 como "nao existe". - discount_type usa "percent" em faturas e "percentage" em propostas. - sort_dir padrao e desc em contatos, conversas, faturas e propostas, e asc em agendamentos e produtos. - O campo filename de uma mensagem sempre responde null. Use media_url. - deals_count e contracts_count so vem preenchidos na listagem de contatos. Na leitura individual vem zero. - O filtro source de contatos e sensivel a maiusculas e a base tem grafias diferentes para a mesma origem. - PUT /invoices nao aceita itens e nao recalcula totais. - PUT /proposals nao aceita itens. - PUT /appointments nao verifica se end_at vem depois de start_at. A criacao verifica. - PUT /conversations ignora em silencio um assigned_user_id de outra conta, e responde 200. - contact_id, deal_id, product_id e category_id enviados no corpo nao sao verificados contra a sua conta. - Enviar mensagem sempre atribui a conversa ao usuario do token, se ela estiver sem responsavel, e forca o status para open. - send-template nao verifica se o template existe antes de chamar a Meta. - /webhooks nao pagina. /conversations/{id}/messages usa cursor, nao pagina. - O secret de um webhook so aparece na criacao e na leitura individual, nunca na listagem. ============================================================================== EXEMPLOS EM CURL ============================================================================== Todos os exemplos usam dados ficticios. # Listar contatos criados mais recentemente curl -s 'https://atlas.skill-hub.app/api/v1/contacts?per_page=20&sort_by=created_at&sort_dir=desc' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' # Criar um contato curl -s -X POST 'https://atlas.skill-hub.app/api/v1/contacts' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"name":"Maria Exemplo","mobile":"11912345678","type":"lead"}' # Enviar mensagem por telefone, criando a conversa se preciso curl -s -X POST 'https://atlas.skill-hub.app/api/v1/messages/send' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"phone":"5511912345678","channel_id":12,"content":"Ola! Recebemos seu contato."}' # Enviar template aprovado pela API Oficial curl -s -X POST 'https://atlas.skill-hub.app/api/v1/wa-connector/send-template' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"channel_id":12,"to":"5511912345678","template_name":"appointment_reminder","language":"pt_BR","components":[{"type":"body","parameters":[{"type":"text","text":"Maria"}]}]}' # Criar fatura com um item curl -s -X POST 'https://atlas.skill-hub.app/api/v1/invoices' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"contact_id":1001,"due_date":"2026-10-15","items":[{"name":"Plano Pro","quantity":1,"unit_price":499.90}]}' # Cadastrar webhook e guardar o segredo curl -s -X POST 'https://atlas.skill-hub.app/api/v1/webhooks' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"url":"https://seusistema.com/hooks/atlas","events":["contact.created","message.received"]}' # Ler mensagens novas de uma conversa (cursor) curl -s 'https://atlas.skill-hub.app/api/v1/conversations/500/messages?after_id=12345&limit=50' \ -H 'Authorization: Bearer SEU_TOKEN' \ -H 'Accept: application/json' ============================================================================== FIM ==============================================================================