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 2xx quickly — 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}/test sends a signed ping. This route is rate limited more tightly than normal routes (6/minute, 200/day).
    • POST /v1/webhook-endpoints/{id}/rotate-secret issues 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.