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
/public/api/v1/inboxes/{inbox_identifier}Retorna a configuração pública do inbox API. A resposta não possui campo active.
{
"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
}Contatos
/public/api/v1/inboxes/{inbox_identifier}/contactsCria um contato no inbox público. O body é opcional; sem ele, a API gera o source_id e o nome.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
source_id | string | Nao | Identificador de sessão; quando omitido, a API gera um UUID |
name | string | Nao | Nome do contato |
email | string | Nao | |
phone_number | string | Nao | Telefone no formato E.164, por exemplo +5511999999999 |
identifier | string | Nao | Identificador externo |
identifier_hash | string | Nao | HMAC-SHA256 do identifier quando a validação de identidade é obrigatória |
avatar_url | string (URL) | Nao | URL pública da imagem a importar de forma assíncrona |
custom_attributes | object | Nao | Atributos personalizados |
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"
}'{
"id": 101,
"source_id": "contact_source_abc123",
"pubsub_token": "token_for_websocket",
"name": "Visitante do Site",
"email": "visitante@email.com",
"phone_number": null
}{ "error": "HMAC failed: Invalid Identifier Hash Provided" }{ "error": "Resource could not be found" }{ "error": "source_id must be a non-empty string" }{
"message": "Phone number should be in e164 format",
"attributes": ["phone_number"]
}/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)
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
identifier(query) | string | Sim | Identificador externo exato usado como mensagem do HMAC |
identifier_hash(query) | string | Sim | Hexadecimal minúsculo de HMAC-SHA256(hmac_token, identifier) |
{ "error": "HMAC failed: Invalid Identifier Hash Provided" }{ "error": "Resource could not be found" }/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
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
identifier | string | Nao | Somente a credencial assinada pelo HMAC; não é encaminhado para a atualização nem altera/mescla o contato |
identifier_hash | string | Nao | Hexadecimal minúsculo de HMAC-SHA256(hmac_token, identifier), exigido junto ao identifier quando configurado |
name | string | Nao | Novo nome; valores vazios não substituem o atual |
email | string | Nao | Novo email; conflito retorna 422 no fluxo assinado e pode mesclar no fluxo legado sem assinatura |
phone_number | string | Nao | Novo telefone E.164; conflito retorna 422 no fluxo assinado e pode mesclar no fluxo legado sem assinatura |
avatar_url | string (URL) | Nao | URL pública da imagem a importar |
custom_attributes | object | Nao | Objeto mesclado aos atributos atuais |
{
"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"
}{ "error": "HMAC failed: Invalid Identifier Hash Provided" }{ "error": "Resource could not be found" }{ "error": "name must be a string" }{
"message": "Phone number has already been taken",
"attributes": ["phone_number"]
}Conversas
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversationsCria uma nova conversa para o contato.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
custom_attributes | object | Nao | Atributos da conversa |
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" } }'{
"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.
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_statusResolve a conversa. Apesar do nome histórico toggle_status, esta rota não reabre uma conversa resolvida.
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/toggle_typingEmite o estado de digitação do contato.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
typing_status | string | Sim | Use exatamente on ou off |
{ "error": "typing_status must be on or off" }/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversationsLista as conversas acessíveis pelo contato; a resposta é um array sem envelope data.
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}Obtém uma conversa pelo display_id, com mensagens públicas embutidas.
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/update_last_seenAtualiza a última visualização do contato e agenda a marcação das mensagens de saída como lidas.
Mensagens
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messagesEnvia uma mensagem na conversa pública.
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
content | string | Nao | Texto da mensagem; pode ser omitido quando há anexo |
echo_id | string | Nao | ID temporário devolvido nos eventos WebSocket; não aparece no corpo HTTP |
attachments[] | file[] | Nao | Arquivos enviados via multipart/form-data |
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" }'{
"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.
/public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messagesLista mensagens públicas da conversa, sem envelope data; mensagens privadas e de atividade são removidas.
Query
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
before(query) | integer | Nao | ID de mensagem para buscar até 20 itens anteriores; sem cursor, retorna os 20 mais recentes |
/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
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
submitted_values | object | array | Nao | Resposta com name, title, value ou csat_survey_response |
submitted_values.csat_survey_response.rating | integer | Nao | Nota da pesquisa CSAT |
submitted_values.csat_survey_response.feedback_message | string | Nao | Comentário da pesquisa CSAT |
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" } } }'{ "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.
/public/api/v1/inboxes/{inbox_identifier}/appointments/servicesLista os serviços ativos e disponíveis para agendamento online.
Path
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inbox_identifier | string | Sim | Identificador público do inbox |
{
"data": [
{
"id": 7,
"name": "Consulta inicial",
"description": "Primeira avaliação",
"duration_minutes": 60,
"default_price_cents": 15000,
"currency": "BRL",
"color": "#2781F6",
"online_available": true
}
]
}/public/api/v1/inboxes/{inbox_identifier}/appointments/professionalsLista os profissionais ativos. Opcionalmente filtra os que atendem um serviço.
Path
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inbox_identifier | string | Sim | Identificador público do inbox |
Query
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
service_id(query) | integer (int64) | Nao | Serviço ativo e publicado online desta conta; filtra profissionais ativos que o oferecem |
{
"data": [
{ "id": 3, "name": "Dra. Ana", "specialty": "Clinica geral", "color": "#10b981" }
]
}/public/api/v1/inboxes/{inbox_identifier}/appointments/slotsRetorna os horários livres de um profissional ativo para um serviço ativo e publicado online que ele realmente oferece.
Path
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inbox_identifier | string | Sim | Identificador público do inbox |
Query
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
professional_id(query) | integer (int64) | Sim | ID do profissional ativo da conta |
service_id(query) | integer (int64) | Sim | ID do serviço ativo/online oferecido pelo profissional |
date(query) | string (date) | Sim | Data a consultar (YYYY-MM-DD) |
{
"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.
/public/api/v1/inboxes/{inbox_identifier}/appointmentsAgendamento anônimo via widget público. Encontra ou cria o contato pelo telefone ou email fornecido.
Path
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
inbox_identifier | string | Sim | Identificador público do inbox |
Body
| Nome | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
professional_id | integer (int64) | Sim | ID de profissional ativo obtido na listagem pública |
service_id | integer (int64) | Sim | ID de serviço ativo e online obtido na listagem pública |
scheduled_at | string (ISO 8601) | Sim | Iní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. |
notes | string | Nao | Observações do cliente |
contact | object | Sim | Dados de identificação do contato. Requer phone/phone_number OU email. |
contact.name | string | Nao | Nome do contato. Padrão: "Visitante" |
contact.phone | string | Nao | Telefone opcional quando há email, mas E.164 é obrigatório quando informado (+ e 2-15 dígitos). Alias: phone_number |
contact.email | string | Nao | Email 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"
}
}'{
"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.
{ "error": "feature_not_enabled" }
// ou
{ "error": "public_widget_not_enabled" }// 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"] } }