API Guide
Webhooks
Register endpoints, verify signed deliveries, rotate secrets, and inspect the delivery log.
Webhooks push events to your server instead of making you poll. Endpoints are managed through the API with a key holding webhooks:manage — plus the read scope of each event family you subscribe to (e.g. job.* events require jobs:read, customer.* requires customers:read). Subscribing without the matching read scope is rejected with 403 missing_read_scope, so a webhooks-only key can't stand up a data feed it couldn't read directly.
Events
customer.created customer.updated customer.deleted
lead.created lead.updated lead.deleted
estimate.created estimate.updated
job.created job.updated
payment.recorded
ping is reserved for the test route and cannot be subscribed to.
Register an endpoint
curl -X POST https://api.moveitpro.com/v1/webhook-endpoints \
-H "Authorization: Bearer mip_live_..." \
-H "Idempotency-Key: …" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/moveitpro",
"events": ["job.created", "job.updated"],
"description": "Ops sync"
}'
The signing secret (mip_whsec_…) is returned once, on creation. Store it immediately — it cannot be read back later, only rotated.
Verify every delivery
Deliveries are HTTP POSTs carrying these headers:
X-MIP-Signature: t=1786000000,v1=9a3f…
X-MIP-Event: job.created
X-MIP-Endpoint-Id:
X-MIP-Delivery-Id:
User-Agent: MoveitPro-Webhooks/v1
The timestamp is inside X-MIP-Signature, not a separate header. To verify: parse t and v1 out of the header, recompute HMAC-SHA-256 over t + "." + rawBody with your endpoint secret, compare against v1 in constant time, and reject anything whose t is more than ~5 minutes from your clock.
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2))
);
const { t, v1 } = parts;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(t + "." + rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verify against the raw body bytes. Parsing to JSON and re-serializing before verification will break the comparison. If you run multiple endpoints, select the secret by X-MIP-Endpoint-Id.
Endpoint contract
- Respond
2xxquickly — acknowledge first, process asynchronously. - Expect redelivery. Deduplicate on the event id in the payload; a delivery may arrive more than once.
- Do not assume ordering between events.
- Deliveries always carry the record's latest state at delivery time, not the state at the moment the event occurred.
Retries and auto-disable
A failed delivery retries on a backoff ladder for roughly 24 hours before being abandoned. An endpoint that keeps failing is automatically disabled and your admins are notified. Re-enable it via PATCH /v1/webhook-endpoints/{id} once the receiver is fixed — there is a short cooldown after an auto-disable (409 reenable_too_soon); use the test route to prove the receiver works first.
Test, rotate, inspect
POST /v1/webhook-endpoints/{id}/testsends a signedping. This route is rate limited more tightly than normal routes (6/minute, 200/day).POST /v1/webhook-endpoints/{id}/rotate-secretissues a new secret.GET /v1/webhook-deliveries?endpoint_id={id}returns that endpoint's recent delivery attempts with status (up to 100; the log is retained for 30 days) — the first place to look when a listener goes quiet.DELETE /v1/webhook-endpoints/{id}soft-deletes an endpoint. Captures and deliveries stop immediately; the delivery log is retained for its normal window.