Developer Docs
Integre seu sistema ao CRM Odonto via REST API v2 (CRUD completo) ou receba eventos em tempo real via Webhooks de Saída com HMAC-SHA256.
REST API v2
CRUD completo para pacientes e agendamentos. Ideal para sincronizar seu sistema ERP, criar automações via Zapier/Make, ou integrar com sistemas próprios. Requer plano Pro ou Enterprise.
https://laurinhia.com/api/v2
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /pacientes | Listar pacientes (paginado, filtro por status) |
| POST | /pacientes | Criar novo paciente |
| GET | /pacientes/{id} | Buscar paciente por ID |
| PATCH | /pacientes/{id} | Atualizar paciente |
| DELETE | /pacientes/{id} | Anonimizar (LGPD soft-delete) |
| GET | /agendamentos | Listar agendamentos (filtro por data/status) |
| POST | /agendamentos | Criar novo agendamento |
| GET | /agendamentos/{id} | Buscar agendamento por ID |
| PATCH | /agendamentos/{id} | Atualizar agendamento |
| DELETE | /agendamentos/{id} | Cancelar agendamento |
Autenticação com API Key
Todas as rotas /api/v2/* requerem uma API Key no header X-API-Key. As keys são gerenciadas em Configurações → REST API.
curl -X GET "https://laurinhia.com/api/v2/pacientes" \ -H "X-API-Key: sk_live_sua_chave_aqui"
| Código | Significado |
|---|---|
401 MISSING_API_KEY | Header X-API-Key ausente |
401 INVALID_API_KEY | Key inválida ou não encontrada |
401 API_KEY_REVOKED | Key foi revogada |
401 API_KEY_EXPIRED | Key expirada |
403 INSUFFICIENT_PERMISSIONS | Key não tem a permissão para esta operação |
429 RATE_LIMIT_EXCEEDED | Mais de 100 req/min com a mesma key |
Permissões disponíveis
pacientes:readGET em /pacientes e /pacientes/{id}
pacientes:writePOST, PATCH, DELETE em /pacientes
agendamentos:readGET em /agendamentos
agendamentos:writePOST, PATCH, DELETE em /agendamentos
Pacientes
/api/v2/pacientesParâmetros de query opcionais:
status— captacao | agendado | em_tratamento | finalizado | opt_outpage— página (default: 1)limit— itens por página (default: 20, max: 100)
Resposta 200
{ "data": [...], "meta": { "page": 1, "limit": 20, "total": 47, "total_pages": 3 } }/api/v2/pacientes{
"telefone": "5573999990001", // obrigatório
"nome": "Maria Silva", // opcional
"canal_origem": "formulario_web", // opcional
"consentimento_lgpd": true, // obrigatório por LGPD
"consentimento_whatsapp": true // opcional
}Agendamentos
/api/v2/agendamentosParâmetros de query opcionais:
status— pendente | confirmado | cancelado | realizado | no_show | reagendadodata_inicio— ISO 8601 (ex: 2026-04-01T00:00:00Z)data_fim— ISO 8601paciente_id— UUID do pacientedentista_id— UUID do dentistapage/limit— paginação
/api/v2/agendamentos{
"data_agendamento": "2026-04-15T09:00:00-03:00", // obrigatório (ISO 8601 com timezone)
"paciente_id": "uuid-do-paciente", // opcional
"dentista_id": "uuid-do-dentista", // opcional
"data_hora_fim": "2026-04-15T10:00:00-03:00", // opcional
"procedimento": "Limpeza", // opcional
"observacoes": "Paciente com histórico de ansiedade" // opcional
}curl "https://laurinhia.com/api/v2/agendamentos?data_inicio=2026-04-15T00:00:00Z&data_fim=2026-04-15T23:59:59Z&status=confirmado" \ -H "X-API-Key: sk_live_sua_chave_aqui"
Webhooks de Saída
Como começar
- 1
Acesse Configurações → Webhooks
No painel do LaurinhIA, vá em Configurações e clique na aba "Webhooks de Saída".
- 2
Crie um webhook
Informe a URL do seu endpoint HTTPS, escolha os eventos que deseja receber e clique em Criar. Um secret HMAC-SHA256 será gerado automaticamente.
- 3
Salve o secret
Copie o secret imediatamente após a criação — ele é exibido apenas uma vez. Armazene-o como variável de ambiente no seu servidor.
- 4
Verifique a assinatura
A cada requisição recebida, verifique o header X-Webhook-Signature usando o secret salvo. Rejeite requisições sem assinatura válida.
- 5
Teste o webhook
Use o botão "Testar" no painel para enviar um evento de teste e confirmar que o endpoint responde com HTTP 2xx.
Headers da Requisição
| Header | Descrição |
|---|---|
Content-Typeapplication/json | Sempre JSON. |
X-Webhook-Signaturesha256=<hex> | Assinatura HMAC-SHA256 do body. Verifique antes de processar. |
X-Webhook-Eventlead.created | Nome do evento que gerou esta entrega. |
X-Webhook-Timestamp2026-03-25T14:00:00.000Z | Timestamp ISO-8601 da entrega. |
X-Webhook-Deliveryd4e5f6a7-b8c9-4123-8ef0-123456789012 | ID estável desta entrega. Use-o junto com o id do payload para deduplicar retries. |
X-Webhook-Attempt1 | Número da tentativa HTTP, de 1 a 6. |
User-AgentLaurinhIA-Webhook/1.0 | Identificação do sistema de origem. |
Política de Retry
Timeout/rede, HTTP 408, 425, 429 e 5xx recebem retry. Redirects e demais 4xx são falhas terminais. Cada chamada tem timeout de 10 segundos; o agendador coleta itens vencidos aproximadamente uma vez por minuto.
O painel em Configurações → Webhooks → Logs de entrega mostra o estado atual e o número de tentativas de cada entrega, não um registro separado por tentativa. Para eventos registrados no outbox, a entrega é at-least-once: um crash após o POST pode produzir retry com o mesmo corpo e ID. Se a última tentativa terminar sem resultado de rede conhecido, o estado é marcado como incerto para revisão, sem uma nova chamada.
Eventos
Cada webhook pode assinar um ou mais eventos abaixo. O campo event no payload sempre identifica o tipo.
lead.createdDisparado quando um novo paciente é captado (via WhatsApp, formulário ou import).
LGPD: Nome e telefone não incluídos por padrão (LGPD). Use o paciente_id para consultar via API.
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000001",
"event": "lead.created",
"timestamp": "2026-03-25T14:00:00.000Z",
"data": {
"paciente_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "captacao",
"canal_origem": "whatsapp",
"criado_em": "2026-03-25T14:00:00.000Z"
}
}patient.updatedDisparado quando o status de um paciente é alterado no Kanban.
LGPD: Apenas IDs e status são incluídos no payload.
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000002",
"event": "patient.updated",
"timestamp": "2026-03-25T14:05:00.000Z",
"data": {
"paciente_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status_anterior": "captacao",
"status_novo": "agendado",
"atualizado_em": "2026-03-25T14:05:00.000Z"
}
}appointment.bookedDisparado quando um agendamento é criado ou confirmado.
LGPD: Payload minimizado: não inclui nome, telefone ou observações do paciente; o procedimento é limitado a 255 caracteres.
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000003",
"event": "appointment.booked",
"timestamp": "2026-03-25T14:10:00.000Z",
"data": {
"agendamento_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"paciente_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"data_agendamento": "2026-03-28T09:00:00.000Z",
"procedimento": "Limpeza",
"status": "pendente",
"criado_em": "2026-03-25T14:10:00.000Z"
}
}appointment.canceledDisparado quando um agendamento é cancelado ou marcado como no-show.
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000004",
"event": "appointment.canceled",
"timestamp": "2026-03-25T14:15:00.000Z",
"data": {
"agendamento_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"paciente_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status_anterior": "confirmado",
"status_novo": "cancelado",
"atualizado_em": "2026-03-25T14:15:00.000Z"
}
}campaign.sentDisparado após cada lote real que aumenta o total enviado da campanha.
LGPD: IDs de pacientes individuais nunca incluídos no payload da campanha.
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000005",
"event": "campaign.sent",
"timestamp": "2026-03-25T14:20:00.000Z",
"data": {
"campanha_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"tipo": "repescagem",
"total_enviados": 47,
"criado_em": "2026-03-25T14:20:00.000Z"
}
}nps.receivedDisparado quando um paciente responde à pesquisa NPS pós-consulta.
LGPD: Comentário do paciente não incluído por padrão (LGPD).
Payload de exemplo
{
"id": "e1000000-0000-4000-8000-000000000006",
"event": "nps.received",
"timestamp": "2026-03-25T14:25:00.000Z",
"data": {
"paciente_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nota": 9,
"respondido_em": "2026-03-25T14:25:00.000Z"
}
}Verificando a Assinatura (HMAC-SHA256)
Sempre verifique o header X-Webhook-Signature antes de processar o payload. A assinatura é calculada como sha256=HMAC(secret, body_bytes). Use comparação segura (timing-safe) para evitar ataques de timing.
const crypto = require("crypto");
const express = require("express");
const app = express();
function verificarWebhook(bodyBytes, assinaturaRecebida) {
// Use os bytes exatos recebidos. Recriar o JSON altera a assinatura.
if (!Buffer.isBuffer(bodyBytes)) return false;
const assinaturaEsperada = "sha256=" + crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(bodyBytes)
.digest("hex");
// Comparação segura contra timing attacks
const recvBuf = Buffer.from(assinaturaRecebida ?? "", "ascii");
const expBuf = Buffer.from(assinaturaEsperada, "ascii");
if (recvBuf.length !== expBuf.length) return false;
return crypto.timingSafeEqual(recvBuf, expBuf);
}
app.post(
"/webhooks/laurinhia",
express.raw({ type: "application/json" }),
(req, res) => {
const assinatura = req.headers["x-webhook-signature"];
if (!verificarWebhook(req.body, assinatura)) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString("utf8"));
// Persista/deduplicate evento.id antes de processar em background.
return res.sendStatus(200);
},
);import hmac, hashlib
def verificar_webhook(body_bytes: bytes, assinatura_recebida: str, secret: str) -> bool:
"""Verifica assinatura HMAC-SHA256 do webhook."""
assinatura_esperada = "sha256=" + hmac.new(
secret.encode("utf-8"),
body_bytes,
hashlib.sha256
).hexdigest()
# Comparação segura contra timing attacks
return hmac.compare_digest(assinatura_recebida, assinatura_esperada)
# Em um handler FastAPI:
from fastapi import Request, HTTPException
@app.post("/meu-webhook")
async def handle_webhook(request: Request):
body = await request.body()
sig = request.headers.get("x-webhook-signature", "")
if not verificar_webhook(body, sig, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Assinatura inválida")
payload = await request.json()
# processar...<?php
function verificarWebhook(string $payload, string $assinaturaRecebida, string $secret): bool {
$assinaturaEsperada = "sha256=" . hash_hmac("sha256", $payload, $secret);
// Comparação segura contra timing attacks
return hash_equals($assinaturaEsperada, $assinaturaRecebida);
}
// Em um controller Laravel / endpoint puro:
$payload = file_get_contents("php://input");
$assinatura = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? "";
if (!verificarWebhook($payload, $assinatura, getenv("WEBHOOK_SECRET"))) {
http_response_code(401);
die("Assinatura inválida");
}
$dados = json_decode($payload, true);
// processar...Configuração do webhook
Crie, edite, teste ou desative endpoints somente em Configurações → Webhooks, usando uma sessão administrativa. A API REST v2 usa API keys para pacientes e agendamentos; ela não expõe administração programática de webhooks. Copie o secret na criação: ele é exibido uma única vez.
Boas Práticas
- ✓Responda com HTTP 200 o mais rápido possível. Processe o evento em background para evitar timeout.
- ✓Implemente idempotência: o mesmo evento pode ser entregue mais de uma vez. Deduplicate pelo campo id no topo do payload; X-Webhook-Delivery identifica a entrega por endpoint.
- ✓Valide sempre a assinatura HMAC antes de processar. Rejeite com 401 se inválida.
- ✓Armazene o secret como variável de ambiente — nunca no código-fonte.
- ✓Monitore os logs de entrega no painel para identificar falhas rapidamente.
- ✓Use HTTPS com certificado válido. Endpoints HTTP não são aceitos.