GuruAgenda API
menu Guias

Webhooks#

Um webhook avisa o seu sistema na hora em que algo acontece no GuruAgenda: um agendamento novo, um cancelamento, um sinal pago, uma mensagem no WhatsApp. Em vez de consultar a API de tempos em tempos, você informa uma URL e escolhe os eventos; a cada evento, enviamos um POST para ela.

Há três formatos de entrega:

Formato Para quê
generic (padrão) n8n, Make, Zapier ou o seu próprio sistema. Recebe o evento completo em JSON, assinado.
slack, discord, google_chat, teams Uma mensagem pronta no canal do chat. Veja Chats.
custom Um JSON (ou formulário) no formato que o destino espera, montado por um template. Veja Template livre.

O dono da conta cria webhooks em Configurações → API e Webhooks, ou por uma chave com o escopo webhooks:manage (POST /v1/webhooks; veja a referência).

O evento#

Cada entrega leva um evento:

{
  "id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
  "type": "appointment.rescheduled",
  "api_version": "2026-10-01",
  "created_at": "2026-10-13T18:22:05-03:00",
  "organization_id": 12,
  "livemode": true,
  "data": {
    "object": { "object": "appointment", "id": 90321, "start": "2026-10-21T10:00:00-03:00", "…": "…" },
    "changes": { "start": { "from": "2026-10-20T14:30:00-03:00", "to": "2026-10-21T10:00:00-03:00" } },
    "batch_id": null
  },
  "context": {
    "actor": { "type": "api_key", "id": "ga_live_a1b2c3d4", "name": "n8n" },
    "source": "api",
    "request_id": "req_01JABCDEF0123456789XYZ"
  }
}
  • data.object é o objeto completo, igual ao que a API devolve (GET /v1/appointments/{id}, por exemplo). Você não precisa de uma segunda chamada para saber o nome do cliente ou o horário.
  • data.changes diz o que mudou, com o valor de antes e o de depois (null quando não se aplica, como na criação).
  • context.actor diz quem causou o evento: user (alguém da equipe), client (o cliente, pela página de agendamento), api_key, agent (o assistente de IA) ou system (uma rotina automática).
  • id é único por evento. Use-o para descartar repetições (veja Entrega e novas tentativas).
  • api_version é a versão do formato do evento, fixada na assinatura do webhook.

Os mesmos eventos ficam 30 dias em GET /v1/events, para quem prefere consultar.

Eventos#

Ao assinar, você pode usar curingas por grupo: appointment.*, queue.*, deposit.* e assim por diante.

Evento Quando
appointment.created Agendamento criado, por qualquer canal.
appointment.updated Mudou algo sem evento próprio: observações, serviços, preço, metadata.
appointment.rescheduled Mudou a data, a hora ou o profissional. changes traz o valor anterior.
appointment.confirmed Agendamento confirmado.
appointment.cancelled Cancelado pelo cliente, pela equipe ou pelo sistema.
appointment.completed Atendimento concluído.
appointment.no_show O cliente faltou.
appointment.deleted Agendamento excluído. data.object traz só id e deleted: true.
appointment.reminder_sent Lembrete enviado ao cliente (e-mail ou WhatsApp).
client.created Cliente novo na carteira.
client.updated Dados do cliente ou metadata mudaram.
client.first_appointment Primeiro agendamento do cliente na organização.
queue.entry.created Cliente entrou na Fila Online.
queue.entry.called Cliente chamado.
queue.entry.in_service Atendimento em andamento.
queue.entry.completed Atendimento da fila concluído.
queue.entry.skipped Cliente pulado.
queue.entry.no_show Chamado e não apareceu no prazo.
queue.entry.cancelled Entrada cancelada.
deposit.created Sinal cobrado (PIX gerado).
deposit.paid Sinal pago.
deposit.expired Sinal não pago no prazo.
deposit.refunded Sinal estornado.
service.created Serviço criado.
service.updated Serviço alterado.
service.deactivated Serviço desativado ou excluído.
professional.created Profissional entrou na equipe.
professional.deactivated Profissional saiu da equipe.
blocked_time.created Bloqueio de agenda criado.
blocked_time.deleted Bloqueio de agenda removido.
whatsapp.message.received Mensagem recebida no WhatsApp próprio da organização. Traz o cliente, quando o telefone é de alguém da carteira.
whatsapp.message.sent Mensagem automática enviada pelo GuruAgenda (confirmação, lembrete…).
whatsapp.message.failed Mensagem automática que falhou.
whatsapp.connection.updated O WhatsApp próprio conectou ou desconectou.

Além desses, o botão Testar (ou POST /v1/webhooks/{id}/test) envia na hora um evento ping e mostra a resposta do seu endpoint. O ping só sai pelo teste: não dá para assiná-lo.

A lista cresce com o tempo, sem aviso de quebra: ignore tipos de evento que você não conhece.

Filtros#

Para receber só parte dos eventos, a assinatura aceita filters. Valores na mesma lista são alternativas (OU); listas diferentes precisam valer juntas (E). Lista vazia não filtra.

Filtro Exemplo de uso
professional_ids Só os agendamentos de uma profissional.
service_ids Só um serviço.
statuses Só agendamentos e entradas da fila que ficaram num status (depois do evento).
types Só TELEMEDICINE ou só IN_PERSON.
sources Em appointment.created, só o que veio de um canal: public_booking (página de agendamento), dashboard (equipe), recurrence, api ou agent.

Um filtro que não se aplica ao evento (por exemplo, service_ids num whatsapp.message.received) não bloqueia a entrega.

Headers de cada entrega#

POST /seu/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: GuruAgenda-Webhooks/1.0 (+https://api.guruagenda.com.br/docs/webhooks)
X-GuruAgenda-Event: appointment.created
X-GuruAgenda-Event-Id: evt_01JAB2C3D4E5F6G7H8J9K0MNPQ
X-GuruAgenda-Delivery-Id: dlv_01JAB3C4D5E6F7G8H9J0K1MNPQ
X-GuruAgenda-Webhook-Id: whk_01JAB1C2D3E4F5G6H7J8K9MNPQ
X-GuruAgenda-Timestamp: 1792183859
X-GuruAgenda-Signature: t=1792183859,v1=44f18d12cf…
webhook-id: evt_01JAB2C3D4E5F6G7H8J9K0MNPQ
webhook-timestamp: 1792183859
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

Também enviamos os headers que você cadastrar na assinatura (headers, até 10). É assim que se usa o Header Auth do nó Webhook do n8n: cadastre, por exemplo, X-Webhook-Token com o mesmo valor da credencial.

Verificar a assinatura#

Toda entrega generic e custom é assinada com o segredo do webhook (whsec_…). Ele aparece uma vez, na criação e na rotação; você também pode informar o seu próprio segredo (16 a 128 caracteres, sem espaços) ao criar o webhook. Verificar é opcional, mas é o que garante que o pedido veio do GuruAgenda e não foi alterado.

Há dois esquemas no mesmo pedido. Use um deles.

X-GuruAgenda-Signature: HMAC-SHA256, em hexadecimal, de "{t}.{corpo}", usando o segredo inteiro (com o whsec_) como chave. t é o horário da assinatura (segundos desde 1970).

  1. Leia o corpo cru, byte a byte, antes de qualquer parse de JSON. Corpo reformatado não bate.
  2. Separe t e v1 do header.
  3. Recuse se t estiver a mais de 5 minutos do horário atual (proteção contra repetição).
  4. Calcule o HMAC e compare com v1 em tempo constante.
  5. Depois de girar o segredo, por 24 horas o header também traz v0, assinado com o segredo anterior. Aceite v1 ou v0 para trocar o segredo sem perder entregas.

webhook-signature: segue a especificação Standard Webhooks, com bibliotecas prontas em várias linguagens (pip install standardwebhooks, npm install standardwebhooks). Funciona com o segredo gerado pelo GuruAgenda; se você informou o seu próprio segredo e ele não for base64, use o X-GuruAgenda-Signature.

Python#

import hashlib
import hmac
import time


def verify_guruagenda(secret, signature_header, raw_body, tolerance=300):
    """True se o corpo (bytes) veio do GuruAgenda com este segredo."""
    parts = dict(item.split("=", 1) for item in signature_header.split(",") if "=" in item)
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, parts.get(key, "")) for key in ("v1", "v0"))

Com Flask, o corpo cru é request.get_data():

@app.post("/hooks/guruagenda")
def guruagenda_hook():
    if not verify_guruagenda(SECRET, request.headers.get("X-GuruAgenda-Signature", ""), request.get_data()):
        return "", 400
    event = request.get_json()
    # … trate o evento; responda rápido
    return "", 200

Node.js#

const crypto = require("crypto");

function verifyGuruAgenda(secret, signatureHeader, rawBody, toleranceSeconds = 300) {
  const parts = {};
  for (const item of signatureHeader.split(",")) {
    const i = item.indexOf("=");
    if (i > 0) parts[item.slice(0, i)] = item.slice(i + 1);
  }
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  return ["v1", "v0"].some((key) =>
    typeof parts[key] === "string" && parts[key].length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts[key]), Buffer.from(expected)));
}

Com Express, peça o corpo cru com express.raw:

app.post("/hooks/guruagenda", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyGuruAgenda(process.env.GURUAGENDA_WEBHOOK_SECRET, req.get("X-GuruAgenda-Signature") || "", req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // … trate o evento; responda rápido
  res.sendStatus(200);
});

n8n#

Veja o nó Code pronto em n8n, Make e Zapier.

Entrega e novas tentativas#

  • Sucesso é qualquer resposta 2xx em até 10 segundos (5 para conectar). O corpo da resposta é ignorado. Responda logo e processe depois, se o trabalho for demorado.
  • Falha é tempo esgotado, erro de rede, redirecionamento (3xx, que não seguimos), 4xx ou 5xx.
  • Novas tentativas depois de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h (com uma pequena variação aleatória): 8 tentativas em cerca de 45 horas. Em 429, respeitamos o Retry-After (até 1 hora).
  • 410 Gone desativa o webhook na hora. Use-o para dizer "não mande mais".
  • Desativação automática: 7 dias seguidos só com falhas desativam o webhook, e o dono da conta recebe um e-mail. Reativar é um clique (ou PATCH /v1/webhooks/{id} com active: true).
  • Ordem não é garantida, e um mesmo evento pode chegar mais de uma vez (numa nova tentativa ou num reenvio manual). Descarte o id repetido e, para saber o estado mais recente, compare created_at ou confie no data.object, que sempre traz o objeto inteiro.

Cada tentativa fica registrada por 30 dias, com o código de resposta, o tempo e o começo da resposta: em Configurações → API e Webhooks ou em GET /v1/webhooks/{id}/deliveries. Uma entrega pode ser reenviada por POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver.

Segurança do destino#

  • A URL precisa ser https, num domínio público. Recusamos IPs internos, endereços privados e redirecionamentos.
  • Nunca enviamos anotações clínicas, fichas ou prontuários por webhook.
  • Para chats, enviamos os dados do cliente reduzidos por padrão (veja Chats).