All help articles

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 Integrations

The 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/json
X-SimpleDock-Event-Typeappointment.created
X-SimpleDock-Event-Idevt_2tQ9pX8Lm6Nz
X-SimpleDock-Signaturet=1747156323,v1=8a91c…

The payload

Click any event type to see the JSON body your endpoint will receive.

appointment.created

A carrier books a new appointment via your booking link.

json
{
  "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"
    }
  }
}
Do this first

Verify the signature

Why this matters
A recent r/netsec study found 1,542 of 6,000 webhook-receiving apps don't verify signatures. Anyone who learns your URL can post fake events. Verifying takes 6 lines of code.

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.

webhooks/simpledock.ts
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.

No match. Reject the request as untrusted.

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:

Send test event

Fires a webhook.test envelope to your endpoint.

Request
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…
Response
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

  1. 1
    Stand up an HTTPS endpoint
    Any URL that accepts POST with a JSON body. We require HTTPS in production.
  2. 2
    Add the endpoint in SimpleDock
    In Settings → Integrations, click Add Custom, paste your URL, choose the events and locations to receive.
  3. 3
    Copy the signing secret
    Click the eye icon next to the endpoint to reveal the secret. Store it as SIMPLEDOCK_SIGNING_SECRET in your environment.
  4. 4
    Verify before you trust
    Drop in the snippet for your language above. Reject any request whose signature doesn't match.
  5. 5
    Send a test event and ship
    Click 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 updatedAt on 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.

shell
export SIMPLEDOCK_SIGNING_SECRET="whsec_..."   # new value