Knock. — Integration Guide

app

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:

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

  1. 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.
  2. 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": "…"}
  3. 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

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.

FieldTypeNotes
lat, lngnumber, required Knock is a map; a door needs coordinates. Geocode on your side if your CRM stores only addresses.
addressstringStreet address shown to the rep. Strongly recommended.
dispositionstringnew_lead (default) or appointment_set.
contactobject{name, phone, email, notes}. Flat top-level name/phone/email/notes are also accepted.
assigned_tostringRep 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.
callbackstringYYYY-MM-DDTHH:MM — appointment / follow-up time. appointment_at is an accepted alias.
external_idstring ≤80Your CRM's record id. Idempotency key: pushing the same external_id again returns the existing pin with "created": false instead of a duplicate.
sourcestring ≤40Label 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).

Pushed leads are deliberately excluded from doors-knocked stats (nobody knocked them) but can take outcomes — so the deal a closer signs off a pushed appointment flows back to you through /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.

ParamNotes
sinceISO timestamp; only pins whose updated_at ≥ since. Inclusive.
cursorOpaque value from the previous page's next_cursor. Exclusive — no row is served twice.
limitPage size, default 100, max 500.
dispositionComma 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

FieldNotes
pin_idKnock's stable id for the door.
dispositionnot_home, do_not_knock, come_back_later, new_lead, appointment_set, sold.
address, lat, lngWhere the door is.
canvasser, canvasser_emailWho 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_emailThe closer it was dispatched to, if any.
timestampWhen the door was knocked (not when it synced).
updated_atLast change (knock, dispatch, ack, or outcome) — the field since compares against on /pins.
homeowner{name, phone, email}.
notes, callbackRep notes; scheduled follow-up time.
outcome{status, amount, by, at} once a deal outcome is logged, else null. status is signed or lost.
distance_mGPS-verified distance from rep to door at the knock — dated, located proof of presence.
has_photoWhether a doorstep photo exists.
unitThe 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.
coachAI conversation coaching for the knock, when present — objection tags and coaching summary no other system captures.
external_idYour id, echoed back, when the pin came from /api/v1/leads.
sourcedoorknock (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:

EventFires when
appointment_setA rep books an appointment at the door.
soldA rep marks a door sold.
outcome_loggedA deal outcome (signed / lost, with amount) is logged on an appointment or sold door.
testThe 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

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));
}
Unsigned events (no secret generated) carry no signature headers. The optional photo POST and the coverage batch are not signed in this version — verify the JSON event webhook, which is where the business data is.

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

  1. New Zap → trigger Webhooks by Zapier → Catch Hook. Copy the hook URL.
  2. Paste it into Report → Integrations → CRM webhook URL, press Send test event so Zapier has a sample to map from.
  3. 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.
  4. Optionally filter by event so appointments and sales create different things.

Zapier — CRM leads onto the Knock map

  1. Trigger: your CRM's "new lead/job" event.
  2. Action: Webhooks by Zapier → Custom Request — POST to /api/v1/leads, header Authorization: Bearer dk_live_…, JSON body per the table above. Use your CRM record id as external_id so 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