API do LaurinhIA — v2.0

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.

Base URL
https://laurinhia.com/api/v2
MétodoEndpointDescrição
GET/pacientesListar pacientes (paginado, filtro por status)
POST/pacientesCriar novo paciente
GET/pacientes/{id}Buscar paciente por ID
PATCH/pacientes/{id}Atualizar paciente
DELETE/pacientes/{id}Anonimizar (LGPD soft-delete)
GET/agendamentosListar agendamentos (filtro por data/status)
POST/agendamentosCriar 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.

Exemplo cURL
curl -X GET "https://laurinhia.com/api/v2/pacientes" \
  -H "X-API-Key: sk_live_sua_chave_aqui"
CódigoSignificado
401 MISSING_API_KEYHeader X-API-Key ausente
401 INVALID_API_KEYKey inválida ou não encontrada
401 API_KEY_REVOKEDKey foi revogada
401 API_KEY_EXPIREDKey expirada
403 INSUFFICIENT_PERMISSIONSKey não tem a permissão para esta operação
429 RATE_LIMIT_EXCEEDEDMais de 100 req/min com a mesma key

Permissões disponíveis

pacientes:read

GET em /pacientes e /pacientes/{id}

pacientes:write

POST, PATCH, DELETE em /pacientes

agendamentos:read

GET em /agendamentos

agendamentos:write

POST, PATCH, DELETE em /agendamentos

Pacientes

GET/api/v2/pacientes

Parâmetros de query opcionais:

  • status — captacao | agendado | em_tratamento | finalizado | opt_out
  • page — 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 } }
POST/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
}
LGPD: Campos de saúde nunca são retornados. O DELETE executa anonimização (LGPD Art. 18, VI) — mantém o registro para integridade referencial, substituindo dados pessoais por valores anônimos.

Agendamentos

GET/api/v2/agendamentos

Parâmetros de query opcionais:

  • status — pendente | confirmado | cancelado | realizado | no_show | reagendado
  • data_inicio — ISO 8601 (ex: 2026-04-01T00:00:00Z)
  • data_fim — ISO 8601
  • paciente_id — UUID do paciente
  • dentista_id — UUID do dentista
  • page / limit — paginação
POST/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
}
Exemplo: Buscar agendamentos de amanhã
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. 1

    Acesse Configurações → Webhooks

    No painel do LaurinhIA, vá em Configurações e clique na aba "Webhooks de Saída".

  2. 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. 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. 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. 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

HeaderDescrição
Content-Type
application/json
Sempre JSON.
X-Webhook-Signature
sha256=<hex>
Assinatura HMAC-SHA256 do body. Verifique antes de processar.
X-Webhook-Event
lead.created
Nome do evento que gerou esta entrega.
X-Webhook-Timestamp
2026-03-25T14:00:00.000Z
Timestamp ISO-8601 da entrega.
X-Webhook-Delivery
d4e5f6a7-b8c9-4123-8ef0-123456789012
ID estável desta entrega. Use-o junto com o id do payload para deduplicar retries.
X-Webhook-Attempt
1
Número da tentativa HTTP, de 1 a 6.
User-Agent
LaurinhIA-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.

Imediato
+1 minuto
+5 minutos
+30 minutos
+2 horas
+12 horas

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.created

Disparado 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.updated

Disparado 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.booked

Disparado 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.canceled

Disparado 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.sent

Disparado 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.received

Disparado 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.

Node.js
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);
  },
);
Python
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
<?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.