Digital Front Desk can tell your own systems what happens at your front desk as it happens: a visitor checking in, a call being summarised, an invoice being paid. Add an https address in Settings, under "Webhooks and Zapier", and we POST a small signed JSON message to it for each event you choose. Or make an API key there and connect through Zapier.
Every message is one JSON object, version 1:
{
"id": "evt_3f9a0c...", // unique per event; the same on every retry
"type": "visitor.checked_in",
"version": 1,
"created_at": "2026-10-09T14:03:11Z",
"tenant": "your-account", // your account's short name
"data": { ... } // the fields listed for each event below
}
Headers on every request:
| Header | Meaning |
|---|---|
X-DFD-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> — see verifying. |
X-DFD-Event-Id | The event's id. Use it to ignore a message you already processed. |
X-DFD-Event-Type | The event's type. |
X-DFD-Delivery-Id | This delivery, for when you write to us about one. |
Privacy by default. We never send a visitor's
reason for visiting or their answers to your questions, call transcripts or
recordings, the answers to your forms, notes, health details or payment card
data. A visitor's name is included only if your arrival alerts may name
visitors and you keep visitor details (Kiosk settings →
Notifications and Privacy); otherwise visitor_name is null, and
your Webhooks settings page says which of the two is keeping it out. Call summaries are sent only
to an address you tick "Include call summaries" for. Fields we add later are
additive: ignore what you do not recognise.
An event is offered only while the part of the product it comes from is
switched on for your account. webhook.test is sent by the "Send
test event" button and carries only a message.
visitor.checked_in — Somebody finished check-in on your lobby screen.
Field in data | Meaning |
|---|---|
visit_id | Our id for this visit. |
status | checked_in, checked_out or cancelled. |
checked_in_at | When they checked in (ISO 8601, UTC). |
checked_out_at | When they left, or null. |
badge_number | The badge number printed or shown, if any. |
host_staff_id | The staff member they asked for, or null. |
host_name | That staff member's name as it appears in your staff list. |
visitor_name | Only if your arrival alerts may name visitors and you keep visitor details; otherwise null. |
visitor.checked_out — A visit was closed at the desk, from a staff member's phone link, or by a desk sweep. Visits closed automatically overnight do not fire it.
Field in data | Meaning |
|---|---|
visit_id | Our id for this visit. |
status | checked_in, checked_out or cancelled. |
checked_in_at | When they checked in (ISO 8601, UTC). |
checked_out_at | When they left, or null. |
badge_number | The badge number printed or shown, if any. |
host_staff_id | The staff member they asked for, or null. |
host_name | That staff member's name as it appears in your staff list. |
visitor_name | Only if your arrival alerts may name visitors and you keep visitor details; otherwise null. |
checked_out_by | desk, phone (a staff member's link) or sweep. |
visitor.claimed — A member of staff pressed "I'll take this" on an arrival.
Field in data | Meaning |
|---|---|
visit_id | Our id for this visit. |
status | checked_in, checked_out or cancelled. |
checked_in_at | When they checked in (ISO 8601, UTC). |
checked_out_at | When they left, or null. |
badge_number | The badge number printed or shown, if any. |
host_staff_id | The staff member they asked for, or null. |
host_name | That staff member's name as it appears in your staff list. |
visitor_name | Only if your arrival alerts may name visitors and you keep visitor details; otherwise null. |
claimed_at | When they claimed it. |
claimed_by_staff_id | Who claimed it, or null for a login with no staff row. |
claimed_by_name | Their name as your staff list shows it. |
staff.signed_in — A member of staff signed in at the door.
Field in data | Meaning |
|---|---|
presence_id | Our id for this sign-in. |
staff_id | The staff member. |
staff_name | Their name as your staff list shows it. |
signed_in_at | When. |
source | How: phone or dashboard. |
staff.signed_out — A member of staff signed out, or the desk signed them out.
Field in data | Meaning |
|---|---|
presence_id | Our id for this sign-in. |
staff_id | The staff member. |
staff_name | Their name as your staff list shows it. |
signed_in_at | When they signed in. |
signed_out_at | When they left. |
source | How it ended: phone or dashboard. |
call.completed — A phone call ended and its summary is ready. No transcript, ever.
Field in data | Meaning |
|---|---|
call_id | Our id for this call. |
direction | inbound or outbound. |
status | How it ended, e.g. completed, voicemail, transferred. |
from_number | The caller's number. |
to_number | The number they rang. |
started_at | When it started. |
ended_at | When it ended. |
duration_seconds | Length in seconds. |
summary | The AI summary — ONLY if you ticked "Include call summaries" for this address; otherwise absent. |
contact.created — A new client record was created — by a call, a check-in with consent, a form or a person. Spreadsheet imports do not fire it.
Field in data | Meaning |
|---|---|
contact_id | Our id for the client. |
name | Their name. |
email | Their email, if known. |
phone | Their phone, if known. |
company | Their company, if known. |
source | Where they came from: call, visit, form, manual ... |
created_at | When. |
appointment.booked — An appointment or booking request was made here (not one mirrored from Google Calendar).
Field in data | Meaning |
|---|---|
appointment_id | Our id. |
contact_id | The client, or null. |
starts_at | When it starts (UTC). |
minutes | How long. |
status | requested or confirmed. |
assigned_to | Who it is with, as typed. |
title | The appointment's title. |
appointment.cancelled — An appointment was cancelled. The reason is not sent.
Field in data | Meaning |
|---|---|
appointment_id | Our id. |
contact_id | The client, or null. |
starts_at | When it was to start (UTC). |
minutes | How long. |
status | Always cancelled. |
assigned_to | Who it was with. |
title | The appointment's title. |
form.submitted — Somebody submitted one of your forms. The answers are NOT sent — open the submission in Digital Front Desk.
Field in data | Meaning |
|---|---|
submission_id | Our id. |
form_id | Which form. |
form_name | The form's name. |
source | public, link or kiosk. |
submitted_at | When. |
quote.accepted — Your customer accepted a quote.
Field in data | Meaning |
|---|---|
quote_id | Our id. |
number | The quote number. |
total_cents | Total in cents. |
currency | e.g. CAD. |
deal_id | The job it belongs to. |
contact_id | The client. |
accepted_at | When. |
accepted_via | link or staff. |
invoice.sent — An invoice was issued (numbered and made a link).
Field in data | Meaning |
|---|---|
invoice_id | Our id. |
number | The invoice number. |
total_cents | Total in cents. |
paid_cents | Paid so far. |
currency | e.g. CAD. |
issue_date | Issue date. |
due_date | Due date. |
deal_id | The job. |
contact_id | The client. |
sent_at | When. |
invoice.paid — The balance reached zero. Card details are never sent.
Field in data | Meaning |
|---|---|
invoice_id | Our id. |
number | The invoice number. |
total_cents | Total in cents. |
paid_cents | Paid so far. |
currency | e.g. CAD. |
issue_date | Issue date. |
due_date | Due date. |
deal_id | The job. |
contact_id | The client. |
paid_at | When. |
Each address has its own signing secret (whsec_…), shown once
when you add it. The signature is HMAC-SHA256, keyed with the secret, over the
timestamp, a full stop, and the exact request body. Reject a message whose
signature does not match, or whose timestamp is more than
5 minutes from your clock — that window is what stops
somebody replaying a message they captured.
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
fields = dict(part.split("=", 1) for part in header.split(","))
timestamp = int(fields["t"])
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields.get("v1", ""))
# Flask: verify(SECRET, request.headers["X-DFD-Signature"], request.get_data())
const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300) {
const fields = Object.fromEntries(header.split(",").map(p => p.split("=", 2)));
const timestamp = Number(fields.t);
if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${timestamp}.`).update(rawBody).digest("hex");
const given = Buffer.from(fields.v1 || "", "hex");
const want = Buffer.from(expected, "hex");
return given.length === want.length && crypto.timingSafeEqual(given, want);
}
// Express: app.post("/dfd", express.raw({ type: "application/json" }), (req, res) => {
// if (!verify(SECRET, req.get("X-DFD-Signature"), req.body)) return res.sendStatus(400);
// res.sendStatus(200);
// });
Verify against the raw bytes you received, before parsing the JSON — re-serialising changes the bytes and the signature with them.
id to ignore repeats, and created_at to order them.Make an API key in Settings, under "Webhooks and Zapier", and send it as
X-API-Key: dfd_k_… (or Authorization: Bearer dfd_k_…).
A key belongs to one account and is shown once; revoking it also removes every
subscription it made.
| Call | What it does |
|---|---|
GET /api/v1/me | Checks the key; returns your account's name and the events available to it. |
POST /api/v1/hooks | Body {"event": "visitor.checked_in", "target_url": "https://…"}. Subscribes the address to one event; returns its id. Messages are the same signed messages described above. |
DELETE /api/v1/hooks/{id} | Unsubscribes. |
GET /api/v1/hooks | The subscriptions this key made. |
GET /api/v1/events/{type}/samples | Up to three recent messages of that event (an example if there are none yet) — what Zapier shows while you build a Zap. |
Requests are limited to 60 a minute per address.