Integrations · Custom webhooks
Build a custom webhook
Receive signed JSON events at your own URL. Same payload as Zapier and Slack — 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 10 seconds. We retry with exponential backoff for 24 hours on 5xx or timeout.
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.createdA carrier books a new appointment via your booking link.
{
"id": "evt_2tQ9pX8Lm6Nz",
"type": "appointment.created",
"createdAt": "2026-05-08T17:12:03.481Z",
"organizationId": "org_acme",
"locationId": "loc_main",
"data": {
"id": "appt_abc123def456",
"status": "APPROVED",
"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": "DRY",
"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 Custom, paste your URL, choose the events and locations to receive.
- 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: 5xx and timeouts retry with exponential backoff for up to 24 hours.
- 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-disable: 20+ consecutive 4xx responses disables the endpoint and shows a banner. Reactivate in Settings → Integrations once your fix is deployed.
Rotating the secret
Compromised? In Settings → Integrations, open the endpoint menu and click Rotate secret. The new secret is shown once. Update your env var and redeploy — there's no overlap window, so plan a quick redeploy.
export SIMPLEDOCK_SIGNING_SECRET="whsec_..." # new value