All help articles

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 Integrations

The 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/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

Any new booking: from your booking link, by staff on the dashboard, or through the API or MCP. Pending requests fire it too.

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

Verify the signature

Why this matters
Without a signature check, 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 endpoint in the Custom webhook section, paste your URL, and choose the events and locations to receive. Every event except appointment.updated starts checked.
  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: 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 updatedAt on 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.

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