v1server-to-serverJSON

API Externa da Alana

Interface HTTP autenticada por API key para integrar sistemas parceiros à Alana. Cada chave pertence a uma clínica — você nunca envia o id da clínica, a chave já resolve o tenant.

Base URL
…/external/v1
Rate limit por chave
60 req/min · 1000 req/h
CORS
desabilitado (uso server-to-server)

Visão geral

Quick start

1. Crie uma chave

No painel da Alana, em Personalizar Alana → API Externa, clique em Criar chave, nomeie e selecione apenas os escopos necessários. O segredo aparece uma única vez — copie e guarde no cofre de credenciais.

2. Confirme a credencial

curl -s BASE/me \
  -H "Authorization: Bearer SUA_CHAVE"

Resposta traz o nome, o timezone e o expediente da clínica, além dos escopos da chave.

3. Faça a primeira chamada útil

curl -s "BASE/contacts?limit=5" \
  -H "Authorization: Bearer SUA_CHAVE"

Autenticação

Toda requisição precisa do header:

Authorization: Bearer alk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Sem o header, ou com uma chave inválida/revogada, a resposta é 401 unauthorized. Se a chave é válida mas não tem o escopo necessário, é 403 forbidden.

Escopos

Cada chave carrega uma lista de escopos. Peça só o que a integração usa.

EscopoPermite
identity.readGET /me
contacts.readListar e ler contatos
contacts.writeCriar e editar contatos e suas etiquetas
crm.readLer etapas do funil e o board
crm.writeMover um contato de etapa
catalog.readLer procedimentos e produtos
professionals.readLer profissionais
agenda.readLer agenda e disponibilidade
agenda.writeCriar e alterar agendamentos

Rate limit

Por chave: 60 requisições por minuto e 1000 por hora. Ao estourar, a resposta é 429 rate_limited. Os headers RateLimit-* acompanham a janela de minuto.

Idempotência

Em POST e PATCH, envie um header Idempotency-Key com um valor único por operação (ex.: um UUID). Se a mesma chave chegar de novo, a Alana devolve a mesma resposta da primeira vez, com o header Idempotency-Replayed: true, sem reexecutar a ação. Use isso para retries seguros.

curl -X POST BASE/appointments \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0b1e6a-..." \
  -d '{ "contact": { "phone": "5511999999999" }, "procedure_id": "...", "start": "2026-09-10T13:00:00Z" }'

Erros

O corpo de erro é sempre:

{ "error": { "code": "invalid_request", "message": "phone é obrigatório..." } }
HTTPcodeQuando
400 / 422invalid_requestCorpo ou parâmetro inválido
401unauthorizedChave ausente, inválida ou revogada
403forbiddenA chave não tem o escopo
404not_foundRecurso ou endpoint inexistente
409conflictEx.: horário já ocupado
429rate_limitedLimite de requisições
500internalErro interno — pode repetir

Paginação

Listas de contatos usam cursor: a resposta traz { "data": [...], "next_cursor": "..." }. Passe ?cursor= com esse valor para a próxima página; next_cursor: null significa fim. ?limit= vai de 1 a 200 (padrão 50).

Identidade

GET /me

Escopo identity.read. Valida a chave e devolve os dados da clínica.

{
  "clinic": { "id": "...", "name": "Diamond Clinic", "timezone": "America/Sao_Paulo",
              "work_days": "1,2,3,4,5", "work_start_hour": 9, "work_end_hour": 19 },
  "key": { "name": "Integração site", "scopes": ["identity.read", "contacts.read"] }
}

Contatos

GET /contacts

Escopo contacts.read.

QueryDescrição
searchFiltra por nome ou telefone
limit1–200 (padrão 50)
cursorDa página anterior
GET /contacts/:id

Escopo contacts.read. Objeto do contato:

{
  "id": "...", "name": "Odete", "phone": "5514999...", "email": null, "cpf": null,
  "notes": null, "birth_date": "1980-04-12", "funnel_stage": "novo_lead",
  "tags": [{ "id": "...", "label": "Indicação", "color": "#16a34a" }],
  "created_at": "2026-08-30T19:25:00.000Z"
}
POST /contacts

Escopo contacts.write. Cria ou atualiza pelo telefone (upsert).

Campo
phone *Só dígitos, DDI+DDD
name, email, cpf, notesOpcionais
birth_dateAAAA-MM-DD
tagsArray de rótulos (texto). Cria a etiqueta se não existir e substitui as do contato.
PATCH /contacts/:id

Escopo contacts.write. Atualiza só os campos enviados.

CRM

GET /crm/stages

Escopo crm.read. Etapas do funil: { id, label, color, kind, order }. kindaberta | avaliacao_agendada | ganho | pos_procedimento | perdido.

GET /crm/board

Escopo crm.read. Etapas com a lista de contatos em cada uma.

POST /contacts/:id/stage

Escopo crm.write. Corpo { "stage": "interesse_confirmado" } (um id de /crm/stages).

GET /procedures

Escopo catalog.read. { id, name, duration_min, price, price_variable, payment_methods, description }.

GET /products

Escopo catalog.read. { id, name, price, description }.

Profissionais

GET /professionals

Escopo professionals.read. { id, name, bio, instagram, procedures: [{ id, name }] }.

Agenda

GET /appointments

Escopo agenda.read. Query: from, to (ISO; padrão -7d a +30d), status.

{
  "data": [{
    "id": "...", "start": "2026-09-10T13:00:00.000Z", "start_local": "quinta-feira, 10/09 às 10:00",
    "status": "confirmed", "patient_confirmed": false,
    "procedure": { "id": "...", "name": "Toxina botulínica" },
    "professional": { "id": "...", "name": "Dra. Natieli" },
    "contact": { "id": "...", "name": "Odete", "phone": "5514999..." }
  }]
}
GET /availability

Escopo agenda.read. Query: procedure_id * , professional_id (opcional), days (1–30, padrão 10). Retorna horários livres já considerando expediente, duração, conflitos, bloqueios e a agenda de cada profissional.

{ "data": [{ "start": "2026-09-10T13:00:00.000Z", "start_local": "quinta-feira, 10/09 às 10:00",
             "professional": { "id": "...", "name": "Dra. Natieli" } }] }
POST /appointments

Escopo agenda.write. Revalida o horário dentro de uma transação — se acabou de ser ocupado, responde 409 conflict.

{
  "contact": { "phone": "5511999999999", "name": "Odete" },
  "procedure_id": "...", "professional_id": null,
  "start": "2026-09-10T13:00:00Z"
}
PATCH /appointments/:id

Escopo agenda.write. Corpo com status (confirmed|completed|cancelled), start e/ou professional_id. Ao cancelar, a Alana oferece a vaga liberada para a lista de espera.

Checklist do parceiro

  1. Crie a chave com os escopos mínimos e guarde o segredo no cofre.
  2. Valide com GET /me e leia o timezone da clínica.
  3. Converta horários para UTC antes de enviar (start sempre em ISO/UTC).
  4. Use Idempotency-Key em toda escrita e trate 409/429 com retry.
  5. Paginе contatos por next_cursor.
  6. Nunca chame a API do navegador do cliente final — só do seu backend.
  7. Ao desativar a integração, revogue a chave no painel.