ManageSync API

Send leads and work into ManageSync from your own software, and get signed webhooks back as things move. Base URL: https://app.managesync.com/api/v1

Authentication

Every request carries an API key in the Authorization header:

Authorization: Bearer msk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys come in two scopes. Platform keysare issued by ManageSync to partner programs — they submit estimate requests into the ManageSync team's triage queue. Organization keysbelong to a contractor or location on ManageSync and act on that organization's behalf; org admins mint them under Settings → Integrations. A key is shown once at creation and can be revoked at any time. Keys can carry an optional expiry picked at mint time — an expired key fails with a 401 exactly like a revoked one.

Errors & rate limits

Errors are JSON with a stable machine-readable code:

{ "error": { "code": "validation_failed", "message": "state: Select a state" } }

Codes: invalid_api_key (401), key_not_permitted / feature_disabled (403), not_found (404), invalid_json (400), validation_failed (422), rate_limited (429). Write endpoints allow 30 requests per 10 minutes per key by default — contact ManageSync if your integration needs more; limits are adjustable per key.

Create an estimate request

POST /api/v1/estimate-requests— submit a lead / project-estimate request. Platform keys land in the ManageSync triage queue; location keys submit on the org's behalf and may choose managementType: "unmanaged" to publish straight to the contractor marketplace.

FieldRequiredNotes
contactNameYesSite contact, max 200 chars
contactPhone / contactEmailYesAt least one of the two
addressLine1YesJob-site street address
addressLine2No
cityYes
stateYesTwo-letter US state code
zipYes
projectTypeNo"residential" (default) or "commercial"
tradeNoOptional trade classification
descriptionYes20–5000 chars
managementTypeNoLocation keys only: "managed_by_ms" (default) or "unmanaged"
curl -X POST https://app.managesync.com/api/v1/estimate-requests \
  -H "Authorization: Bearer msk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "contactName": "Jane Doe",
    "contactEmail": "jane@example.com",
    "addressLine1": "123 Main St",
    "city": "Baltimore",
    "state": "MD",
    "zip": "21201",
    "projectType": "commercial",
    "description": "Roof membrane inspection and repair estimate."
  }'

// 201
{ "id": "…", "status": "new", "managementType": "managed_by_ms", "createdAt": "…" }

Read estimate requests

GET /api/v1/estimate-requests— your requests, newest first. Platform keys list the requests they submitted; location keys list any of their organization's requests. Query params: status (new, in_review, open, closed), limit (max 100, default 25), offset.

{
  "estimateRequests": [
    { "id": "…", "status": "open", "managementType": "managed_by_ms",
      "projectType": "commercial", "trade": "roofing",
      "city": "Baltimore", "state": "MD", "createdAt": "…", "closedAt": null }
  ],
  "total": 12, "limit": 25, "offset": 0
}

GET /api/v1/estimate-requests/{id} — one request, plus a contractor-routing summary:

{
  "id": "…",
  "status": "open",            // new | in_review | open | closed
  "managementType": "managed_by_ms",
  "projectType": "commercial",
  "trade": "roofing",
  "city": "Baltimore",
  "state": "MD",
  "createdAt": "…",
  "closedAt": null,
  "contractors": { "sent": 3, "converted": 1, "declined": 1 }
}

Contractor request inbox

With a contractor key, the same endpoints read your organization's bid-request inbox — the estimate requests you were invited to or engaged from the marketplace. Each item carries the full site detail plus your organization's response state under target; other contractors' involvement is never included.

GET /api/v1/estimate-requests            // list, ?status=&limit=&offset=
GET /api/v1/estimate-requests/{id}       // one request you were invited to

{
  "id": "…",
  "status": "open",
  "projectType": "commercial",
  "trade": "roofing",
  "description": "…",
  "contactName": "…", "contactPhone": "…", "contactEmail": "…",
  "addressLine1": "…", "city": "Baltimore", "state": "MD", "zip": "…",
  "createdAt": "…", "closedAt": null,
  "target": {
    "status": "converted",     // sent | declined | converted
    "origin": "ms_sent",       // ms_sent | marketplace
    "sentAt": "…",
    "respondedAt": "…",
    "declinedReason": null,
    "bidPwoNumber": "PWO-1042" // the project spawned from this request
  }
}

List sites & services

GET /api/v1/sites— location keys only. Returns the organization's active sites and the services priced at each; use a site's id and a service's serviceRateId to create work orders.

{
  "sites": [
    {
      "id": "…", "name": "Store #12", "addressLine1": "…",
      "city": "…", "state": "…", "zip": "…",
      "services": [
        { "serviceRateId": "…", "name": "HVAC — rooftop unit", "rateType": "hourly",
          "standardRate": "95.00", "priorityRate": "120.00", "emergencyRate": "160.00" }
      ]
    }
  ]
}

Create a work order

POST /api/v1/work-orders— behavior depends on the key's organization type.

Location keyscreate a marketplace work order at one of the org's sites (contractors serving the area can accept it):

FieldRequiredNotes
siteIdYesFrom GET /api/v1/sites
serviceRateIdYesA service priced at that site
urgencyNo"standard" (default), "priority", "emergency"
descriptionYes20–2000 chars
propertyTypeNo"residential" or "commercial"
onSiteContactName / onSiteContactPhoneNoDefaults to the site's contact
scheduledForNoISO 8601 datetime

Contractor keyscreate a work order in the contractor's own pipeline (status unassigned) with free-text facility fields:

FieldRequiredNotes
descriptionYes1–2000 chars
urgency / propertyType / scheduledForNoAs above
facilityName, facilityAddressLine1/2, facilityCity, facilityState, facilityZipNoFree-text job-site details
onSiteContactName / onSiteContactEmail / onSiteContactPhoneNo
// 201
{ "id": "…", "woid": "WOID-1234", "status": "unassigned" }

Read work orders

GET /api/v1/work-orders— your organization's work orders, newest first (contractor keys: the org's assigned pipeline; location keys: the work orders the org submitted). Query params: status (e.g. unassigned, complete), limit (max 100, default 25), offset.

GET /api/v1/work-orders/{id} — one work order in the same shape.

{
  "id": "…", "woid": "WOID-1234",
  "status": "on_site",          // unassigned | assigned | en_route | on_site
  "isSuspended": false,         //   | complete | closed | canceled | revisions_requested
  "urgency": "priority",
  "propertyType": "commercial",
  "description": "…",
  "source": "api",
  "facility": { "name": "…", "addressLine1": "…", "city": "…", "state": "…", "zip": "…" },
  "scheduledFor": null,
  "firstArrivedAt": "…", "completedAt": null, "closedAt": null,
  "createdAt": "…"
}

Webhooks

Add a webhook URL to a key (in-app, next to the key) to receive POSTed events for estimate requests that key submitted. Saving a URL mints a signing secret (whsec_…), shown once.

Events:

  • request.status_changed { status, previousStatus }
  • request.routed — sent to contractors, { sentTo }
  • target.converted — a contractor took the request, { contractorName, origin, pwoNumber }
  • target.declined — an invited contractor declined, { contractorName, reason }
  • target.won — the project a contractor spawned from your request was approved / awarded, { contractorName, pwoNumber }
  • work_order.status_changed — a work order your key created changed status, { workOrderId, woid, status, previousStatus, corrected? }
POST <your url>
X-ManageSync-Event: target.converted
X-ManageSync-Signature: t=1783900000,v1=5f3a…

{
  "id": "<delivery id — dedupe on this>",
  "event": "target.converted",
  "createdAt": "…",
  "data": { "requestId": "…", "contractorName": "…", "origin": "invitation" }
}

Verify the signature by computing HMAC-SHA256 over `${t}.${rawBody}` with your secret and comparing to v1:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(signatureHeader, rawBody, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Respond with a 2xx quickly. Deliveries are at-least-once (dedupe on the delivery id); a failed delivery retries after ~15s and ~45s, then daily, up to 5 attempts total. Recent deliveries and their outcomes are visible in-app next to the key (the delivery-log icon).