Overview
Read-only access to jobs and the documents attached to them, plus a webhook that announces document changes.
Authentication
Send 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.
Rate limits
Sustained ~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.
Paging
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.
Timestamps
Date-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).
Errors
Every error body is { "ok": false, "error": "<code>", "message": "<human readable>" } (except the gateway's 429 above).
Base URL
https://safewaymoving.movingpro.net/api
All endpoints below are relative to this base. Example:
curl -H "x-api-key: YOUR_KEY" "https://safewaymoving.movingpro.net/api/jobs?limit=100"Meta
Liveness probe
200 OK
{
"ok": true,
"service": "safewaymovinginc-documents-api"
}This reference (HTML)
200 HTML page
Interactive Swagger UI
200 HTML page
This document as JSON
200 OpenAPI document
Jobs
List jobs
Newest-first, with optional filters and keyset paging. Unknown query parameters are rejected (400) so a typo never silently widens a backfill.
| Name | In | Type | Description |
|---|---|---|---|
status | query | LEAD FOLLOW_UP BOOKED READY_FOR_PICKUP PICKED_UP ON_TRIP IN_STORAGE READY_FOR_DELIVERY DELIVERED CANCELLED DEAD DELETED | Folded lead + invoice status. |
orderType | query | integer | Order type id (see orderType.id on a job; e.g. 1 Local, 2 Long Distance, 5 Intrastate). |
flag | query | string | Flag description exactly as shown in the CRM and in flag on a job (e.g. 6 - Visual Estimate). |
branchId | query | integer | Branch id (see branch.id). |
createdFrom | query | date-time | Job created on/after (YYYY-MM-DD or YYYY-MM-DDTHH:mm[:ss], CRM local time). |
createdTo | query | date-time | Job created on/before. A date-only value covers the whole day. |
bookedFrom | query | date | |
bookedTo | query | date | |
moveFrom | query | date | |
moveTo | query | date | |
updatedFrom | query | date-time | Last CRM activity on/after. Enables the (slower) updatedAt computation. |
updatedTo | query | date-time | |
pickupState | query | string | Two-letter state of the first pickup stop. |
deliveryState | query | string | |
hasDocuments | query | boolean | Only jobs with (or without) at least one document. |
limit | query | integer | |
cursor | query | string | Opaque nextCursor from the previous page. |
200 A page of jobs
{
"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 Bad filter, cursor or limit
{
"ok": false,
"error": "invalid_filter",
"message": "Unknown query parameter \"statuss\"."
}401 Missing or invalid key
{
"ok": false,
"error": "unauthorized",
"message": "An x-api-key header is required."
}429 Rate limit exceeded (emitted by the gateway, not this API)
{
"message": "Too Many Requests"
}Get one job
| Name | In | Type | Description |
|---|---|---|---|
jobId | path · required | integer | The jobId from the jobs list. |
200 The job (with `updatedAt` populated)
{
"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 Missing or invalid key
{
"ok": false,
"error": "unauthorized",
"message": "The API key is invalid or inactive."
}404 Unknown job
{
"ok": false,
"error": "not_found",
"message": "No job with id 999999."
}429 Rate limit exceeded (emitted by the gateway, not this API)
{
"message": "Too Many Requests"
}Documents
List documents on a job
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.
| Name | In | Type | Description |
|---|---|---|---|
jobId | path · required | integer | The jobId from the jobs list. |
200 Documents
{
"ok": true,
"items": [
{
"documentId": 79159,
"name": "Bill of Lading - order 20312.pdf",
"sizeBytes": 320786,
"addedBy": "Office User",
"addedOn": "2026-07-03T14:02:00"
}
]
}401 Missing or invalid key
{
"ok": false,
"error": "unauthorized",
"message": "The API key is invalid or inactive."
}404 Unknown job
{
"ok": false,
"error": "not_found",
"message": "No job with id 999999."
}429 Rate limit exceeded (emitted by the gateway, not this API)
{
"message": "Too Many Requests"
}Get a temporary direct download link
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.
| Name | In | Type | Description |
|---|---|---|---|
jobId | path · required | integer | The jobId from the jobs list. |
documentId | path · required | integer | The documentId from the documents list (or a webhook). |
200 Download link
{
"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 Missing or invalid key
{
"ok": false,
"error": "unauthorized",
"message": "The API key is invalid or inactive."
}404 Unknown job or document
{
"ok": false,
"error": "not_found",
"message": "Document 1 was not found on job 20312."
}429 Rate limit exceeded (emitted by the gateway, not this API)
{
"message": "Too Many Requests"
}501 Stored where this API cannot serve it
{
"ok": false,
"error": "storage_unsupported",
"message": "This document is stored in a location this API cannot serve."
}Webhooks
Events MovingPro pushes to the HTTPS URL you register. Names below are the event values.
A document was added to a job
Delivered as an HTTPS POST to the URL you register with MovingPro, within about a minute of the change in the CRM.
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.
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.
| Name | In | Type | Description |
|---|---|---|---|
X-MovingPro-Timestamp | header · required | string | Unix seconds when the request was signed. |
X-MovingPro-Signature | header · required | string | v1= + hex HMAC-SHA256 of timestamp + "." + body. |
X-MovingPro-Event | header · required | document.created document.deleted | |
X-MovingPro-Event-Id | header · required | string |
Payload
{
"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
}
}
}
2xx Acknowledged. Any other status is treated as a failed delivery (4xx: dropped; 5xx/timeout: retried later).
A document was removed from a job
| Name | In | Type | Description |
|---|---|---|---|
X-MovingPro-Timestamp | header · required | string | Unix seconds when the request was signed. |
X-MovingPro-Signature | header · required | string | v1= + hex HMAC-SHA256 of timestamp + "." + body. |
X-MovingPro-Event | header · required | document.created document.deleted | |
X-MovingPro-Event-Id | header · required | string |
Payload
{
"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"
}
}