{"openapi":"3.1.0","info":{"title":"MovingPro Documents API — Safeway Moving","version":"1.0.0","description":"Read-only access to jobs and the documents attached to them, plus a webhook that announces document changes.\n\n## Authentication\nSend your API key on every request as an `x-api-key` header. Keys are issued by MovingPro (a 26-character key tied to your company); keep it server-side and never embed it in a browser or mobile app. A missing, invalid or deactivated key returns **401**.\n\n## Rate limits\nSustained **~5 requests/second** with short bursts allowed. Over the limit the gateway answers **429** with body `{\"message\":\"Too Many Requests\"}` — back off and retry. Document downloads happen through the temporary direct link returned by the download endpoint and do **not** count against the limit.\n\n## Paging\n`GET /jobs` returns up to `limit` jobs (default 100, max 500) newest-first plus a `nextCursor`. Pass it back as `?cursor=` to get the next page; stop when `nextCursor` is `null`. A cursor is bound to the filters it was issued with — changing filters mid-walk returns **400 invalid_cursor**; restart the walk. Paging is stable while new jobs are being created.\n\n## Timestamps\nDate-times such as `createdAt` and `addedOn` are the CRM's own wall-clock time and carry **no time-zone suffix** (`2026-07-03T14:02:00`). Dates (`bookingDate`, `moveDate`) are `YYYY-MM-DD`. Webhook `timestamp` and download `expiresAt` are UTC (`Z`).\n\n## Errors\nEvery error body is `{ \"ok\": false, \"error\": \"<code>\", \"message\": \"<human readable>\" }` (except the gateway's 429 above)."},"servers":[{"url":"https://safewaymoving.movingpro.net/api"}],"security":[{"apiKey":[]}],"tags":[{"name":"Jobs","description":"Jobs (leads/orders) in the CRM."},{"name":"Documents","description":"Documents attached to a job."},{"name":"Meta","description":"Health and reference pages (no key required)."}],"paths":{"/health":{"get":{"tags":["Meta"],"summary":"Liveness probe","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"example":{"ok":true,"service":"safewaymovinginc-documents-api"}}}}}}},"/docs":{"get":{"tags":["Meta"],"summary":"This reference (HTML)","security":[],"responses":{"200":{"description":"HTML page"}}}},"/swagger":{"get":{"tags":["Meta"],"summary":"Interactive Swagger UI","security":[],"responses":{"200":{"description":"HTML page"}}}},"/openapi.json":{"get":{"tags":["Meta"],"summary":"This document as JSON","security":[],"responses":{"200":{"description":"OpenAPI document"}}}},"/jobs":{"get":{"tags":["Jobs"],"summary":"List jobs","description":"Newest-first, with optional filters and keyset paging. Unknown query parameters are rejected (400) so a typo never silently widens a backfill.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["LEAD","FOLLOW_UP","BOOKED","READY_FOR_PICKUP","PICKED_UP","ON_TRIP","IN_STORAGE","READY_FOR_DELIVERY","DELIVERED","CANCELLED","DEAD","DELETED"]},"description":"Folded lead + invoice status."},{"name":"orderType","in":"query","schema":{"type":"integer"},"description":"Order type id (see `orderType.id` on a job; e.g. 1 Local, 2 Long Distance, 5 Intrastate)."},{"name":"flag","in":"query","schema":{"type":"string","maxLength":200},"description":"Flag description exactly as shown in the CRM and in `flag` on a job (e.g. `6 - Visual Estimate`)."},{"name":"branchId","in":"query","schema":{"type":"integer"},"description":"Branch id (see `branch.id`)."},{"name":"createdFrom","in":"query","schema":{"type":"string","format":"date-time"},"description":"Job created on/after (`YYYY-MM-DD` or `YYYY-MM-DDTHH:mm[:ss]`, CRM local time)."},{"name":"createdTo","in":"query","schema":{"type":"string","format":"date-time"},"description":"Job created on/before. A date-only value covers the whole day."},{"name":"bookedFrom","in":"query","schema":{"type":"string","format":"date"}},{"name":"bookedTo","in":"query","schema":{"type":"string","format":"date"}},{"name":"moveFrom","in":"query","schema":{"type":"string","format":"date"}},{"name":"moveTo","in":"query","schema":{"type":"string","format":"date"}},{"name":"updatedFrom","in":"query","schema":{"type":"string","format":"date-time"},"description":"Last CRM activity on/after. Enables the (slower) `updatedAt` computation."},{"name":"updatedTo","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"pickupState","in":"query","schema":{"type":"string","minLength":2,"maxLength":2},"description":"Two-letter state of the first pickup stop."},{"name":"deliveryState","in":"query","schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"hasDocuments","in":"query","schema":{"type":"boolean"},"description":"Only jobs with (or without) at least one document."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque `nextCursor` from the previous page."}],"responses":{"200":{"description":"A page of jobs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobsPage"},"example":{"ok":true,"items":[{"jobId":20312,"referenceNumber":"SW2031200","status":"DELIVERED","orderType":{"id":2,"name":"Long Distance Move"},"flag":"6 - Visual Estimate","branch":{"id":9,"name":"Safeway Moving"},"createdAt":"2026-06-14T09:12:41","updatedAt":null,"bookingDate":"2026-06-15","moveDate":"2026-07-02","customer":{"firstName":"Jane","lastName":"Doe","email":"jane.doe@example.com","phone":"(555) 010-2000"},"pickup":{"city":"Hobart","state":"IN","zip":"46342"},"delivery":{"city":"Austin","state":"TX","zip":"73301"},"assignedTo":"Sam Rivera","documentCount":3}],"nextCursor":"eyJ2IjoxLCJjZCI6IjIwMjYtMDYtMTRUMDk6MTI6NDEuMDAwWiIsImlkIjoyMDMxMiwiZiI6IjNhOWY1YzBiIn0"}}}},"400":{"description":"Bad filter, cursor or limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"invalid_filter","message":"Unknown query parameter \"statuss\"."}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"unauthorized","message":"An x-api-key header is required."}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/jobs/{jobId}":{"get":{"tags":["Jobs"],"summary":"Get one job","parameters":[{"$ref":"#/components/parameters/jobId"}],"responses":{"200":{"description":"The job (with `updatedAt` populated)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"job":{"$ref":"#/components/schemas/Job"}}},"example":{"ok":true,"job":{"jobId":20312,"referenceNumber":"SW2031200","status":"DELIVERED","orderType":{"id":2,"name":"Long Distance Move"},"flag":"6 - Visual Estimate","branch":{"id":9,"name":"Safeway Moving"},"createdAt":"2026-06-14T09:12:41","updatedAt":"2026-07-03T15:40:02","bookingDate":"2026-06-15","moveDate":"2026-07-02","customer":{"firstName":"Jane","lastName":"Doe","email":"jane.doe@example.com","phone":"(555) 010-2000"},"pickup":{"city":"Hobart","state":"IN","zip":"46342"},"delivery":{"city":"Austin","state":"TX","zip":"73301"},"assignedTo":"Sam Rivera","documentCount":3}}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"unauthorized","message":"The API key is invalid or inactive."}}}},"404":{"description":"Unknown job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"not_found","message":"No job with id 999999."}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/jobs/{jobId}/documents":{"get":{"tags":["Documents"],"summary":"List documents on a job","description":"Every document attached to the job, newest first. Documents added on the invoice after booking are included; internal mirror copies are not, so each document appears once.","parameters":[{"$ref":"#/components/parameters/jobId"}],"responses":{"200":{"description":"Documents","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Document"}}}},"example":{"ok":true,"items":[{"documentId":79159,"name":"Bill of Lading - order 20312.pdf","sizeBytes":320786,"addedBy":"Office User","addedOn":"2026-07-03T14:02:00"}]}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"unauthorized","message":"The API key is invalid or inactive."}}}},"404":{"description":"Unknown job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"not_found","message":"No job with id 999999."}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/jobs/{jobId}/documents/{documentId}/download":{"get":{"tags":["Documents"],"summary":"Get a temporary direct download link","description":"Returns a pre-signed URL. Fetch the file from `url` with a plain GET (no API key) before `expiresAt` (10 minutes). The download itself does not count against the rate limit. Request a fresh link if it expires.","parameters":[{"$ref":"#/components/parameters/jobId"},{"$ref":"#/components/parameters/documentId"}],"responses":{"200":{"description":"Download link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadLink"},"example":{"ok":true,"documentId":79159,"name":"Bill of Lading - order 20312.pdf","sizeBytes":320786,"contentType":"application/pdf","url":"https://safewaymovinginc.s3.us-east-1.amazonaws.com/mobile-uploads/20312/0f3a…?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=600&…","expiresAt":"2026-07-03T14:12:00.000Z"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"unauthorized","message":"The API key is invalid or inactive."}}}},"404":{"description":"Unknown job or document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"not_found","message":"Document 1 was not found on job 20312."}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"description":"Stored where this API cannot serve it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"ok":false,"error":"storage_unsupported","message":"This document is stored in a location this API cannot serve."}}}}}}}},"webhooks":{"document.created":{"post":{"summary":"A document was added to a job","description":"Delivered as an HTTPS POST to the URL you register with MovingPro, within about a minute of the change in the CRM.\n\n**Verify every delivery.** Each request carries `X-MovingPro-Timestamp` (unix seconds) and `X-MovingPro-Signature` (`v1=<hex>`). Compute `HMAC-SHA256(secret, timestamp + \".\" + rawBody)` over the exact request bytes, hex-encode it, and compare (constant-time) with the value after `v1=`. Reject deliveries whose timestamp is more than 5 minutes off.\n\n**Respond 2xx quickly** (do the work asynchronously). A 4xx response is treated as a permanent reject and the event is dropped; 5xx or a timeout (10 s) is queued for a later retry. `eventId` is stable across retries — deduplicate on it. To fetch the file, call `GET /jobs/{jobId}/documents/{documentId}/download`. Renames are not announced.","parameters":[{"name":"X-MovingPro-Timestamp","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds when the request was signed."},{"name":"X-MovingPro-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`v1=` + hex HMAC-SHA256 of `timestamp + \".\" + body`."},{"name":"X-MovingPro-Event","in":"header","required":true,"schema":{"type":"string","enum":["document.created","document.deleted"]}},{"name":"X-MovingPro-Event-Id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentCreatedEvent"},"example":{"event":"document.created","eventId":"doc-79159-created","timestamp":"2026-07-03T14:02:31.512Z","data":{"jobId":20312,"document":{"documentId":79159,"name":"Bill of Lading - order 20312.pdf","sizeBytes":320786,"createdOn":"2026-07-03T14:02:00","createdByName":"Office User","createdById":14}}}}}},"responses":{"2xx":{"description":"Acknowledged. Any other status is treated as a failed delivery (4xx: dropped; 5xx/timeout: retried later)."}}}},"document.deleted":{"post":{"summary":"A document was removed from a job","parameters":[{"name":"X-MovingPro-Timestamp","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds when the request was signed."},{"name":"X-MovingPro-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`v1=` + hex HMAC-SHA256 of `timestamp + \".\" + body`."},{"name":"X-MovingPro-Event","in":"header","required":true,"schema":{"type":"string","enum":["document.created","document.deleted"]}},{"name":"X-MovingPro-Event-Id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentDeletedEvent"},"example":{"event":"document.deleted","eventId":"doc-79159-deleted","timestamp":"2026-07-03T15:40:02.004Z","data":{"jobId":20312,"documentId":79159,"name":"Bill of Lading - order 20312.pdf"}}}}},"responses":{"2xx":{"description":"Acknowledged."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Your API key, as issued by MovingPro."}},"parameters":{"jobId":{"name":"jobId","in":"path","required":true,"schema":{"type":"integer"},"description":"The `jobId` from the jobs list."},"documentId":{"name":"documentId","in":"path","required":true,"schema":{"type":"integer"},"description":"The `documentId` from the documents list (or a webhook)."}},"responses":{"TooManyRequests":{"description":"Rate limit exceeded (emitted by the gateway, not this API)","content":{"application/json":{"example":{"message":"Too Many Requests"}}}}},"schemas":{"Error":{"type":"object","required":["ok","error","message"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string"},"message":{"type":"string"}}},"Lookup":{"type":["object","null"],"properties":{"id":{"type":"integer"},"name":{"type":["string","null"]}}},"Job":{"type":"object","required":["jobId","status","createdAt","customer","pickup","delivery","documentCount"],"properties":{"jobId":{"type":"integer"},"referenceNumber":{"type":["string","null"]},"status":{"type":"string","enum":["LEAD","FOLLOW_UP","BOOKED","READY_FOR_PICKUP","PICKED_UP","ON_TRIP","IN_STORAGE","READY_FOR_DELIVERY","DELIVERED","CANCELLED","DEAD","DELETED"]},"orderType":{"$ref":"#/components/schemas/Lookup"},"flag":{"type":["string","null"],"description":"The CRM flag description, or null when the job has no flag."},"branch":{"$ref":"#/components/schemas/Lookup"},"createdAt":{"type":"string","description":"CRM local time, no zone suffix."},"updatedAt":{"type":["string","null"],"description":"Last CRM activity. Only populated on `GET /jobs/{jobId}` or when filtering by `updatedFrom`/`updatedTo`."},"bookingDate":{"type":["string","null"],"format":"date"},"moveDate":{"type":["string","null"],"format":"date"},"customer":{"type":"object","properties":{"firstName":{"type":["string","null"]},"lastName":{"type":["string","null"]},"email":{"type":["string","null"]},"phone":{"type":["string","null"]}}},"pickup":{"$ref":"#/components/schemas/Stop"},"delivery":{"$ref":"#/components/schemas/Stop"},"assignedTo":{"type":["string","null"],"description":"Full name of the assigned employee, or null when unassigned."},"documentCount":{"type":"integer"}}},"Stop":{"type":"object","properties":{"city":{"type":["string","null"]},"state":{"type":["string","null"]},"zip":{"type":["string","null"]}}},"JobsPage":{"type":"object","required":["ok","items","nextCursor"],"properties":{"ok":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"nextCursor":{"type":["string","null"]}}},"Document":{"type":"object","required":["documentId","name","sizeBytes"],"properties":{"documentId":{"type":"integer"},"name":{"type":"string"},"sizeBytes":{"type":"integer"},"addedBy":{"type":["string","null"]},"addedOn":{"type":["string","null"],"description":"CRM local time, no zone suffix."}}},"DownloadLink":{"type":"object","required":["ok","documentId","name","sizeBytes","contentType","url","expiresAt"],"properties":{"ok":{"type":"boolean"},"documentId":{"type":"integer"},"name":{"type":"string"},"sizeBytes":{"type":"integer"},"contentType":{"type":"string"},"url":{"type":"string","format":"uri"},"expiresAt":{"type":"string","format":"date-time"}}},"DocumentCreatedEvent":{"type":"object","required":["event","eventId","timestamp","data"],"properties":{"event":{"type":"string","const":"document.created"},"eventId":{"type":"string","description":"Stable per document; dedupe on it."},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"jobId":{"type":"integer"},"document":{"type":"object","properties":{"documentId":{"type":"integer"},"name":{"type":"string"},"sizeBytes":{"type":"integer"},"createdOn":{"type":"string"},"createdByName":{"type":["string","null"]},"createdById":{"type":["integer","null"]}}}}}}},"DocumentDeletedEvent":{"type":"object","required":["event","eventId","timestamp","data"],"properties":{"event":{"type":"string","const":"document.deleted"},"eventId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"jobId":{"type":"integer"},"documentId":{"type":"integer"},"name":{"type":"string"}}}}}}}}