CliniDeckDocumentação para integradores

API de integração do CliniDeck: guia do parceiro (v1)

Documento público para o integrador, publicado em https://clinideck.com.br/integration/api. O contrato formal das rotas é o openapi.v1.json, gerado do código e conferido por teste a cada mudança. Nada aqui é segredo: credencial e dado de paciente nunca aparecem neste guia.

Contrato OpenAPI (openapi.v1.json)

A API v1 deixa o site da clínica e o chatbot de IA do parceiro conduzirem a agenda online da clínica: listar especialidades, profissionais, procedimentos e horários livres, reservar e acompanhar a reserva. O site e o chatbot usam a mesma API e a mesma credencial. A reserva segue as mesmas regras da agenda online da clínica.

Endereço base: https://api.clinideck.com.br/api/integration/v1

1. Credencial

São duas peças, entregues por links de visualização única:

PeçaO que éQuantas
Token do integrador (cdk_int_...)identifica o seu softwareum por integrador
Login e senha da integraçãoidentificam a clínica que autorizou vocêum par por clínica

Toda chamada leva os quatro headers:

Authorization: Bearer <token do integrador>
X-Integration-Type: AGENDA_ONLINE
X-Integration-Login: <login da clínica>
X-Integration-Password: <senha da clínica>

A clínica sai do login e da senha, nunca do corpo nem da URL. O token sozinho não abre nada, e a senha de uma clínica não alcança outra.

Como a credencial chega

  1. O CliniDeck cadastra o seu software com o nome, o CNPJ ou CPF e o e-mail técnico.
  2. O e-mail técnico recebe um link de confirmação, que vale 48 horas. Abrir o link não confirma; quem confirma é o botão da página.
  3. Confirmado o e-mail, ele recebe o termo de uso da API de integração. O link do termo vale 7 dias. Leia as páginas em ordem, até a última.
  4. Na hora de assinar, peça o código: ele vai ao mesmo e-mail técnico e vale 10 minutos. Informe o seu nome completo e o cargo, marque os itens do termo e digite o código. São até 5 códigos e 5 tentativas por link; esgotou, peça ao CliniDeck um link novo, que invalida o anterior.
  5. Assinado o termo, o e-mail técnico recebe dois e-mails: a cópia em PDF, com o certificado da assinatura, e o link do token do integrador. Sem a assinatura, não existe token.
  6. Cada clínica autoriza o seu software na tela dela, aceitando o termo de compartilhamento da clínica. O login e a senha daquela clínica chegam ao mesmo e-mail técnico, por outro link de visualização única. A clínica nunca vê a senha.

Os links de visualização única valem 48 horas e só se consomem pelo botão da página: abrir o link (inclusive o robô do antivírus ou a prévia do aplicativo de mensagens) não gasta nada. Vencido o link, peça ao CliniDeck um novo. Uma versão nova do termo de uso não derruba o token nem as clínicas já autorizadas.

Quando o CliniDeck emite um token ou uma senha nova no lugar da atual, a anterior segue aceita por 7 dias: troque-a no seu servidor dentro desse prazo.

Credencial ausente, inválida, revogada ou sem permissão para a rota recebe sempre a mesma resposta:

HTTP/1.1 401 Unauthorized

{ "code": "integration.unauthorized", "message": "Invalid or missing integration credentials." }

Guarde a credencial num cofre de segredos do seu servidor. Ela nunca vai para o navegador do paciente nem para o prompt do modelo de IA: quem chama a API é o seu backend.

2. Respostas nunca entram em cache

Toda resposta, inclusive a de erro, sai com:

Cache-Control: no-store, private
Pragma: no-cache
Vary: Authorization, X-Integration-Type, X-Integration-Login, X-Integration-Password, Idempotency-Key

A URL é a mesma para todas as clínicas: o que muda são os headers. Não ponha CDN nem cache na frente destas chamadas.

3. Rotas

RotaPara quê
GET /specialtiesespecialidades atendidas pela agenda online da clínica
GET /professionals?specialty=profissionais agendáveis, com filtro opcional por especialidade (ignora maiúsculas e acentos)
GET /procedures?professionalId=procedimentos do profissional, com preço em centavos e duração
GET /availability?professionalId=&month=YYYY-MMhorários livres do mês, no fuso da clínica (sem month, o mês corrente)
POST /bookingsreserva um horário
GET /bookings/{id}situação de uma reserva que você criou

Cada horário de availability traz startsAt (instante UTC, o valor que a reserva recebe), time (hora local da clínica), durationMinutes e group. bookingEnabled: false quer dizer que a clínica não recebe reserva agora, e days vem vazio.

O profissional traz as restrições que a reserva confere: idade mínima e máxima, sexos atendidos, antecedência mínima em minutos e horizonte máximo em dias (0 é sem limite). Use-as para não oferecer o que vai ser recusado.

4. Reservar

POST /bookings
Idempotency-Key: 7f9c2b1e-4a1d-4f3e-9c55-2d8f0b6a1e10

{
  "patient": {
    "name": "Nome da paciente",
    "document": "CPF só com números",
    "birthDate": "1990-05-20",
    "phone": "47999999999",
    "email": "[email protected]"
  },
  "professionalId": "<id do profissional>",
  "procedureId": "<id do procedimento>",
  "startsAt": "<startsAt de um horário de /availability>"
}

O paciente novo leva name, email, document, birthDate e phone, todos obrigatórios. Os que faltarem entre name, email, document e phone voltam juntos numa recusa só, com o caminho de cada um:

{ "code": "validation_failed", "message": "Patient email, phone are required.", "details": { "fields": [{ "field": "patient.email", "rules": ["isNotEmpty"] }, { "field": "patient.phone", "rules": ["isNotEmpty"] }] } }

A birthDate ausente ou inválida recebe 400 publicCheckout.patientBirthDateRequired, nos dois modos. O gender (MALE, FEMALE ou OTHER) é opcional, mas obrigatório quando o profissional restringe os sexos atendidos: sem ele, a recusa é 409 publicCheckout.genderNotAttended.

O resultado depende da regra da clínica:

  • A clínica exige pagamento (o padrão): a reserva nasce AWAITING_PAYMENT com o checkoutUrl. Envie o link à paciente. Sem pagamento no prazo, a reserva é cancelada e o horário volta à grade.
  • A clínica não exige pagamento: a reserva nasce PENDING_CONFIRMATION, sem link.

Uma reserva por checkout. Cada POST /bookings reserva um horário e gera, quando a clínica exige pagamento, um link próprio. Pacote de sessões se reserva pela 1ª sessão; as demais a clínica combina com a paciente.

Paciente que já é da clínica

Mande useExistingPatient: true, o CPF e a data de nascimento:

{ "patient": { "useExistingPatient": true, "document": "CPF só com números", "birthDate": "1990-05-20" }, "...": "..." }

CPF desconhecido e data que não confere recebem a mesma recusa, 400 publicCheckout.existingPatientNotFound. A API não diz se alguém é paciente da clínica. Depois de 10 tentativas com o mesmo CPF em 24 horas, a recusa passa a ser 429 publicCheckout.existingPatientTooManyAttempts, valha a data certa ou não.

Regras que a reserva confere

RecusaCódigo
menor de 18 anos400 publicCheckout.patientUnder18
idade ou sexo fora do que o profissional atende409 publicCheckout.ageNotAttended, 409 publicCheckout.genderNotAttended
horário fora da antecedência mínima409 publicCheckout.minBookingNotice
horário além do horizonte do profissional409 publicCheckout.maxBookingHorizon
horário tomado ou fora da grade400 appointment.slotUnavailable
profissional que não é da agenda online da clínica404 integration.professionalNotFound
clínica que exige pagamento e não tem como cobrar agora (bookingEnabled: false em availability)400 publicCheckout.paymentsNotConfigured
clínica que não recebe reserva online agora409 onlineBooking.unavailable (vale para toda rota, menos GET /bookings/{id})
outra reserva da mesma credencial ainda em curso409 integration.bookingBusy: tente de novo em instantes, com a mesma Idempotency-Key

5. Idempotency-Key

Obrigatória em todo POST: de 1 a 255 caracteres ASCII visíveis. Um UUID novo por reserva pretendida serve.

SituaçãoResposta
sem a chave, ou malformada400 idempotency.keyRequired, 400 idempotency.keyInvalid
a mesma chave com o mesmo corpoa mesma reserva, com o status original e o header Idempotent-Replayed: true
a mesma chave com outro corpo422 idempotency.keyReused
a mesma chave enquanto o 1º pedido ainda corre409 idempotency.inProgress: tente de novo em instantes
o pedido falhou (horário tomado, validação, indisponível)a chave fica livre para uma nova tentativa

A chave que gerou uma reserva fica presa a ela para sempre: não reuse chave entre reservas. A resposta repetida traz a situação ATUAL da reserva, não a do momento da criação.

Ao perder a resposta (timeout, queda de rede), refaça o pedido com a MESMA chave e o MESMO corpo: você recebe a reserva que já existe, sem criar outra.

6. Situação da reserva

GET /bookings/{id} só enxerga as reservas que a sua credencial criou. O id de outra clínica, de outro integrador ou da recepção recebe o mesmo 404 integration.bookingNotFound de um id que não existe. A consulta segue respondendo quando a clínica deixa de receber reserva online.

statusQuer dizer
AWAITING_PAYMENTespera o pagamento; checkoutUrl vem preenchido
PENDING_CONFIRMATIONa clínica não exige pagamento e ainda não confirmou
CONFIRMEDconfirmada (paga, confirmada pela clínica ou em atendimento)
CANCELEDcancelada, vencida sem pagamento ou excluída
COMPLETEDatendimento finalizado
NO_SHOWa paciente não compareceu

Para saber quando a situação muda sem consultar, use o webhook (seção 10). A consulta conta no teto de leitura.

7. Limites

TetoValorRecusa
leituras por credencial300, repondo 5 por segundo429 rate_limited
reservas por credencial30, repondo 1 a cada 2 segundos429 rate_limited
credenciais recusadas por IP120, repondo 2 por segundo429 rate_limited
reservas em aberto por credencial20409 integration.pendingLimitReached

O teto é por credencial: as clínicas que você atende têm tetos separados, mesmo saindo do mesmo servidor. Todo 429 e 503 traz o header Retry-After e details.retryAfterSeconds, em segundos.

Reserva em aberto é a que espera pagamento ou a que, sem cobrança, ainda está pendente com o horário no futuro. Ela deixa de contar quando é paga, confirmada, cancelada ou vence.

Reservar depende de um serviço interno que, fora do ar, devolve 503 integration.unavailable. As leituras seguem respondendo. Refaça a reserva depois do Retry-After, com a mesma Idempotency-Key.

8. Erros

Todo erro sai no mesmo envelope:

{ "code": "validation_failed", "message": "...", "details": { "fields": [{ "field": "patient.document", "rules": ["isNotEmpty"] }] } }
  • code é estável e é por ele que o seu código decide. A message pode mudar.
  • details só vem quando há o que detalhar: fields (a validação), retryAfterSeconds (os tetos) e supportId (o erro interno, para o atendimento achar o caso).
  • Campo que a API não conhece no corpo ou na query é recusado (validation_failed).
  • Corpo acima de 10 MB é recusado antes de chegar à API, com 413 sem corpo, fora deste envelope.
  • Os códigos genéricos por status são bad_request, unauthorized, not_found, conflict, payload_too_large, unprocessable, rate_limited, unavailable e internal_error. Os códigos de domínio estão nas seções acima.

9. O fluxo do chatbot

  1. GET /specialties: "Qual especialidade você procura?"
  2. GET /professionals?specialty=<nome>: lista quem atende, para a pessoa escolher.
  3. GET /procedures?professionalId=<id>: escolhe o atendimento, com preço e duração.
  4. GET /availability?professionalId=<id>&month=<YYYY-MM>: oferece os horários; use o time para falar e guarde o startsAt.
  5. Colete nome, e-mail, CPF, data de nascimento e telefone, e o sexo quando o profissional restringe os sexos atendidos. Se a pessoa disser que já é paciente, peça só o CPF e a data de nascimento e mande useExistingPatient: true.
  6. POST /bookings com uma Idempotency-Key nova.
  7. Com AWAITING_PAYMENT, envie o checkoutUrl e avise que o horário só fica garantido com o pagamento.
  8. GET /bookings/{id} para responder "minha consulta está confirmada?".

O modelo de IA nunca recebe a credencial: ele pede ao seu backend, e o backend chama a API.

10. Avisos por webhook

O CliniDeck avisa o seu servidor quando uma reserva que a sua credencial criou muda:

EventoQuando
booking.confirmeda reserva saiu de pendente para confirmada (paga, confirmada pela clínica ou em atendimento), ou a clínica reativou uma reserva cancelada
booking.canceleda reserva foi cancelada, venceu sem pagamento ou foi excluída
payment.confirmeda cobrança da reserva foi paga

O pagamento da reserva costuma gerar dois avisos: payment.confirmed e booking.confirmed. A criação pelo POST /bookings não gera aviso, porque a resposta já traz a situação.

Como ligar

  1. Passe ao CliniDeck a URL que recebe os avisos. É uma por integrador, para todas as clínicas que você atende. Ela precisa ser https://, num endereço público, sem usuário, senha nem #.
  2. A chave de assinatura (cdk_whsec_...) chega ao e-mail técnico por um link de visualização única, como o token. Ela só existe depois do termo assinado. Guarde-a no cofre de segredos do seu servidor.
  3. Os avisos começam a sair com a URL e a chave cadastradas.

O pedido

POST <sua URL>
Content-Type: application/json
User-Agent: CliniDeck-Webhooks/1.0
X-CliniDeck-Event-Id: 01970000-0000-7000-8000-000000000001
X-CliniDeck-Event-Type: booking.canceled
X-CliniDeck-Signature: t=1792771200,v1=5f2b...

{"id":"01970000-0000-7000-8000-000000000001","type":"booking.canceled","occurredAt":"2026-10-23T18:00:00.000Z","company":{"id":"..."},"integration":{"id":"...","login":"clinica-demo"},"data":{"booking":{"id":"...","status":"CANCELED"}}}
  • id é o do aviso. Ele se repete em toda tentativa do MESMO aviso: guarde os ids já processados e descarte a repetição.
  • company.id e integration.login dizem de qual clínica é o aviso. data.booking.id é o mesmo id do POST /bookings.
  • Nos eventos da reserva, data.booking.status é CONFIRMED ou CANCELED. No payment.confirmed, vem data.payment.status igual a PAID.
  • O aviso traz só ids e estados, nunca nome, documento, telefone ou dado de saúde. Para o resto, GET /bookings/{id}.
  • A ordem de chegada não é garantida: decida pelo occurredAt e, na dúvida, consulte a reserva.

Conferir a assinatura

O header é t=<instante do envio em segundos>,v1=<assinatura>, e pode trazer mais de um v1=. Cada v1 é o HMAC-SHA256, em hexadecimal, de <t>.<corpo>, com a chave como texto. Confira sobre os BYTES do corpo como chegaram, antes de interpretar o JSON: reserializar muda a ordem ou o espaçamento e a assinatura deixa de bater.

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidCliniDeckSignature(rawBody, header, signingKey, nowSeconds = Math.floor(Date.now() / 1000)) {
  const parts = header.split(',');
  const t = Number(parts.find((part) => part.startsWith('t='))?.slice(2));
  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;
  const expected = createHmac('sha256', signingKey).update(`${t}.${rawBody}`, 'utf8').digest();
  return parts
    .filter((part) => part.startsWith('v1='))
    .some((part) => {
      const received = Buffer.from(part.slice(3), 'hex');
      return received.length === expected.length && timingSafeEqual(received, expected);
    });
}
  • Aceite o pedido se QUALQUER v1= casar. Quando o CliniDeck gera uma chave nova, a anterior assina junto por 7 dias: troque a chave no seu servidor dentro desse prazo.
  • Recuse t com mais de 300 segundos de diferença do seu relógio: é a defesa contra a repetição de um pedido capturado.
  • Cada tentativa é assinada de novo, com o t daquele envio: a mesma repetição chega com outra assinatura.

Responder

  • Responda 2xx em até 10 segundos e processe depois, numa fila sua. Qualquer outra resposta, o redirecionamento (3xx), o tempo esgotado e a falha de conexão contam como falha da tentativa.
  • A falha volta a ser tentada em 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h e 12 h: são 10 tentativas em cerca de 26 horas. Aviso com mais de 72 horas não é mais enviado; reconcilie pelo GET /bookings/{id}.
  • Desligamento: 20 avisos seguidos que esgotam as 10 tentativas desligam o webhook, e o e-mail técnico recebe o aviso. Uma tentativa que falha não conta sozinha: o seu deploy ou uma queda de algumas horas não desliga nada. Um aviso entregue zera a conta. Enquanto desligado, os avisos não saem e são descartados: religar não reenvia o que ficou para trás. Corrija o recebedor, peça ao CliniDeck para religar e reconcilie pelo GET /bookings/{id}.
  • A clínica que revoga a sua integração para de gerar avisos, e os pendentes dela são descartados.

11. Fora da v1

  • Consultar, cancelar ou remarcar reservas de uma paciente: isso é da área do paciente.
  • Avisos de outros eventos além dos três da seção 10.
  • Paciente estrangeiro sem CPF.
  • Mais de um horário por reserva.