API Pública

API pública para integrações de terceiros e widgets embarcados. Não requer autenticação de usuário — usa o identificador do inbox.

Autenticação

A API pública usa o inbox_identifier no path para identificar um inbox do tipo API. O contato usa o source_id retornado na criação como contact_identifier nas rotas seguintes.

Quando identity_validation_enabled e true, envie o identifier exato e seu identifier_hash: o hexadecimal minúsculo de HMAC-SHA256(hmac_token_do_canal, identifier). No POST e no PATCH eles vão no JSON; no GET do contato, vão na query string. Credencial ausente ou inválida retorna sempre 401 com { "error": "HMAC failed: Invalid Identifier Hash Provided" }. Não existe header de autenticação de usuário nessas rotas.

O hmac_token do canal é um segredo e nunca deve ser incluído no JavaScript do navegador, no widget ou em um aplicativo cliente. Gere o identifier_hash somente em um backend confiável. Para acessar um contato existente, assine o identifier que pertence ao contato identificado pelo contact_identifier/source_id alvo; uma assinatura válida para outro identifier não autoriza esse contato.

No GET e no PATCH de um source_id bem formado, a autenticação HMAC acontece antes da busca do contato. Por isso, credenciais inválidas retornam 401 mesmo quando o source_id não existe; depois de uma autenticação válida, um source_id desconhecido retorna 404.

Inbox Público

GET/public/api/v1/inboxes/{inbox_identifier}

Retorna a configuração pública do inbox API. A resposta não possui campo active.

200Configuração pública do inbox, sem envelope data.
json
{
  "identifier": "INBOX_IDENTIFIER",
  "identity_validation_enabled": true,
  "name": "API",
  "timezone": "America/Sao_Paulo",
  "working_hours": [
    {
      "day_of_week": 1,
      "closed_all_day": false,
      "open_hour": 9,
      "open_minutes": 0,
      "close_hour": 17,
      "close_minutes": 0,
      "open_all_day": false
    }
  ],
  "working_hours_enabled": true,
  "csat_survey_enabled": false,
  "greeting_enabled": false
}
404inbox_identifier inexistente.

Contatos

POST/public/api/v1/inboxes/{inbox_identifier}/contacts

Cria um contato no inbox público. O body é opcional; sem ele, a API gera o source_id e o nome.

Body

NomeTipoObrigatorioDescricao
source_idstringNaoIdentificador de sessão; quando omitido, a API gera um UUID
namestringNaoNome do contato
emailstringNaoEmail
phone_numberstringNaoTelefone no formato E.164, por exemplo +5511999999999
identifierstringNaoIdentificador externo
identifier_hashstringNaoHMAC-SHA256 do identifier quando a validação de identidade é obrigatória
avatar_urlstring (URL)NaoURL pública da imagem a importar de forma assíncrona
custom_attributesobjectNaoAtributos personalizados
bash
curl -X POST "https://chat.seudominio.com/public/api/v1/inboxes/INBOX_IDENTIFIER/contacts" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Visitante do Site",
    "email": "visitante@email.com"
  }'
200Contato criado
json
{
  "id": 101,
  "source_id": "contact_source_abc123",
  "pubsub_token": "token_for_websocket",
  "name": "Visitante do Site",
  "email": "visitante@email.com",
  "phone_number": null
}
401HMAC ausente ou inválido quando a validação de identidade está habilitada.
json
{ "error": "HMAC failed: Invalid Identifier Hash Provided" }
404inbox_identifier inexistente; o canal não existe para avaliar HMAC.
json
{ "error": "Resource could not be found" }
422source_id ou campo escalar inválido.
json
{ "error": "source_id must be a non-empty string" }
422Validação do contato.
json
{
  "message": "Phone number should be in e164 format",
  "attributes": ["phone_number"]
}
GET/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}

Obtém o contato pelo source_id retornado na criação.

Query (quando identity_validation_enabled = true)

NomeTipoObrigatorioDescricao
identifier(query)stringSimIdentificador externo exato usado como mensagem do HMAC
identifier_hash(query)stringSimHexadecimal minúsculo de HMAC-SHA256(hmac_token, identifier)
200Mesma projeção compacta do POST: source_id, pubsub_token, id, name, email e phone_number.
401HMAC ausente ou inválido quando a validação de identidade está habilitada.
json
{ "error": "HMAC failed: Invalid Identifier Hash Provided" }
404Inbox inexistente ou source_id desconhecido depois de uma autenticação HMAC válida, quando exigida.
json
{ "error": "Resource could not be found" }
PATCH/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}

Atualiza o contato. Com HMAC válido, o identifier serve somente para autenticar: o PATCH não altera o identifier, não mescla contatos e retorna 422 se outro contato da conta possuir o email ou telefone. Quando o canal torna HMAC opcional e identifier_hash não é enviado, o fluxo legado sem assinatura preserva a mesclagem por email ou telefone dentro da mesma conta.

Body

NomeTipoObrigatorioDescricao
identifierstringNaoSomente a credencial assinada pelo HMAC; não é encaminhado para a atualização nem altera/mescla o contato
identifier_hashstringNaoHexadecimal minúsculo de HMAC-SHA256(hmac_token, identifier), exigido junto ao identifier quando configurado
namestringNaoNovo nome; valores vazios não substituem o atual
emailstringNaoNovo email; conflito retorna 422 no fluxo assinado e pode mesclar no fluxo legado sem assinatura
phone_numberstringNaoNovo telefone E.164; conflito retorna 422 no fluxo assinado e pode mesclar no fluxo legado sem assinatura
avatar_urlstring (URL)NaoURL pública da imagem a importar
custom_attributesobjectNaoObjeto mesclado aos atributos atuais
200Retorna o registro Contact atualizado diretamente, sem source_id nem pubsub_token. Além dos campos abaixo, o schema pode incluir os demais atributos persistidos do contato.
json
{
  "id": 101,
  "name": "Visitante identificado",
  "email": "visitante@email.com",
  "phone_number": null,
  "account_id": 1,
  "identifier": "cliente-101",
  "custom_attributes": {},
  "additional_attributes": {},
  "contact_type": "visitor",
  "blocked": false,
  "created_at": "2027-02-15T14:00:00.000Z",
  "updated_at": "2027-02-15T14:05:00.000Z"
}
401HMAC ausente ou inválido antes da busca do source_id quando a validação de identidade está habilitada.
json
{ "error": "HMAC failed: Invalid Identifier Hash Provided" }
404Inbox inexistente ou source_id desconhecido depois de uma autenticação HMAC válida, quando exigida.
json
{ "error": "Resource could not be found" }
422Campo inválido.
json
{ "error": "name must be a string" }
422Conflito de email ou telefone no fluxo assinado; nenhuma mesclagem é realizada.
json
{
  "message": "Phone number has already been taken",
  "attributes": ["phone_number"]
}

Conversas

POST/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations

Cria uma nova conversa para o contato.

Body

NomeTipoObrigatorioDescricao
custom_attributesobjectNaoAtributos da conversa
bash
curl -X POST "https://chat.seudominio.com/public/api/v1/inboxes/INBOX_ID/contacts/CONTACT_ID/conversations" \
  -H "Content-Type: application/json" \
  -d '{ "custom_attributes": { "page": "/pricing" } }'
200Conversa criada
json
{
  "id": 999,
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "inbox_id": 1,
  "contact_last_seen_at": 1802700000,
  "status": "open",
  "agent_last_seen_at": 0,
  "messages": [],
  "contact": {
    "id": 101,
    "name": "Visitante do Site",
    "email": "visitante@email.com",
    "phone_number": null,
    "account_id": 1,
    "identifier": null,
    "additional_attributes": {},
    "custom_attributes": {},
    "last_activity_at": null,
    "contact_type": "visitor",
    "middle_name": "",
    "last_name": "",
    "location": "",
    "country_code": "",
    "blocked": false,
    "company_id": null,
    "created_at": "2027-02-15T14:00:00.000Z",
    "updated_at": "2027-02-15T14:00:00.000Z"
  }
}

Timestamps e identificadores

id é o display_id da conversa dentro da conta. Os campos contact_last_seen_at e agent_last_seen_at são timestamps Unix em segundos, não strings ISO 8601. Cada item de messages usa a projeção documentada abaixo.

POST/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_status

Resolve a conversa. Apesar do nome histórico toggle_status, esta rota não reabre uma conversa resolvida.

200Projeção da conversa já em status resolved.
404Conversa não acessível por este contato/inbox.
POST/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_typing

Emite o estado de digitação do contato.

Body

NomeTipoObrigatorioDescricao
typing_statusstringSimUse exatamente on ou off
200Evento emitido com sucesso; resposta sem corpo.
422typing_status ausente ou diferente de on/off.
json
{ "error": "typing_status must be on or off" }
404Conversa não acessível por este contato/inbox.
GET/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations

Lista as conversas acessíveis pelo contato; a resposta é um array sem envelope data.

200Array de projeções de conversa.
GET/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}

Obtém uma conversa pelo display_id, com mensagens públicas embutidas.

200Projeção de conversa mostrada acima.
404Conversa não acessível por este contato/inbox.
POST/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/update_last_seen

Atualiza a última visualização do contato e agenda a marcação das mensagens de saída como lidas.

200Sucesso sem corpo.
404Conversa não acessível por este contato/inbox.

Mensagens

POST/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages

Envia uma mensagem na conversa pública.

Body

NomeTipoObrigatorioDescricao
contentstringNaoTexto da mensagem; pode ser omitido quando há anexo
echo_idstringNaoID temporário devolvido nos eventos WebSocket; não aparece no corpo HTTP
attachments[]file[]NaoArquivos enviados via multipart/form-data
bash
curl -X POST "https://chat.seudominio.com/public/api/v1/inboxes/INBOX_ID/contacts/CONTACT_ID/conversations/999/messages" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Preciso de ajuda com meu pedido" }'
200Mensagem incoming criada; a resposta não possui envelope data.
json
{
  "id": 501,
  "content": "Preciso de ajuda com meu pedido",
  "message_type": 0,
  "content_type": "text",
  "content_attributes": {},
  "created_at": 1802700000,
  "conversation_id": 999,
  "sender": {
    "id": 101,
    "name": "Visitante do Site",
    "email": "visitante@email.com",
    "phone_number": null,
    "identifier": null,
    "additional_attributes": {},
    "custom_attributes": {},
    "thumbnail": "",
    "blocked": false,
    "type": "contact"
  }
}

Campos da mensagem

message_type é numérico: 0 incoming, 1 outgoing,2 activity e 3 template. created_at é Unix em segundos. attachments só aparece quando existem anexos, e sender varia conforme o tipo do remetente.

422Falha de validação, por exemplo content maior que 150.000 caracteres.
GET/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages

Lista mensagens públicas da conversa, sem envelope data; mensagens privadas e de atividade são removidas.

Query

NomeTipoObrigatorioDescricao
before(query)integerNaoID de mensagem para buscar até 20 itens anteriores; sem cursor, retorna os 20 mais recentes
200Array cronológico de projeções de mensagem.
404Conversa não acessível por este contato/inbox.
PATCH/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages/{message_id}

Envia valores de resposta para mensagens interativas ou para a pesquisa CSAT; não marca mensagens como lidas.

Body

NomeTipoObrigatorioDescricao
submitted_valuesobject | arrayNaoResposta com name, title, value ou csat_survey_response
submitted_values.csat_survey_response.ratingintegerNaoNota da pesquisa CSAT
submitted_values.csat_survey_response.feedback_messagestringNaoComentário da pesquisa CSAT
bash
curl -X PATCH "https://chat.seudominio.com/public/api/v1/inboxes/INBOX_ID/contacts/CONTACT_ID/conversations/999/messages/501" \
  -H "Content-Type: application/json" \
  -d '{ "submitted_values": { "csat_survey_response": { "rating": 5, "feedback_message": "Otimo atendimento" } } }'
200Projeção da mensagem com submitted_values em content_attributes.
422Respostas CSAT não podem ser alteradas depois de 14 dias.
json
{ "error": "You cannot update the CSAT survey after 14 days" }

Agendamento (Widget Público)

Endpoint anônimo para o widget de agendamento público. Não requer autenticação. Disponível em toda licença NooviChat válida quando o widget público está configurado operacionalmente na conta.

Flags operacionais e limites

Todos os quatro endpoints exigem appointments_module e appointments_public_widget habilitados na conta. Quando a primeira está desabilitada, a resposta é 403 { "error": "feature_not_enabled" }; quando somente o widget está desabilitado, é 403 { "error": "public_widget_not_enabled" }. Isso não representa uma diferença de plano.

As três leituras compartilham o limite padrão de 120 requisições por minuto por IP. O POST permite por padrão 10 criações por hora por IP. O limite global por IP também se aplica.

Identificação de contato obrigatória

O objeto contact deve conter phone (ou phone_number) OU email. Se ambos estiverem ausentes, a API retorna 422 com {"errors": {"contact": ["phone or email is required"]}}. Quando informado, o telefone deve obrigatoriamente estar no formato E.164: sinal +, código do país e de 2 a 15 dígitos no total. O email é normalizado com remoção dos espaços externos e conversão para minúsculas antes da busca.

Se telefone e email forem enviados juntos e nenhum deles existir, a API cria um novo contato com os dois valores. Para reutilizar um contato conhecido, cada identificador deve resolver de forma única para o mesmo contato. Se apenas um deles encontrar um contato existente, se apontarem para contatos diferentes ou se a busca for ambígua, a API não enriquece nem altera o cadastro: retorna 422 com o erro genérico { "errors": { "contact": ["the provided contact information could not be verified"] } }. A criação do contato e do agendamento é atômica, portanto uma rejeição não deixa contato órfão.

Fluxo do widget: 1) liste os serviços, 2) liste os profissionais (opcionalmente filtrando por serviço), 3) consulte os horários livres e 4) crie o agendamento. Os três primeiros são GET anônimos.

GET/public/api/v1/inboxes/{inbox_identifier}/appointments/services

Lista os serviços ativos e disponíveis para agendamento online.

Path

NomeTipoObrigatorioDescricao
inbox_identifierstringSimIdentificador público do inbox
200Lista de serviços
json
{
  "data": [
    {
      "id": 7,
      "name": "Consulta inicial",
      "description": "Primeira avaliação",
      "duration_minutes": 60,
      "default_price_cents": 15000,
      "currency": "BRL",
      "color": "#2781F6",
      "online_available": true
    }
  ]
}
403Módulo ou widget público desabilitado operacionalmente.
GET/public/api/v1/inboxes/{inbox_identifier}/appointments/professionals

Lista os profissionais ativos. Opcionalmente filtra os que atendem um serviço.

Path

NomeTipoObrigatorioDescricao
inbox_identifierstringSimIdentificador público do inbox

Query

NomeTipoObrigatorioDescricao
service_id(query)integer (int64)NaoServiço ativo e publicado online desta conta; filtra profissionais ativos que o oferecem
200Lista de profissionais
json
{
  "data": [
    { "id": 3, "name": "Dra. Ana", "specialty": "Clinica geral", "color": "#10b981" }
  ]
}
403Módulo ou widget público desabilitado operacionalmente.
404service_id malformado, estruturado, fora do intervalo, cross-tenant, inativo, arquivado, inexistente ou não publicado online.
GET/public/api/v1/inboxes/{inbox_identifier}/appointments/slots

Retorna os horários livres de um profissional ativo para um serviço ativo e publicado online que ele realmente oferece.

Path

NomeTipoObrigatorioDescricao
inbox_identifierstringSimIdentificador público do inbox

Query

NomeTipoObrigatorioDescricao
professional_id(query)integer (int64)SimID do profissional ativo da conta
service_id(query)integer (int64)SimID do serviço ativo/online oferecido pelo profissional
date(query)string (date)SimData a consultar (YYYY-MM-DD)
200Horários livres (ISO 8601, fuso da conta)
json
{
  "data": ["2027-06-15T09:00:00-03:00", "2027-06-15T10:00:00-03:00"]
}

Duração efetiva da oferta

Os slots usam ProfessionalService.override_duration_minutes quando existe; caso contrário, usam Service.duration_minutes. Horário de trabalho, buffer, compromissos existentes e horários passados também participam do cálculo.

Erros

Data inválida retorna 422 {"error": "invalid_date"}; ID malformado, estruturado, fora do intervalo, cross-tenant ou de recurso inativo, arquivado ou inexistente retorna 404. O mesmo 404 se aplica quando o profissional não oferece o serviço.

403Módulo ou widget público desabilitado operacionalmente.
POST/public/api/v1/inboxes/{inbox_identifier}/appointments

Agendamento anônimo via widget público. Encontra ou cria o contato pelo telefone ou email fornecido.

Path

NomeTipoObrigatorioDescricao
inbox_identifierstringSimIdentificador público do inbox

Body

NomeTipoObrigatorioDescricao
professional_idinteger (int64)SimID de profissional ativo obtido na listagem pública
service_idinteger (int64)SimID de serviço ativo e online obtido na listagem pública
scheduled_atstring (ISO 8601)SimInício futuro em ISO 8601 estrito, com T entre data e hora. Data impossível, apenas data ou separador por espaço são rejeitados. Offset/Z preserva o instante; sem offset usa o fuso de agendamento da conta.
notesstringNaoObservações do cliente
contactobjectSimDados de identificação do contato. Requer phone/phone_number OU email.
contact.namestringNaoNome do contato. Padrão: "Visitante"
contact.phonestringNaoTelefone opcional quando há email, mas E.164 é obrigatório quando informado (+ e 2-15 dígitos). Alias: phone_number
contact.emailstringNaoEmail do contato
curl -X POST "https://chat.seudominio.com/public/api/v1/inboxes/INBOX_IDENTIFIER/appointments" \
  -H "Content-Type: application/json" \
  -d '{
    "professional_id": 3,
    "service_id": 7,
    "scheduled_at": "2027-06-15T09:00:00-03:00",
    "notes": "Primeira consulta",
    "contact": {
      "name": "Maria Silva",
      "phone": "+5511999990000",
      "email": "maria@example.com"
    }
  }'
201Atendimento agendado com sucesso
json
{
  "data": {
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "scheduled_at": "2027-06-15T12:00:00.000Z",
    "ends_at": "2027-06-15T13:00:00.000Z",
    "status": "scheduled",
    "professional": { "name": "Dr. Carlos Mendes" },
    "service": { "name": "Limpeza Dental", "duration_minutes": 60 }
  }
}

Snapshot da oferta no retorno

A criação rejeita com 422 um par profissional-serviço que não exista. O ends_at é calculado pela duração efetiva da oferta. Dentro de service, duration_minutes informa essa mesma duração efetiva persistida para o atendimento, inclusive quando existe override do profissional. O preço também é salvo no atendimento a partir do override da oferta ou, sem ele, do preço base do serviço; esse snapshot não é exposto por esta resposta pública.

403Exposição pública desabilitada operacionalmente na conta (não é restrição de plano)
json
{ "error": "feature_not_enabled" }
// ou
{ "error": "public_widget_not_enabled" }
422Erro de validação
json
// contato sem phone e sem email:
{ "errors": { "contact": ["phone or email is required"] } }

// data no passado:
{ "errors": { "scheduled_at": ["must be in the future"] } }

// data impossível, apenas data ou sem o separador T:
{ "errors": { "scheduled_at": ["is invalid"] } }

// telefone informado fora do formato E.164:
{ "errors": { "phone_number": ["should be in e164 format"] } }

// telefone e email não verificam unicamente o mesmo contato conhecido:
{ "errors": { "contact": ["the provided contact information could not be verified"] } }

// profissional não oferece o serviço:
{ "errors": { "service": ["professional does not offer this service"] } }

// conflito de horário:
{ "errors": { "slot": ["time slot conflicts with an existing appointment"] } }
404Inbox, profissional ou serviço inexistente, cross-tenant, inativo, arquivado ou não publicado online.