Overview
Knock is a door-to-door canvassing platform: GPS-verified knocks, AI conversation coaching, storm targeting, and appointment dispatch. This page is for the person wiring it to the rest of the company's stack — JobNimbus, AccuLynx, ServiceTitan, Salesforce, or anything with a webhook or an HTTP client. No SDK is required; everything below is plain HTTPS + JSON.
There are two directions, and you can use either or both:
- Outbound webhook — Knock POSTs an event to your URL the moment a rep sets an appointment, marks a door sold, or logs a deal outcome. Optionally HMAC-signed. Works out of the box with Zapier and Make.
- REST API — your system pushes leads/appointments into Knock (they appear on the crew's live map and in a closer's dispatch list) and pulls knocks & outcomes back out on a poll, with cursor pagination.
Base URL: https://doorknock.onrender.com (or your own deployment's
origin). All timestamps are ISO-8601 to the second, in the server's local
time, with no timezone suffix.
Quick start
- A Knock manager opens Report → Integrations and taps
Generate key. The key (
dk_live_…) is shown once — store it in your secrets manager, not in a spreadsheet. - Prove the wiring:
curl -H "Authorization: Bearer dk_live_YOUR_KEY" \ https://doorknock.onrender.com/api/v1/ping{"ok": true, "company": "Your Roofing Co", "company_id": "…"} - For outbound events, paste your receiver's URL into the same panel and press Send test event. The panel shows your endpoint's actual HTTP answer, and keeps a log of the last 20 deliveries.
Authentication
Every REST call carries the company API key, either way:
Authorization: Bearer dk_live_… # preferred
X-Api-Key: dk_live_… # for platforms that only offer a header field
- The key scopes every call to its own company. There is no way to read or write another company's data with it.
- A missing, wrong, or revoked key returns
401with a JSON error. - Managers can revoke or regenerate the key at any time in Report → Integrations. Generating a new key immediately invalidates the old one. Knock stores only a digest of the key, so it can never be shown again — losing it means generating a new one.
REST — push a lead in
POST /api/v1/leads creates a pin on the crew's map. Use it to send
your CRM's fresh leads or booked appointments to the field.
| Field | Type | Notes |
|---|---|---|
lat, lng | number, required | Knock is a map; a door needs coordinates. Geocode on your side if your CRM stores only addresses. |
address | string | Street address shown to the rep. Strongly recommended. |
disposition | string | new_lead (default) or appointment_set. |
contact | object | {name, phone, email, notes}. Flat top-level
name/phone/email/notes are also accepted. |
assigned_to | string | Rep name or email on the team. The rep gets a push
notification and the door appears in their dispatch list. 400 if no rep matches. |
callback | string | YYYY-MM-DDTHH:MM — appointment / follow-up time.
appointment_at is an accepted alias. |
external_id | string ≤80 | Your CRM's record id. Idempotency key: pushing the
same external_id again returns the existing pin with "created": false instead of a duplicate. |
source | string ≤40 | Label shown as the pin's origin (default "CRM"). |
curl -X POST https://doorknock.onrender.com/api/v1/leads \
-H "Authorization: Bearer dk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "JN-4482",
"source": "JobNimbus",
"disposition": "appointment_set",
"address": "412 Birch Ln, Franklin, TN 37064",
"lat": 35.9251, "lng": -86.8689,
"contact": {"name": "Dana Holt", "phone": "615-555-0142",
"notes": "Hail claim opened 8/12, adjuster Friday"},
"assigned_to": "weston@yourco.com",
"callback": "2026-08-24T14:00"
}'
Answers 201 with {"ok": true, "created": true, "pin": {…}} —
the pin object is the same shape the pull API returns (below).
/api/v1/outcomes and the outcome_logged webhook. Knock never
echoes a pushed lead back out of the webhook, so you will not receive your
own push as an event.REST — pull knocks & outcomes out
GET /api/v1/pins — every pin for your company, oldest-first.
| Param | Notes |
|---|---|
since | ISO timestamp; only pins whose updated_at ≥ since. Inclusive. |
cursor | Opaque value from the previous page's next_cursor. Exclusive — no row is served twice. |
limit | Page size, default 100, max 500. |
disposition | Comma list filter, e.g. appointment_set,sold. |
Response: {"pins": […], "count": n, "has_more": bool, "next_cursor": "…"}
(next_cursor only when has_more). A sync loop is:
first call with since = last run's start time, then follow
next_cursor until has_more is false.
GET /api/v1/outcomes — same pagination, but only pins carrying a
logged deal outcome, and since walks the outcome clock, not the
knock clock: an outcome logged today on a June door reaches today's poll.
The pin object
| Field | Notes |
|---|---|
pin_id | Knock's stable id for the door. |
disposition | not_home, do_not_knock, come_back_later, new_lead, appointment_set, sold. |
address, lat, lng | Where the door is. |
canvasser, canvasser_email | Who knocked it. Email is the stable join key when a manager has set one; join on it first, fall back to the name. |
assigned_to, assigned_to_email | The closer it was dispatched to, if any. |
timestamp | When the door was knocked (not when it synced). |
updated_at | Last change (knock, dispatch, ack, or outcome) — the field since compares against on /pins. |
homeowner | {name, phone, email}. |
notes, callback | Rep notes; scheduled follow-up time. |
outcome | {status, amount, by, at} once a deal outcome is logged, else null. status is signed or lost. |
distance_m | GPS-verified distance from rep to door at the knock — dated, located proof of presence. |
has_photo | Whether a doorstep photo exists. |
unit | The apartment or suite when the knock was at one door inside a building, e.g. 4B; empty otherwise. address already ends in , Unit 4B, so a closer is sent to the right door either way. |
coach | AI conversation coaching for the knock, when present — objection tags and coaching summary no other system captures. |
external_id | Your id, echoed back, when the pin came from /api/v1/leads. |
source | doorknock (knocked), api (you pushed it), or import (CSV import). |
Outbound webhook
Paste your endpoint's URL in Report → Integrations. Knock POSTs JSON to it on these events:
| Event | Fires when |
|---|---|
appointment_set | A rep books an appointment at the door. |
sold | A rep marks a door sold. |
outcome_logged | A deal outcome (signed / lost, with amount) is logged on an appointment or sold door. |
test | The manager presses "Send test event". Carries "test": true and a fixed pin id — do not persist it. |
{
"event": "appointment_set",
"company": "Your Roofing Co",
"sent_at": "2026-08-21T14:03:22",
"data": {
"pin_id": "9f27ac41d3b8",
"disposition": "appointment_set",
"address": "412 Birch Ln, Franklin, TN 37064",
"lat": 35.9251, "lng": -86.8689,
"canvasser": "Sam Ortiz", "canvasser_email": "sam@yourco.com",
"assigned_to": "Weston E.", "assigned_to_email": "weston@yourco.com",
"timestamp": "2026-08-21T14:02:51",
"homeowner": {"name": "Dana Holt", "phone": "615-555-0142", "email": null},
"notes": "Hail claim opened 8/12", "callback": "2026-08-24T14:00",
"outcome": null, "distance_m": 8.4, "has_photo": true
}
}
The data object is identical in shape to the pull API's pin object
(minus the sync bookkeeping fields), so one parser serves both. A
coach field appears when AI conversation coaching ran on the knock.
Answer any 2xx within a few seconds; the body of a non-2xx answer is read and
shown to the manager, so say why you rejected a payload.
Delivery & retries
- Events are delivered asynchronously — a slow receiver never slows a rep in the field.
- On a network failure or a
5xxanswer, Knock retries up to 3 more times with exponential backoff (about 5s, 25s, 125s). A4xxis treated as a final refusal and is not retried. - The last 20 deliveries — status, attempts, and your endpoint's reply — are visible to managers in Report → Integrations.
- Deliveries can arrive out of order and, in rare retry races, more than
once. Use
data.pin_id(+event) to deduplicate. - If a doorstep photo exists, Knock also POSTs the JPEG to
…/events/doorknock/<pin_id>/photoafter the event, for receivers shaped like the Cedar House platform CRM. Answer 404 if you don't want photos; it is quiet.
Verifying signatures (recommended)
Generate a signing secret (whsec_…) in Report → Integrations —
shown once, like the key. From then on every event carries:
X-DoorKnock-Timestamp: 1787582602 # unix seconds
X-DoorKnock-Signature: sha256=hex(HMAC_SHA256(secret, "{timestamp}.{raw_body}"))
Recompute and compare with a constant-time equality; reject if the timestamp is older than ~5 minutes to block replays.
# Python
import hmac, hashlib
def verify(secret, ts, raw_body, header_sig):
expect = "sha256=" + hmac.new(secret.encode(),
f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expect, header_sig)
// Node
const crypto = require("crypto");
function verify(secret, ts, rawBody, headerSig) {
const expect = "sha256=" + crypto.createHmac("sha256", secret)
.update(`${ts}.`).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expect), Buffer.from(headerSig));
}
MCP — let Claude or ChatGPT work the map
Knock is also a Model Context Protocol server. Point Claude Code, Claude
Desktop or ChatGPT at POST /mcp with the same API key and the assistant
can read today's knocks, search doors, list callbacks, check hail history and closer
slots, and write: log a knock, set a callback, change an outcome, add a note, put an
address on the do-not-knock list, watch an address for hail, or say something to the
team. Booking outcomes (appointment set, sold) stay in the app. The key acts as the
team's first manager, and every deed is filed under that name.
# Claude Code
claude mcp add --transport http knock https://doorknock.onrender.com/mcp \
--header "Authorization: Bearer dk_live_YOUR_KEY"
# Claude Desktop (claude_desktop_config.json), through the bundled bridge
{"mcpServers": {"knock": {"command": "python3", "args": ["/path/to/tools/mcp_stdio.py"],
"env": {"KNOCK_MCP_URL": "https://doorknock.onrender.com/mcp", "KNOCK_API_KEY": "dk_live_YOUR_KEY"}}}}
# Prove it
curl -s https://doorknock.onrender.com/mcp -H "Authorization: Bearer dk_live_YOUR_KEY" \
-H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
ChatGPT: Settings → Connectors → Create, URL https://doorknock.onrender.com/mcp,
header Authorization: Bearer dk_live_YOUR_KEY (Developer mode).
Recipes
Zapier — Knock events into any CRM
- New Zap → trigger Webhooks by Zapier → Catch Hook. Copy the hook URL.
- Paste it into Report → Integrations → CRM webhook URL, press Send test event so Zapier has a sample to map from.
- Add your CRM's action (JobNimbus "Create Contact/Job", AccuLynx's own
Zapier app with its Create Lead action, ServiceTitan via API middleware).
AccuLynx gives out no lead webhook URL of its own, so Zapier is its door: the
AccuLynx app connects with an API key made in AccuLynx under Account Settings,
Add-On Features and Integrations, API Keys. (Webhooks by Zapier needs a paid
Zapier plan.) Map
data.address,data.homeowner.*,data.canvasser_email,data.callback. - Optionally filter by
eventso appointments and sales create different things.
Zapier — CRM leads onto the Knock map
- Trigger: your CRM's "new lead/job" event.
- Action: Webhooks by Zapier → Custom Request — POST to
/api/v1/leads, headerAuthorization: Bearer dk_live_…, JSON body per the table above. Use your CRM record id asexternal_idso retries never duplicate doors.
Make (Integromat)
Outbound: a Custom webhook module receives events (paste its URL into
the Integrations panel). Inbound: an HTTP → Make a request module POSTs to
/api/v1/leads with the Bearer header.
Direct / middleware
For ServiceTitan or a homegrown stack, run a small middleware: receive
events at your URL (verify the signature), write into your system's API, and
poll /api/v1/outcomes?since=… nightly to reconcile anything a webhook
missed. Ping is GET /api/v1/ping; a monitoring check on it costs one line.
Field notes
- All endpoints are HTTPS on the app's own origin; there is no separate API host to allowlist.
- Keep the API key server-side. Never embed it in a browser page or a mobile client.
- Webhook receiver URLs must resolve to public hosts — private and link-local targets are refused by design.
- Page size caps at 500; a full historical backfill is a
since=2000-01-01T00:00:00walk with cursors. - Questions or a connector you wish existed: tell the team that sold you Knock — the integration surface grows with buyers' stacks.