Atendimentos

API de agendamento integrada ao chat para gerenciar agenda, profissionais, serviços e lembretes automáticos.

Licença e permissão

Atendimentos é uma funcionalidade nativa disponível em todos os planos. Qualquer membro autenticado da conta pode listar, consultar, criar, consultar disponibilidade e acessar as métricas dentro do seu escopo autorizado. Quando appointments_visibility está em own_professional, listagem, histórico de contato e métricas consideram somente a agenda do profissional vinculado ao usuário; sem vínculo, essas coleções ficam vazias. Leitura e mutações de outro profissional retornam 403. Ações em massa e exportação exigem administrador ou a permissão appointment_manage.

Credencial ausente ou inválida responde 401. Uma conta inexistente, suspensa, malformada ou inacessível ao usuário autenticado responde sempre o mesmo 404 genérico, com { "error": "Resource could not be found" }. As rotas autenticadas não possuem bloqueio nem URL de upgrade por plano.

Envelopes e erros

Respostas de recurso usam { "data": ... }; a listagem acrescenta meta. Parâmetros malformados usam { "error": "..." } com 422. Criação, PATCH e ações de status usam normalmente { "errors": { "campo": ["mensagem"] } }; uma validação propagada pelo handler global, como cancelar a partir de um estado terminal, usa { "message": "...", "attributes": ["status"] }. Uma negativa RBAC reconhecida usa { "error": "forbidden" } com 403.

Base URL

Todos os endpoints usam o prefixo /api/v1/accounts/{account_id}

Fuso de agendamento da conta

Neste guia, o fuso da conta significa: account.reporting_timezone quando válido; depois account.custom_attributes.timezone gravado pelo onboarding; e, sem uma configuração válida, America/Sao_Paulo. Booking, disponibilidade, lembretes, métricas e CSV usam essa mesma resolução. Este fallback é específico de Atendimentos; Follow-ups, Flow Builder e Sequências usam os fusos documentados no guia de Variáveis de Personalização.

Visão Geral

A API autenticada permite operar a agenda pelo dashboard, por automações server-to-server e por integrações próprias. O widget de agendamento sem autenticação possui um contrato separado na página de API pública.

Depois que a criação é confirmada no banco, somente templates ativos e entregáveis via WhatsApp cujo horário de envio ainda está no futuro materializam lembretes pendentes. O job tenta entregá-los posteriormente e procura um vínculo contact_inbox do contato com canal Channel::Whatsapp ou Channel::Api, sem filtro por uma flag active do inbox. Falhas podem deixar o lembrete pendente para nova tentativa ou marcá-lo como falho. A data e a hora usadas no texto do lembrete são renderizadas no fuso de agendamento da conta, nunca diretamente em UTC. Lembretes não fazem parte da projeção desta API.

GET/api/v1/accounts/{account_id}/appointments

Retorna lista paginada de atendimentos com filtros opcionais por data, profissional, serviço, parceiro, status e contato.

Parâmetros de Query

NomeTipoObrigatorioDescricao
from(query)string (date-time)NaoInício em ISO 8601 estrito, com T entre data e hora; data impossível, apenas data ou separador por espaço são rejeitados. Use Z ou offset explícito.
to(query)string (date-time)NaoFim em ISO 8601 estrito, com T entre data e hora; data impossível, apenas data ou separador por espaço são rejeitados. Use Z ou offset explícito.
professional_id(query)integer (int64)NaoFiltrar por profissional; aceita de 1 a 9223372036854775807.
service_id(query)integer (int64)NaoFiltrar por serviço; aceita de 1 a 9223372036854775807.
partner_id(query)integer (int64)NaoFiltrar por parceiro; aceita de 1 a 9223372036854775807.
status(query)stringNaoscheduled | confirmed | completed | cancelled | no_show. Aceita múltiplos valores exatos separados por vírgula (ex: scheduled,confirmed).
contact_id(query)integer (int64)NaoFiltrar por contato da conta.
pipeline_card_id(query)integer (int64)NaoFiltrar por card do pipeline.
conversation_display_id(query)integer (int32)NaoFiltrar por display_id de conversa desta conta; aceita de 1 a 2147483647.
page(query)integerNaoInteiro positivo (padrão 1). Cada página tem 50 registros; o tamanho é fixo.

Filtros não resolvem recursos globais

Os filtros de ID são aplicados sobre a coleção já escopada à conta. Um ID válido de outra conta produz uma lista vazia, sem revelar o recurso. Valor malformado, não positivo, estruturado ou fora do limite retorna 422.

curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments?from=2027-06-01T00:00:00Z&to=2027-06-30T23:59:59Z&status=scheduled" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Lista de summaries (50 por página, ordenada por scheduled_at) e metadados calculados depois dos filtros.
json
{
  "data": [
    {
      "id": 42,
      "cancellation_reason": null,
      "currency": "BRL",
      "ends_at": "2027-08-03T14:00:00.000Z",
      "price_cents": 20000,
      "scheduled_at": "2027-08-03T13:00:00.000Z",
      "status": "scheduled",
      "contact_id": 101,
      "conversation_display_id": null,
      "partner_id": null,
      "pipeline_card_id": null,
      "professional_id": 3,
      "service_id": 7,
      "contact": {
        "id": 101,
        "name": "Maria Silva",
        "avatar_url": null
      },
      "professional": {
        "id": 3,
        "name": "Dr. Santos",
        "specialty": "Odontologia",
        "color": "#3B82F6"
      },
      "service": {
        "id": 7,
        "name": "Limpeza Dental",
        "duration_minutes": 60,
        "default_price_cents": 20000,
        "currency": "BRL",
        "color": "#2781F6"
      },
      "partner": null
    }
  ],
  "meta": {
    "total_count": 1,
    "current_page": 1,
    "total_pages": 1,
    "per_page": 50
  }
}

Summary estável e dados de contato

O index usa uma allowlist reduzida: identificadores de relacionamento, intervalo, status, preço, moeda e motivo de cancelamento, além das associações compactas. Para membros comuns, contact contém somente id, name e avatar_url. Administradores e quem possui appointment_manage recebem também email e phone_number.

401Credencial ausente ou inválida.
404Conta inexistente, suspensa, malformada ou inacessível; o corpo é sempre genérico.
422page não positivo ou fora da faixa, status desconhecido, valor de status que não seja uma string CSV, ou filtro de ID malformado.
POST/api/v1/accounts/{account_id}/appointments

Cria novo atendimento e materializa lembretes automáticos a partir dos templates do serviço.

Body (appointment)

NomeTipoObrigatorioDescricao
contact_idinteger (int64)SimID de contato desta conta, entre 1 e 9223372036854775807.
professional_idinteger (int64)SimID de profissional desta conta, entre 1 e 9223372036854775807.
service_idinteger (int64)SimID de serviço desta conta, entre 1 e 9223372036854775807.
scheduled_atstring (date-time)SimInício em ISO 8601 estrito, com T entre data e hora. Data impossível, apenas data ou separador por espaço são rejeitados. Sem offset, usa o fuso de agendamento da conta.
partner_idinteger (int64) | nullNaoParceiro desta conta.
ends_atstring (date-time) | nullNaoFim em ISO 8601 estrito, com T; data impossível, apenas data ou separador por espaço são rejeitados. Quando enviado, o intervalo customizado é validado e preservado. Se omitido/null, usa scheduled_at + duração efetiva da oferta profissional-serviço (override ou duração base).
notesstring | nullNaoObservações do atendimento.
conversation_display_idinteger (int32) | nullNaodisplay_id de conversa desta conta, entre 1 e 2147483647.
pipeline_card_idinteger (int64) | nullNaoCard existente e autorizado desta conta.
custom_attributesobjectNaoAtributos personalizados (JSONB)

Envelope e referências são estritos

Envie um objeto sob appointment. Campos não suportados, IDs malformados ou datas inválidas retornam 422. Um ID numericamente válido que não exista no escopo autorizado da conta retorna 404; nenhuma referência cross-tenant é criada.

curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments" \
  -H "api_access_token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "appointment": {
      "contact_id": 101,
      "professional_id": 3,
      "service_id": 7,
      "scheduled_at": "2027-08-03T13:00:00Z",
      "notes": "Primeira consulta"
    }
  }'
201Atendimento criado; o corpo retorna a projeção de detalhe em data.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-03T14:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-03T13:00:00.000Z",
    "status": "scheduled",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Primeira consulta",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}

Projeções explícitas e lembretes

Criação, leitura por ID, atualização e ações de status retornam a allowlist de detalhe: o summary do index mais public_id, notes, custom_attributes, cancelled_at, created_at e updated_at. Campos internos de Google, descarte e atores de auditoria não são serializados. A chave partner existe mesmo quando vale null;reminders nunca faz parte desse corpo. O contato segue a regra de PII descrita no index. Serviço, profissional e parceiro usam um escopo histórico com a semântica de with_discarded, portanto continuam aparecendo na projeção depois do arquivamento. partner pode ser null porque esse vínculo é opcional, não por o parceiro ter sido arquivado.

price_cents e currency são snapshots feitos na criação. O preço usa ProfessionalService.override_price_cents quando configurado e, sem override, Service.default_price_cents. O service.default_price_cents embutido continua sendo o valor base atual do serviço, portanto pode diferir do snapshot do atendimento.

401Credencial ausente ou inválida.
404Conta oculta ou referência de contato, profissional, serviço, parceiro, conversa ou card não encontrada no escopo autorizado.
422Payload ou campo malformado, conflito, horário indisponível, data no passado ou outra validação.
json
// Data no passado:
{
  "errors": {
    "scheduled_at": ["must be in the future"]
  }
}

// Horário ocupado (slot já reservado para o profissional):
{
  "errors": {
    "slot": ["time slot conflicts with an existing appointment"]
  }
}

// Fora do horário de trabalho do profissional:
{
  "errors": {
    "slot": ["professional is not available at this time (out of hours)"]
  }
}
GET/api/v1/accounts/{account_id}/appointments/{appointment_id}

Retorna detalhes de um atendimento específico com contact, professional, service e partner embutidos.

Parâmetros de Path

NomeTipoObrigatorioDescricao
account_id(path)integerSimID da conta
appointment_id(path)integer (int64)SimID entre 1 e 9223372036854775807.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/42" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Projeção de detalhe do atendimento em data; lembretes não são incluídos.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-03T14:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-03T13:00:00.000Z",
    "status": "scheduled",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Primeira consulta",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}
401Credencial ausente ou inválida.
403O usuário pertence à conta, mas não pode acessar este atendimento pela regra de visibilidade.
404Conta oculta ou appointment_id válido inexistente ou pertencente a outra conta.
422appointment_id malformado, não positivo ou fora do intervalo bigint.
GET/api/v1/accounts/{account_id}/contacts/{contact_id}/appointment_history

Retorna o histórico paginado de atendimentos de um contato, do mais recente para o mais antigo.

Parâmetros

NomeTipoObrigatorioDescricao
contact_id(path)integer (int64)SimContato desta conta, entre 1 e 9223372036854775807.
page(query)integerNaoInteiro positivo (padrão 1); cada página possui 50 registros.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/contacts/101/appointment_history?page=1" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Timeline autorizada e meta de paginação.
json
{
  "data": [
    {
      "id": 42,
      "public_id": "550e8400-e29b-41d4-a716-446655440000",
      "service_id": 7,
      "scheduled_at": "2027-08-03T13:00:00.000Z",
      "ends_at": "2027-08-03T14:00:00.000Z",
      "status": "scheduled",
      "notes": "Primeira consulta",
      "price_cents": 20000,
      "currency": "BRL",
      "cancelled_at": null,
      "cancellation_reason": null,
      "pipeline_card_id": null,
      "conversation_display_id": null,
      "professional": {
        "id": 3,
        "name": "Dr. Santos",
        "color": "#3B82F6"
      },
      "service": {
        "id": 7,
        "name": "Limpeza Dental",
        "duration_minutes": 60
      },
      "partner": null,
      "created_at": "2027-07-21T12:00:00.000Z"
    }
  ],
  "meta": {
    "total": 1,
    "current_page": 1,
    "total_pages": 1,
    "per_page": 50
  }
}

Visibilidade e registros arquivados

O endpoint usa o mesmo policy_scope da agenda. Em own_professional, ele inclui apenas atendimentos do Professional vinculado ao usuário; sem vínculo, data fica vazio. O service_id permanece no item como snapshot do vínculo. Profissional, serviço e parceiro arquivados continuam resolvidos na projeção histórica pelo escopo with_discarded (ou sua semântica equivalente na associação). partner fica null somente quando o atendimento não possui esse vínculo opcional. Nenhum fallback é fabricado.

401Credencial ausente ou inválida.
404Conta oculta ou contato válido não encontrado nesta conta.
422contact_id ou page malformado, não positivo, estruturado ou fora do intervalo suportado.
GET/api/v1/accounts/{account_id}/appointments/clients

Diretório de quem já foi agendado, agregado sobre TODO o histórico da conta.

Sem janela de datas

Diferente da listagem de agendamentos, aqui não existe recorte por período: um cliente atendido há três anos aparece, e a busca o encontra. Cada linha traz appointments_count com o total de agendamentos do cliente, mais last_appointment_at (última visita passada) e next_appointment_at (próxima visita futura).

Agendamento cancelado ou marcado como no-show conta no total, mas nunca alimenta as duas datas: quem cancelou não foi atendido, e quem faltou não volta naquele horário. partner_name é o parceiro do agendamento mais recente do cliente.

Toda agregação parte do policy_scope de agendamentos, não da conta: no modo own_professional, o agente vê somente os clientes do Professional vinculado a ele, e appointments_counte as duas datas contam apenas os agendamentos que ele pode ver — um contato compartilhado com outro profissional não vaza o volume desse outro.

Os campos de contato seguem a mesma regra da listagem: email e phone_number só aparecem para quem pode gerenciar agendamentos. O CPF do contato nunca é retornado.

Parâmetros de Query

NomeTipoObrigatorioDescricao
q(query)stringNaoBusca sem distinção de maiúsculas, até 120 caracteres. % e _ são tratados como literais. Quem pode gerenciar agendamentos busca por nome, e-mail e telefone; quem não pode busca somente por nome — buscar num campo que a resposta esconde devolveria o dado pelo resultado, permitindo adivinhar um telefone com o total_count confirmando cada palpite.
sort(query)stringNaorecent (padrão): última visita mais recente primeiro. upcoming: próxima visita mais próxima. frequency: mais agendamentos. name: alfabético. Em toda ordenação por data, quem não tem a data vai para o fim. Valor não suportado retorna 422.
page(query)integerNaoPágina, 30 clientes por página. Padrão 1.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/clients?sort=frequency" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Uma linha por cliente
json
{
  "data": [
    {
      "id": 21,
      "name": "Sabrina Oliveira",
      "email": null,
      "phone_number": "+5511988100020",
      "avatar_url": null,
      "partner_name": "Unimed",
      "appointments_count": 18,
      "last_appointment_at": "2026-06-08T08:00:00Z",
      "next_appointment_at": "2027-06-15T13:00:00Z"
    }
  ],
  "meta": {
    "total_count": 11,
    "current_page": 1,
    "total_pages": 1,
    "per_page": 30
  }
}
401Token ausente ou inválido.
422sort fora dos valores suportados, q não escalar ou page fora do intervalo.
PATCH/api/v1/accounts/{account_id}/appointments/{appointment_id}

Atualiza campos do atendimento. Para mudar status use os endpoints de ação (confirm, complete, no_show).

Campos atualizáveis são limitados

O PATCH só permite scheduled_at, notes, partner_id e custom_attributes. ends_at não é aceito no PATCH: ao reagendar, o horário final é recalculado pelo backend a partir da duração efetiva da oferta profissional-serviço. Também não é possível trocar professional_id, service_id ou contact_id — esses campos são definidos apenas na criação. Para mudar status use os endpoints de ação (/confirm, /complete, /no_show).

Body (appointment)

NomeTipoObrigatorioDescricao
scheduled_atstring (date-time)NaoReagendar em ISO 8601 estrito, com T entre data e hora. Data impossível, apenas data ou separador por espaço retornam 422; sem offset, usa o fuso de agendamento da conta. Conflito de slot também retorna 422.
partner_idinteger (int64) | nullNaoTrocar ou remover o parceiro desta conta.
notesstring | nullNaoAtualizar ou limpar as observações.
custom_attributesobjectNaoAtributos personalizados (JSONB)
bash
curl -X PATCH "https://chat.seudominio.com/api/v1/accounts/1/appointments/42" \
  -H "api_access_token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "appointment": { "scheduled_at": "2027-08-04T14:00:00Z", "notes": "Reagendado a pedido da paciente" } }'
200Atendimento atualizado; o corpo retorna { data: appointment }.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-04T15:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-04T14:00:00.000Z",
    "status": "scheduled",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Reagendado a pedido da paciente",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}
401Credencial ausente ou inválida.
403O usuário não pode alterar este atendimento pela regra de visibilidade.
404Conta ou atendimento oculto, ou partner_id válido fora do escopo da conta.
422Envelope/campo malformado, campo não suportado, ou reagendamento inválido, no passado, fora do horário ou em conflito.
DELETE/api/v1/accounts/{account_id}/appointments/{appointment_id}

Cancela o atendimento sem remover o registro. Repetir o DELETE em um atendimento já cancelled é idempotente e retorna 204.

Escopo de conta

O appointment_id é resolvido dentro da conta autenticada (Current.account.appointments.find). Não é um ID global — tentar cancelar um atendimento de outra conta retorna 404.

Parâmetros de Query

NomeTipoObrigatorioDescricao
reason(query)stringNaoMotivo do cancelamento
bash
curl -X DELETE "https://chat.seudominio.com/api/v1/accounts/1/appointments/42?reason=Paciente%20solicitou%20via%20WhatsApp" \
  -H "api_access_token: YOUR_TOKEN"
204Atendimento cancelado, ou já cancelado. A resposta não possui corpo; uma repetição não sobrescreve o motivo original.
401Credencial ausente ou inválida.
403O usuário não pode cancelar este atendimento pela regra de visibilidade.
404Conta ou atendimento não encontrado no escopo autorizado.
422reason não é string/null ou o status atual não pode transicionar para cancelled.
POST/api/v1/accounts/{account_id}/appointments/{appointment_id}/confirm

Transiciona scheduled para confirmed. Repetir em confirmed é idempotente e retorna 200; estados terminais retornam 422.

bash
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments/42/confirm" \
  -H "api_access_token: YOUR_TOKEN"
200Projeção completa com status confirmed. A primeira transição dispara appointment.confirmed.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-03T14:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-03T13:00:00.000Z",
    "status": "confirmed",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Primeira consulta",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}
401Credencial ausente ou inválida.
403Usuário sem acesso a este atendimento.
404Conta ou atendimento não encontrado no escopo autorizado.
422Transição de status inválida.
POST/api/v1/accounts/{account_id}/appointments/{appointment_id}/complete

Transiciona scheduled ou confirmed para completed. Não aceita body; repetir em completed é idempotente e retorna 200.

bash
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments/42/complete" \
  -H "api_access_token: YOUR_TOKEN"
200Projeção completa com status completed. A primeira transição dispara appointment.completed.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-03T14:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-03T13:00:00.000Z",
    "status": "completed",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Primeira consulta",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}
401Credencial ausente ou inválida.
403Usuário sem acesso a este atendimento.
404Conta ou atendimento não encontrado no escopo autorizado.
422Transição inválida a partir de cancelled ou no_show.
POST/api/v1/accounts/{account_id}/appointments/{appointment_id}/no_show

Transiciona scheduled ou confirmed para no_show. Não aceita body; repetir em no_show é idempotente e retorna 200.

bash
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments/42/no_show" \
  -H "api_access_token: YOUR_TOKEN"
200Projeção completa com status no_show. A primeira transição dispara appointment.no_show.
json
{
  "data": {
    "id": 42,
    "cancellation_reason": null,
    "currency": "BRL",
    "ends_at": "2027-08-03T14:00:00.000Z",
    "price_cents": 20000,
    "scheduled_at": "2027-08-03T13:00:00.000Z",
    "status": "no_show",
    "contact_id": 101,
    "conversation_display_id": null,
    "partner_id": null,
    "pipeline_card_id": null,
    "professional_id": 3,
    "service_id": 7,
    "contact": {
      "id": 101,
      "name": "Maria Silva",
      "avatar_url": null
    },
    "professional": {
      "id": 3,
      "name": "Dr. Santos",
      "specialty": "Odontologia",
      "color": "#3B82F6"
    },
    "service": {
      "id": 7,
      "name": "Limpeza Dental",
      "duration_minutes": 60,
      "default_price_cents": 20000,
      "currency": "BRL",
      "color": "#2781F6"
    },
    "partner": null,
    "public_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "Primeira consulta",
    "custom_attributes": {},
    "cancelled_at": null,
    "created_at": "2027-07-21T12:00:00.000Z",
    "updated_at": "2027-07-21T12:00:00.000Z"
  }
}
401Credencial ausente ou inválida.
403Usuário sem acesso a este atendimento.
404Conta ou atendimento não encontrado no escopo autorizado.
422Transição inválida a partir de cancelled ou completed.
GET/api/v1/accounts/{account_id}/appointments/availability

Retorna slots disponíveis para um profissional em uma data, considerando horário de trabalho, buffer e consultas existentes.

Parâmetros de Query

NomeTipoObrigatorioDescricao
professional_id(query)integer (int64)SimProfissional desta conta; aceita de 1 a 9223372036854775807.
date(query)string (date)SimData estrita em YYYY-MM-DD.
service_id(query)integer (int64)NaoServiço desta conta oferecido pelo profissional; usa o override de duração da oferta quando configurado.
duration_minutes(query)integer (int32)NaoDuração entre 1 e 2147483647 quando service_id não for informado (padrão 60). Se enviado com service_id, ainda é validado, mas a duração efetiva da oferta prevalece.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/availability?date=2027-08-03&professional_id=3&service_id=7" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Slots disponíveis (apenas inícios livres, como array de timestamps ISO)
json
{
  "data": {
    "date": "2027-08-03",
    "professional_id": 3,
    "slots": [
      "2027-08-03T08:00:00-03:00",
      "2027-08-03T08:15:00-03:00",
      "2027-08-03T08:30:00-03:00"
    ]
  }
}

Slots já conflitantes não aparecem

A resposta lista apenas os horários de início disponíveis (array de timestamps ISO 8601). Horários ocupados são omitidos — não há campo available: false. A grade avança em 15 minutos e usa o fuso de agendamento da conta; duração, buffer, horário de trabalho, compromissos existentes e horários já passados participam do cálculo.

Com service_id, a duração usada é o override_duration_minutes da oferta desse profissional, quando existir; caso contrário, vale service.duration_minutes.

401Credencial ausente ou inválida.
404Profissional/serviço não encontrado nesta conta, ou profissional não oferece o serviço.
422professional_id, service_id ou duration_minutes malformado/fora da faixa, ou date ausente/inválida.
GET/api/v1/accounts/{account_id}/appointments/availability_range

Mesma resposta de /availability, para todos os dias entre from e to, em uma requisição só.

Parâmetros de Query

NomeTipoObrigatorioDescricao
professional_id(query)integer (int64)SimProfissional desta conta; aceita de 1 a 9223372036854775807.
from(query)string (date)SimPrimeiro dia do intervalo, em YYYY-MM-DD, inclusive.
to(query)string (date)SimÚltimo dia do intervalo, em YYYY-MM-DD, inclusive. Não pode ser anterior a from, e o intervalo tem no máximo 42 dias.
service_id(query)integer (int64)NaoServiço desta conta oferecido pelo profissional; usa o override de duração da oferta quando configurado.
duration_minutes(query)integer (int32)NaoDuração entre 1 e 2147483647 quando service_id não for informado (padrão 60).
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/availability_range?from=2027-08-02&to=2027-08-08&professional_id=3&service_id=7" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Um item por dia do intervalo, em ordem crescente
json
{
  "data": {
    "professional_id": 3,
    "duration_minutes": 60,
    "days": [
      {
        "date": "2027-08-02",
        "slots": [
          "2027-08-02T08:00:00-03:00",
          "2027-08-02T08:15:00-03:00"
        ]
      },
      {
        "date": "2027-08-03",
        "slots": []
      }
    ]
  }
}

Todo dia do intervalo vem na resposta

As regras são exatamente as de /availability — horário de trabalho, buffer, duração do serviço, compromissos existentes e horários já passados — aplicadas dia a dia. O que muda é o número de requisições: desenhar uma semana pediria sete chamadas, e um mês, quarenta e duas.

Dias em que o profissional não atende aparecem com slots vazio, em vez de serem omitidos: um dia ausente seria indistinguível de um dia sem horário livre, e quem chama não teria como separar uma falha na resposta de uma agenda fechada.

401Credencial ausente ou inválida.
404Profissional/serviço não encontrado nesta conta, ou profissional não oferece o serviço.
422professional_id, service_id ou duration_minutes malformado/fora da faixa; from/to ausente ou inválido; to anterior a from; ou intervalo maior que 42 dias.
GET/api/v1/accounts/{account_id}/appointments/available_professionals

Lista profissionais ativos que podem atender um intervalo específico, considerando horário de trabalho, duração efetiva, buffer e conflitos.

Parâmetros de Query

NomeTipoObrigatorioDescricao
scheduled_at(query)string (date-time)SimInício em ISO 8601 estrito, com T entre data e hora. Data impossível, apenas data ou separador por espaço são rejeitados; sem offset, usa o fuso de agendamento da conta.
service_id(query)integer (int64)NaoServiço desta conta; filtra profissionais ativos que o oferecem e aplica a duração efetiva de cada oferta.
duration_minutes(query)integer (int32)NaoDuração de 1 a 2147483647 quando service_id é omitido (padrão 60). Se enviado com service_id, ainda é validado.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/available_professionals?scheduled_at=2027-08-05T10:00:00-03:00&service_id=7" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Profissionais disponíveis e contagens antes/depois dos filtros de horário e conflito.
json
{
  "data": {
    "scheduled_at": "2027-08-05T10:00:00-03:00",
    "ends_at": "2027-08-05T11:00:00-03:00",
    "duration_minutes": 60,
    "service_id": 7,
    "total_professionals": 2,
    "available_count": 1,
    "professionals": [
      {
        "id": 3,
        "name": "Dr. Santos",
        "specialty": "Odontologia",
        "color": "#3B82F6",
        "service_ids": [7]
      }
    ]
  }
}

Conflitos considerados

Atendimentos cancelled e no_show não bloqueiam o intervalo. Com service_id, apenas profissionais ativos que oferecem esse serviço entram no conjunto avaliado. O buffer é aplicado antes e depois dos compromissos.

duration_minutes e ends_at no nível superior continuam descrevendo a duração base do serviço (ou o parâmetro/default quando não há serviço). Entretanto, cada candidato é testado com sua própria duração efetiva: um override pode ampliar ou reduzir a janela avaliada. Confirme os slots do profissional escolhido em /appointments/availability antes de criar o atendimento.

401Credencial ausente ou inválida.
404service_id válido não encontrado nesta conta.
422scheduled_at ausente/inválido, ou service_id/duration_minutes malformado ou fora da faixa.
GET/api/v1/accounts/{account_id}/appointments/metrics

Retorna todas as métricas produzidas pelo relatório de atendimentos para um intervalo efetivo de até um ano.

Janela efetiva e fuso

Sem parâmetros, from usa o início do dia de 30 dias atrás e to usa o fim do dia atual, ambos no fuso de agendamento da conta. Se o intervalo solicitado exceder um ano, o backend avança from para um ano antes de to. data.range informa os timestamps efetivamente usados; os dois limites são inclusivos. by_day e by_day_status agrupam pelo mesmo fuso da conta.

A agregação parte do mesmo policy_scope da listagem. No modo own_professional, um agente vê somente suas contagens, receitas e agrupamentos; sem Professional vinculado, recebe métricas zeradas. Dados de outros profissionais nunca entram no resultado restrito.

Parâmetros de Query

NomeTipoObrigatorioDescricao
from(query)string (date-time)NaoInício ISO 8601 estrito, com T; data impossível, apenas data ou espaço são rejeitados. Offset/Z preserva o instante; sem offset, usa o fuso da conta. Padrão: início do dia de 30 dias atrás nesse fuso.
to(query)string (date-time)NaoFim ISO 8601 estrito, com T; data impossível, apenas data ou espaço são rejeitados. Offset/Z preserva o instante; sem offset, usa o fuso da conta. Padrão: fim do dia atual nesse fuso.
bash
curl -s "https://chat.seudominio.com/api/v1/accounts/1/appointments/metrics?from=2027-08-01T00:00:00&to=2027-08-31T23:59:59" \
  -H "api_access_token: YOUR_TOKEN" | jq .
200Métricas agregadas do período
json
{
  "data": {
    "total_count": 4,
    "by_status": {
      "scheduled": 1,
      "confirmed": 0,
      "completed": 2,
      "cancelled": 0,
      "no_show": 1
    },
    "completed_count": 2,
    "no_show_count": 1,
    "no_show_rate": 33.33,
    "revenue_realized_cents": 40000,
    "revenue_forecast_cents": 20000,
    "by_professional": [
      { "id": 3, "name": "Dr. Santos", "count": 4, "revenue_cents": 60000 }
    ],
    "by_service": [
      { "id": 7, "name": "Limpeza Dental", "count": 4, "revenue_cents": 60000 }
    ],
    "by_day": [
      { "date": "2027-08-02", "count": 2 },
      { "date": "2027-08-03", "count": 2 }
    ],
    "by_day_status": [
      { "date": "2027-08-02", "scheduled": 0, "confirmed": 0, "completed": 1, "cancelled": 0, "no_show": 1 },
      { "date": "2027-08-03", "scheduled": 1, "confirmed": 0, "completed": 1, "cancelled": 0, "no_show": 0 }
    ],
    "range": {
      "from": "2027-08-01T00:00:00-03:00",
      "to": "2027-08-31T23:59:59-03:00"
    }
  }
}

Como as métricas são calculadas

revenue_realized_cents soma o preço de atendimentos completed;revenue_forecast_cents soma scheduled e confirmed. A taxa de no-show usa como denominador apenas atendimentos finalizados: completed + no_show + cancelled. Os agrupamentos por profissional e serviço retornam id, name, count e revenue_cents.

401Credencial ausente ou inválida.
404Conta inexistente, suspensa, malformada ou inacessível; resposta genérica.
422from ou to fora do ISO 8601 estrito com T, incluindo data impossível, apenas data ou separador por espaço.
json
{ "error": "invalid date" }
POST/api/v1/accounts/{account_id}/appointments/bulk_action

Aplica uma ação de status a múltiplos atendimentos simultaneamente.

Escopo de conta e ações suportadas

Todos os IDs precisam existir no escopo autorizado. Se um único ID válido estiver ausente, pertencer a outra conta ou ficar fora da visibilidade do usuário, a API retorna 404 antes de aplicar qualquer ação. O lote não ignora IDs silenciosamente.

Ações suportadas: confirm, cancel, no_show. Não existe ação complete em massa — qualquer outro valor retorna 422.

Use bulk_action, não action

O verbo viaja sob a chave bulk_action — a chave action é reservada pelo roteamento do Rails e seria ignorada. Cada atendimento é processado de forma independente depois que todos os IDs foram resolvidos: uma transição de status inválida em um registro não derruba os demais e aparece em failed. O 200 só se aplica a lotes estruturalmente válidos e inteiramente resolvidos.

Body

NomeTipoObrigatorioDescricao
idsarray[integer (int64)]SimDe 1 a 100 IDs positivos, únicos e dentro do limite bigint.
bulk_actionstringSimconfirm | cancel | no_show
reasonstring | nullNaoMotivo usado somente quando bulk_action=cancel.
bash
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments/bulk_action" \
  -H "api_access_token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "ids": [42, 43, 44], "bulk_action": "confirm" }'
200Resultado por registro de um lote válido e resolvido. count é o número de sucessos.
json
{
  "data": {
    "action": "confirm",
    "count": 2,
    "succeeded": [42, 43],
    "failed": [
      { "id": 44, "errors": { "status": ["cannot transition from cancelled to confirmed"] } }
    ]
  }
}
401Credencial ausente ou inválida.
403Exige administrador ou a permissão appointment_manage.
404Ao menos um ID não foi encontrado no escopo autorizado; nenhuma ação é aplicada.
422ids ausente, não-array, vazio, duplicado, malformado ou com mais de 100 itens; bulk_action inválido; ou reason malformado.
GET/api/v1/accounts/{account_id}/appointments/export.csv

Exporta os atendimentos em CSV (anexo), com horários no fuso de agendamento da conta. Endpoint privilegiado (admin ou custom role appointment_manage). Respeita os mesmos filtros do index (from, to, professional_id, service_id, partner_id, status, contact_id, pipeline_card_id, conversation_display_id).

200Arquivo CSV (text/csv) com cabeçalho + uma linha por atendimento
json
ID,Status,Scheduled at,Ends at,Contact,Professional,Service,Price,Currency,Cancellation reason
42,confirmed,2027-06-15T09:00:00-03:00,2027-06-15T10:00:00-03:00,Maria Souza,Dr Joao,Limpeza,150.0,BRL,

Fuso dos filtros e do arquivo

from e to exigem ISO 8601 estrito com T entre data e hora; data impossível, apenas data ou separador por espaço retornam 422. Um offset/Z preserva o instante e um valor sem offset usa o fuso da conta; os dois limites são inclusivos. Scheduled at e Ends at são convertidos para esse fuso antes de entrar no CSV; a data do nome do arquivo também é a data local da conta.

401Credencial ausente ou inválida.
403Exige administrador ou a permissão appointment_manage.
422Filtro de data, status ou ID malformado ou fora da faixa.
POST/api/v1/accounts/{account_id}/appointments/{appointment_id}/sync_to_google

Rota reservada para compatibilidade futura. A sincronização individual ainda não está implementada.

Não trate esta rota como sucesso

Depois de autenticar, escopar a conta e autorizar o atendimento, a rota sempre retorna 501. Ela não cria nem atualiza um evento no Google Calendar.

bash
curl -X POST "https://chat.seudominio.com/api/v1/accounts/1/appointments/42/sync_to_google" \
  -H "api_access_token: YOUR_TOKEN"
501Sincronização não implementada.
json
{ "error": "Google Calendar sync not yet implemented" }
401Credencial ausente ou inválida.
403Usuário sem acesso a este atendimento.
404Conta ou atendimento não encontrado no escopo autorizado.

Transições de Status

O modelo Appointment implementa um guard de transição de status. Apenas as transições listadas abaixo são permitidas. Os endpoints confirm, complete e no_show retornam 422 com:

{"errors":{"status":["cannot transition from X to Y"]}}
Status atualPode transicionar para
scheduledconfirmed, cancelled, no_show, completed
confirmedcompleted, cancelled, no_show
completed— (terminal)
cancelled— (terminal)
no_show— (terminal)

Endpoints de ação

Use os endpoints dedicados /confirm, /complete, /no_show para transicionar status. O endpoint PATCH só atualiza campos de dados (scheduled_at, notes, partner_id, custom_attributes). Repetir uma ação quando o atendimento já está no status de destino preserva o estado e retorna 200. No DELETE de cancelamento, uma transição inválida também retorna 422, mas pelo envelope global message + attributes descrito no início da página.