Integrations · Custom webhooks
Build a custom webhook
Receive signed JSON events at your own URL. The same JSON envelope Zapier gets (Slack gets formatted messages instead), yours to route however you want. The whole spec fits on one page.
Open IntegrationsThe contract
SimpleDock POSTs a JSON envelope to your URL with three headers and a signature. Your endpoint should return any 2xx within 5 seconds. Timeouts, 429s, and 5xx responses are retried; any other non-2xx response is not.
Content-Typeapplication/jsonX-SimpleDock-Event-Typeappointment.createdX-SimpleDock-Event-Idevt_2tQ9pX8Lm6NzX-SimpleDock-Signaturet=1747156323,v1=8a91c…The payload
Click any event type to see the JSON body your endpoint will receive.
appointment.createdAny new booking: from your booking link, by staff on the dashboard, or through the API or MCP. Pending requests fire it too.
{
"id": "evt_2tQ9pX8Lm6Nz",
"type": "appointment.created",
"createdAt": "2026-05-08T17:12:03.481Z",
"organizationId": "org_acme",
"locationId": "loc_main",
"data": {
"id": "appt_abc123def456",
"status": "CONFIRMED",
"startTime": "2026-05-12T14:00:00.000Z",
"endTime": "2026-05-12T15:00:00.000Z",
"companyName": "Acme Freight",
"contactEmail": "[email protected]",
"contactPhone": "+1-555-234-1180",
"driverName": "Maria Lopez",
"driverPhone": "+1-555-901-2233",
"trailerNumber": "TRL-44781",
"poReference": "PO-2026-4471",
"notes": "Pallet jack ready",
"driverNotes": null,
"customFields": {},
"arrivalStatus": null,
"estimatedDelay": null,
"checkedInAt": null,
"completedAt": null,
"approvedAt": "2026-05-08T17:12:00.000Z",
"rejectedAt": null,
"cancelledAt": null,
"rejectionReason": null,
"createdAt": "2026-05-08T17:10:00.000Z",
"updatedAt": "2026-05-08T17:12:00.000Z",
"manageUrl": "https://simpledock.ai/manage/mt_4j2k...",
"door": {
"id": "door_4",
"name": "Door 4"
},
"appointmentType": {
"id": "atype_inb_dry",
"name": "Inbound dry van",
"loadType": "FTL",
"direction": "INBOUND",
"durationMinutes": 60
},
"location": {
"id": "loc_main",
"name": "Reno DC",
"slug": "reno-dc",
"timezone": "America/Los_Angeles",
"address": "1200 Industrial Way",
"city": "Reno",
"state": "NV",
"zip": "89502"
}
}
}Verify the signature
Compute HMAC-SHA256(t + "." + raw_body) with your signing secret and compare against the v1= portion of X-SimpleDock-Signature. Use a constant-time compare to avoid timing attacks. Reject if the timestamp is older than 5 minutes to prevent replay.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.SIMPLEDOCK_SIGNING_SECRET!;
const TOLERANCE_SECONDS = 5 * 60;
// Capture the raw body so the signature still matches after parsing.
app.use("/webhooks/simpledock", express.raw({ type: "application/json" }));
app.post("/webhooks/simpledock", (req, res) => {
const header = req.header("X-SimpleDock-Signature") ?? "";
const match = /^t=(\d+),v1=([a-f0-9]+)$/.exec(header);
if (!match) return res.status(400).send("bad signature header");
const [, t, v1] = match;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${req.body.toString("utf8")}`)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex"),
);
if (!valid) return res.status(401).send("invalid signature");
const skew = Math.abs(Date.now() / 1000 - Number(t));
if (skew > TOLERANCE_SECONDS) return res.status(400).send("timestamp out of range");
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event.type
res.json({ received: true });
});Try it live
Edit any field — the secret, timestamp, or body — and watch the signature recompute in your browser. This is the same HMAC SimpleDock uses on the server.
Verification runs in your browser using crypto.subtle. Edit any field to see how the signature changes.
Send a test event
From Settings → Integrations, click Send test on any endpoint. Your server should respond 200 OK:
Fires a webhook.test envelope to your endpoint.
POST /webhooks/simpledock HTTP/1.1 Host: your-app.example.com Content-Type: application/json X-SimpleDock-Event-Type: webhook.test X-SimpleDock-Event-Id: evt_t1Q9pX8Lm6 X-SimpleDock-Signature: t=1747156323,v1=8a91c…
HTTP/1.1 200 OK
content-type: application/json
content-length: 18
{"received":true}The test envelope has type: "webhook.test", so it's safe to ignore in your real handler — just return 200.
Set it up
- 1Stand up an HTTPS endpointAny URL that accepts
POSTwith a JSON body. We require HTTPS in production. - 2Add the endpoint in SimpleDockIn Settings → Integrations, click Add endpoint in the Custom webhook section, paste your URL, and choose the events and locations to receive. Every event except
appointment.updatedstarts checked. - 3Copy the signing secretClick the eye icon next to the endpoint to reveal the secret. Store it as
SIMPLEDOCK_SIGNING_SECRETin your environment. - 4Verify before you trustDrop in the snippet for your language above. Reject any request whose signature doesn't match.
- 5Send a test event and shipClick Send test, confirm a 2xx, and you're done.
Retries, idempotency, and ordering
- Retries: timeouts (5 seconds), 429s, and 5xx responses are retried, up to 5 attempts in total. The waits between attempts are 1 minute, 5 minutes, 30 minutes, then 2 hours. Other 4xx responses and redirects are not retried.
- Idempotency: each delivery has a stable
X-SimpleDock-Event-Id. Dedupe on it — retries reuse the same id. - Ordering: not guaranteed. Use
updatedAton the appointment to discard out-of-order updates. - Auto-pause: after 10 deliveries in a row fail (retries used up, or a response that isn't retried), the endpoint is marked Paused and stops receiving events. Once your fix is deployed, open More on the endpoint in Settings → Integrations and click Resume deliveries.
Rotating the secret
Compromised? In Settings → Integrations, open More on the endpoint and click Rotate signing secret. The old secret stops working immediately, so reveal the new one with the eye icon, update your env var, and redeploy quickly.
export SIMPLEDOCK_SIGNING_SECRET="whsec_..." # new value