Developer reference

Webhook API

vaktimo pushes your business events — new appointment, cancellation, new customer, message status, conversation handoff — to your own system (CRM/ERP, n8n/Zapier, a custom endpoint) as outbound webhooks. This page is the reference for building a receiver correctly and securely.

Scope: this is the outbound webhook vaktimo sends to you. Inbound webhooks that vaktimo receives (WhatsApp, Polar) are separate and out of scope here.

1. Setup

In your dashboard go to Settings → Integrations → Webhooks:

  1. Add webhook — give it a name, the destination URL, and select the events you want to receive.
  2. On save the signing secret is shown once — copy and store it securely (it is never shown again). Lost it? Use Rotate secret to generate a new one.
  3. The Test button sends a sample webhook.test event to your endpoint so you can confirm connectivity and signature verification before going live.

Destination URL rules (SSRF protection):

  • Only public http/https addresses are accepted.
  • localhost, 127.0.0.1, private ranges (10.x, 172.16–31.x, 192.168.x), CGNAT (100.64/10), link-local / cloud metadata (169.254.169.254), *.internal and *.local are rejected. To test locally, use a public tunnel (e.g. ngrok, webhook.site).
  • Redirects (3xx) are followed up to 3 hops, and each hop is re-validated.

2. Request format

Each delivery is an HTTP POST to your URL.

Headers

HeaderValue
Content-Typeapplication/json
X-Webhook-EventEvent name (e.g. appointment.created)
X-Webhook-SignatureHMAC-SHA256 of the body (hex) — see §3
User-Agentvaktimo-webhook/1.0

Body (envelope)

{
  "event": "appointment.created",
  "timestamp": "2026-06-26T10:15:30.000Z",
  "data": { /* event-specific — see §4 */ }
}
  • event — the event name (same as X-Webhook-Event).
  • timestamp — when the delivery was generated, ISO-8601 (UTC).
  • data — event-specific fields.

3. Verifying signatures (required)

To confirm a request really came from vaktimo, always verify the signature. It is the HMAC-SHA256 of the raw request body (the bytes — not re-serialized JSON) using your webhook secret, hex-encoded, sent in the X-Webhook-Signature header.

Node.js (Express)

import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.VAKTIMO_WEBHOOK_SECRET;

// IMPORTANT: keep the raw body (signature is over raw bytes).
app.post("/vaktimo-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("X-Webhook-Signature") ?? "";
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");

  // Constant-time comparison (timing-attack safe).
  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!ok) return res.status(401).send("invalid signature");

  const payload = JSON.parse(req.body.toString("utf8"));
  // ... handle payload.event ...
  res.sendStatus(200); // 2xx = success
});

Do not recompute the signature after JSON.parse + JSON.stringify — key ordering / whitespace differences will break it. Always use the raw body.

4. Event catalog

Subscribe a webhook to * for all events, or pick only the ones you need. The data fields per event:

Appointment events

appointment.created — new appointment (panel, online page, or WhatsApp bot):

{
  "appointmentId": "665f...",
  "customerId": "665a...",
  "serviceId": "6650...",
  "addonServiceIds": ["6651..."],
  "staffId": "664f...",        // null if no staff assigned
  "startAt": "2026-06-27T11:00:00.000Z",
  "endAt": "2026-06-27T11:30:00.000Z",
  "price": 350,
  "source": "whatsapp_bot"     // panel | public_booking | whatsapp_bot | import
}

appointment.updated — appointment changed / rescheduled:

{
  "appointmentId": "665f...",
  "customerId": "665a...",
  "serviceId": "6650...",      // may be absent on panel status-only edits
  "staffId": "664f...",        // may be null
  "startAt": "2026-06-27T14:00:00.000Z",
  "endAt": "2026-06-27T14:30:00.000Z"
}

appointment.confirmed, appointment.completed, appointment.cancelled — status transitions. Status-driven deliveries carry a compact body:

{
  "appointmentId": "665f...",
  "customerId": "665a...",
  "status": "completed",       // confirmed | completed | cancelled
  "startAt": "2026-06-27T11:00:00.000Z"
}

Note (important): the data shape of appointment.cancelled and appointment.updated varies by source — online-page/bot cancellations include service+staff fields; panel status changes send the compact { status } body. On the consumer side, always match on appointmentId and treat other fields as optional.

Customer events

customer.created — new customer (panel, online booking, or first WhatsApp contact):

{ "customerId": "665a...", "name": "Ayşe Y.", "phone": "+90555..." }

customer.anonymized — GDPR/KVKK anonymization. No PII (delete your own record):

{ "customerId": "665a..." }

Message events

message.sent, message.delivered, message.read, message.failed — status updates for an outbound WhatsApp message:

{
  "messageJobId": "6661...",
  "customerId": "665a...",
  "providerMessageId": "BAE5...",
  "status": "delivered"        // sent | delivered | read | failed
}

Conversation events

conversation.handoff — a chat was handed from the bot to a human:

{
  "conversationId": "6670...",
  "customerId": "665a...",     // null for anonymous / unmatched-number chats
  "reason": "keyword: şikayet" // e.g. auto_handoff_timeout, manual_takeover, bot_requested
}

Test event

webhook.test — sent when you press Test in the panel (not a real event):

{ "message": "Bu bir test gönderimidir (vaktimo).", "webhookId": "667a..." }

5. Delivery & retries

SuccessHTTP 2xx response
Timeout10 seconds
RetryNetwork error / timeout / 5xx → up to 3 attempts (short incremental backoff)
4xxTreated as a client error — not retried
Auto-disableAfter 15 consecutive failures the webhook is disabled (re-enable from the panel once your endpoint is healthy)
HistoryEvery attempt is shown under History (status code, duration, error)

Guarantees & recommendations

  • Delivery is at-least-once; the same event may arrive again (during a retry). Be idempotent — de-duplicate using IDs like appointmentId / messageJobId.
  • Order is not guaranteed. Use timestamp to reason about ordering.
  • Return 2xx quickly; queue heavy work and process it asynchronously (a response over 10s times out and is retried).
  • If signature verification fails, do not return 2xx (return 401/403) — otherwise you would have processed a forged request as success.

6. Secret management

  • The secret is shown once, only at creation and rotation; vaktimo stores it encrypted and never shows it again.
  • On suspected leak, use Rotate secret — the URL and delivery history are kept, only the signing secret changes. The old secret is invalid immediately; update the secret stored in your receiving system right away.