# Fetch a quote Source: https://apidocs.dyspach.com/api-reference/fetch-quote Retrieve a previously generated quote by its id. **`GET`** `/v1/org/quote/{quoteId}` Retrieve a quote you generated earlier, using the `quoteId` from any `results` entry. The quote must belong to your organisation. ## Authorization Your organisation API key (`dzk_live_…`). ## Path parameters The `quoteId` returned when the quote was generated, e.g. `K541037RNBUQ`. ## Response Same shape as [Generate quotes](/api-reference/get-quote) — `origin`, `destination`, `goods`, and a single `results` entry for the quote. ```bash cURL theme={null} curl https://api.dyspach.com/v1/org/quote/K541037RNBUQ \ -H "x-api-key: $DYSPACH_API_KEY" ``` ```json 200 theme={null} { "origin": { "postalCode": "2000", "suburb": "BARANGAROO", "city": "Sydney", "state": "NSW", "stateName": "New South Wales", "countryCode": "AU" }, "destination": { "postalCode": "3000", "suburb": "MELBOURNE", "city": "Melbourne", "state": "VIC", "stateName": "Victoria", "countryCode": "AU" }, "goods": [ { "type": "Carton/Box", "quantity": 2, "palletSpaces": 0, "length": 30, "width": 30, "height": 30, "weight": 10, "chargeableWeight": 20, "customFieldValues": [{ "key": "ITEM_CATEGORY", "value": "FRAGILE" }] } ], "customFieldValues": [{ "key": "ACCOUNT_TYPE", "value": "PREMIUM" }], "results": [ { "customerId": "ACME", "quoteId": "K541037RNBUQ", "noQuoteMessage": "", "quotes": [ { "rateId": "7cd6c7cf-84af-419d-b34d-6d270cc809ee", "serviceName": "Falcon Freight", "serviceId": "FALCON", "rateCardName": "Falcon Standard", "rateCardId": "FALCON_STD", "carrierName": "Falcon Freight", "carrierId": "FALCON", "isSpotRate": false, "spotRateMessage": "", "amount": 10, "totalAmount": 19.8, "gstAmount": 1.8, "deliveryTime": "1 - 5 business day(s)", "minTransitTime": 1, "maxTransitTime": 5, "transitTimeUnit": "D", "surcharges": [ { "name": "Remote Location", "description": "Remote Location", "key": "Remote Location", "amount": 8 } ] } ] } ] } ``` ```json 404 theme={null} { "statusCode": 404, "message": "Quote not found", "error": "Not Found" } ``` # Check a fulfilment import Source: https://apidocs.dyspach.com/api-reference/get-fulfilment-import Poll a fulfilment import for its status and counts. **`GET`** `/v1/org/fulfilment/imports/{importId}` Reports where the import is and what it did. Imports run on a queue, so a push typically finishes within a couple of minutes. Poll every 30 seconds or so — a tight loop only spends your rate limit. ## Status values | Status | Meaning | | - | - | | `queued` | Accepted, waiting to start | | `processing` | Running | | `completed` | Finished — **check the counts**, some activities may still have been rejected | | `failed` | The import itself could not run. `errorMessage` says why | | `cancelled` | Stopped from the Dyspach portal | ## Counts | Field | Meaning | | - | - | | `counts.received` | Rows read from your submission | | `counts.processed` | Activities written to the ledger | | `counts.failed` | Rows rejected — list them with [the errors endpoint](/api-reference/get-fulfilment-import-errors) | `completed` with failures is the normal case: valid activities are never held back because others were wrong. Counts settle when the import reaches `completed`, `failed` or `cancelled`. While it is still `processing`, `failed` reads `0` and `processed` is the running total. `errorReportUrl` is a signed link to the same rejections as a spreadsheet. It is `null` until the import finishes and stays `null` when nothing was rejected. ```bash cURL theme={null} curl https://api.dyspach.com/v1/org/fulfilment/imports/7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31 \ -H "x-api-key: $DYSPACH_API_KEY" ``` ```json 200 theme={null} { "importId": "7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31", "source": "data", "status": "completed", "progress": 100, "counts": { "received": 5639, "processed": 5610, "failed": 29 }, "sourceDetails": "org:Acme 3PL / nightly-2026-08-17", "submittedAt": "2026-08-17T01:00:00.000Z", "startedAt": "2026-08-17T01:00:05.000Z", "finishedAt": "2026-08-17T01:02:00.000Z", "errorMessage": null, "errorReportUrl": null } ``` # List rejected activities Source: https://apidocs.dyspach.com/api-reference/get-fulfilment-import-errors Every activity a fulfilment import could not accept, with the reason. **`GET`** `/v1/org/fulfilment/imports/{importId}/errors` Returns the activities the import rejected, each with the reason and the values you sent, keyed by column name — enough to fix the record at source and send it again. The same rows are queued in the Dyspach portal, where your customer can correct and submit them by hand. ## Query parameters The `nextCursor` from the previous page. Omit for the first page. Rows per page, 1–500. ## Response | Field | Description | | - | - | | `items[].reference` | The `reference` you sent for that activity, or `null` if you sent none. An activity has no id of its own, so this is the only handle back to the row you sent | | `items[].error` | Why the row was rejected, in plain language | | `items[].data` | The values you sent for that row, keyed by column name | | `nextCursor` | Pass as `cursor` for the next page. `null` on the last page | ## Why activities get rejected | Reason | What to fix | | - | - | | `Warehouse code is required` | The row had no `warehouseCode` | | `Charge code is required` | The row had no `chargeCode` | | `Valid quantity is required` | `quantity` was missing or not a number. Note `0` is not a valid quantity | | `Valid activity timestamp is required` | `activityTimestamp` was missing or unparseable | These four reach you only from a **file** upload, where a required column can be present in the header but blank on a given row. A data push is checked when you send it, so a missing required field comes back as a `400` naming it, not as a rejected row here. ```bash cURL theme={null} curl "https://api.dyspach.com/v1/org/fulfilment/imports/7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31/errors?limit=100" \ -H "x-api-key: $DYSPACH_API_KEY" ``` ```json 200 theme={null} { "items": [ { "reference": "ORD-598782", "error": "Warehouse code is required", "data": { "chargeCode": "Picking", "reference": "ORD-598782", "quantity": "3" } } ], "nextCursor": null } ``` # Check an import Source: https://apidocs.dyspach.com/api-reference/get-import Poll an import for its status and counts. **`GET`** `/v1/org/shipments/imports/{importId}` Reports where a [file import](/api-reference/import-shipments-file) is and what it did. Only file pushes have an id to check. A [data push](/api-reference/import-shipments) is written and answered in its own response, so there is nothing to poll. Imports run on a queue, one at a time per organisation, so a push typically finishes in **one to three minutes**. Poll every 30 seconds or so — a tight loop will just spend your rate limit. ## Authorization Your organisation API key (`dzk_live_…`). ## Path parameters The `importId` returned when the import was accepted. ## Status values | Status | Meaning | | - | - | | `queued` | Accepted, waiting to start | | `processing` | Running | | `completed` | Finished — **check the counts**, some rows may still have been rejected | | `failed` | The import itself could not run; nothing was written. `errorMessage` says why | | `cancelled` | Stopped from the Dyspach portal | ## Counts | Field | Meaning | | - | - | | `counts.received` | **Rows** read from your submission — a shipment with three goods lines is three rows, so this is not the shipment count echoed back when the batch was accepted | | `counts.processed` | Shipments written to the ledger | | `counts.failed` | Rows rejected — list them with [the errors endpoint](/api-reference/get-import-errors) | `completed` with failures is the normal case, not an exception: valid shipments are never held back because other rows were wrong. `errorReportUrl` is a **short-lived signed link** to a spreadsheet of the rejected rows with their reasons, available once the import finishes. It expires after an hour — read the import again for a fresh one. ## Recovering from `failed` A `failed` import wrote nothing. Re-send the same batch — shipments are matched on `shipmentId`, so replaying a push is always safe, even if an earlier attempt got part way. ## Errors | Status | When | | - | - | | `401` | Missing or invalid API key | | `404` | No import with that id in your organisation | ```bash cURL theme={null} curl https://api.dyspach.com/v1/org/shipments/imports/9f2b1c44-0d3e-4a71-9b52-0f4c1e77a8d1 \ -H "x-api-key: $DYSPACH_API_KEY" ``` ```json 200 theme={null} { "importId": "9f2b1c44-0d3e-4a71-9b52-0f4c1e77a8d1", "source": "data", "status": "completed", "progress": 100, "counts": { "received": 1000, "processed": 900, "failed": 100 }, "sourceDetails": "org:Acme 3PL / nightly-2026-08-14", "submittedAt": "2026-08-14T02:20:11.000Z", "startedAt": "2026-08-14T02:20:19.000Z", "finishedAt": "2026-08-14T02:23:02.000Z", "errorMessage": null, "errorReportUrl": "https://dyspach-media.s3.ap-southeast-2.amazonaws.com/long-processing/errors-9f2b1c44.xlsx?X-Amz-Signature=..." } ``` # List rejected rows Source: https://apidocs.dyspach.com/api-reference/get-import-errors Every row an import could not accept, with the reason. **`GET`** `/v1/org/shipments/imports/{importId}/errors` Returns the rows the import rejected, each with the reason and the values you sent, keyed by column name — enough to fix the record in your own system and re-send it. The same rows are also downloadable as a spreadsheet from `errorReportUrl` on the [import](/api-reference/get-import), and are queued in the Dyspach portal where your customer can correct and submit them by hand. ## Authorization Your organisation API key (`dzk_live_…`). ## Path parameters The `importId` of the import. ## Query parameters The `nextCursor` from the previous page. Omit for the first page. Rows per page, 1–500. Keep requesting with the returned `nextCursor` until it comes back `null`. ## Why rows get rejected | Reason | What to fix | | - | - | | `No Shipment ID found` | The row had no `shipmentId`. In a file, blank ids are **not** carried down from the row above | | `Origin Zone not found` | The origin **postcode** is not one we recognise. A mismatched suburb does not cause this — it is corrected to the postcode's own suburb | | `Destination Zone not found` | Same, for the destination | | `Origin and Destination Zones not found` | Neither postcode resolved | | `Missing mandatory custom field(s): …` | A field your customer's carrier marks mandatory had no value. The message names them | A rejected row is rejected whole — the shipment is not partly written. ## Errors | Status | When | | - | - | | `400` | Malformed `cursor` | | `401` | Missing or invalid API key | | `404` | No import with that id in your organisation | ```bash cURL theme={null} curl "https://api.dyspach.com/v1/org/shipments/imports/9f2b1c44-0d3e-4a71-9b52-0f4c1e77a8d1/errors?limit=100" \ -H "x-api-key: $DYSPACH_API_KEY" ``` ```json 200 theme={null} { "items": [ { "shipmentId": "CONN-88251", "error": "Destination Zone(s) not found", "data": { "shipmentId": "CONN-88251", "customerId": "ACME", "destinationPostCode": "9999", "destinationSuburb": "Nowhere", "destinationState": "VIC", "itemType": "Carton" } }, { "shipmentId": "CONN-88252", "error": "Missing mandatory custom field(s): Purchase Order", "data": { "shipmentId": "CONN-88252", "customerId": "GLOBEX", "itemType": "Pallet" } } ], "nextCursor": "MjAyNi0wOC0xNFQwMjoyMjoxMS4wMDBafGVyci0xMDA" } ``` # Generate quotes Source: https://apidocs.dyspach.com/api-reference/get-quote Price carrier services for one or more customers. **`POST`** `/v1/org/quote` Send an origin, destination, items, and the customer(s) to quote for. The response returns shared `origin` / `destination` / `goods`, plus one `results` entry per customer with its priced `quotes`. ## Authorization Your organisation API key (`dzk_live_…`). See [Authentication](/authentication). ## Body Customer internal id(s) to quote for, e.g. `["ACME"]`. At least one, max 50. Each must belong to your organisation. Pickup location. Only `postalCode` is required; the rest is enriched server-side. Origin postcode, e.g. `"2000"`. ISO country code. Pickup company name. Suburb. Address line 1. Contact name. Contact phone. Contact email. Delivery location. Same fields as `origin` (with `postalCode` required). Goods to quote. At least one item. Number of items. Number of pallet spaces occupied by this item. Defaults to 0. Length (metres). Width (metres). Height (metres). Weight per item (kg). Item type, e.g. `"Carton/Box"`. Declared value. Per-item custom field values. Keys must match the org's **SHIPMENT\_LEDGER\_GOODS** custom field keys. Custom field key, e.g. `"ITEM_CATEGORY"`. Field value. Pass an array of strings for multi-select fields. Optional shipment-level custom field values. Keys must match the org's **SHIPMENT\_LEDGER** custom field keys (e.g. `"ACCOUNT_TYPE"`). Echoed back unchanged at the response root. Custom field key, e.g. `"ACCOUNT_TYPE"`. Field value. Pass an array of strings for multi-select fields. Optional. Narrow which services / rate cards are priced. Empty or omitted = no restriction. See [Filters](/guides/filters). Carrier aliases. Service aliases (= `serviceId` in the response). Rate-card aliases (= `rateCardId` in the response). e.g. `DOMESTIC`, `INTERNATIONAL`. e.g. `STANDARD`, `EXPRESS`. ## Response Enriched pickup location echoed back. Enriched delivery location echoed back. The items, with computed `chargeableWeight`. Each entry also echoes back any `customFieldValues` supplied for that item (empty array when none were sent). Shipment-level custom field values echoed back from the request. Each entry is `{ key: string; value: string | string[] }`. Empty array when none were sent. One entry per requested customer. The customer this block is for. Unique id — pass to [Fetch a quote](/api-reference/fetch-quote). Set when nothing could be priced. One entry per matched service. Reference for this priced line. Human service name. Service alias (use in `filters.serviceIds`). Human rate-card name. Rate-card alias (use in `filters.rateCardIds`). Name of the carrier (vendor) that provides this service. Carrier (vendor) code (use in `filters.carrierIds`). Freight amount (sell). Total incl. surcharges + GST. GST portion. Whether this is a spot rate. Human transit estimate. Minimum transit. Maximum transit. Transit unit, e.g. `D` (days). Applied surcharges. Surcharge name. Description. Surcharge key. Surcharge amount. ```bash cURL theme={null} # Every accepted field. Required fields carry real values; optional fields are # shown with their empty default. Two cannot be literally blank: `email` is # validated as an email so use null (or omit) when blank, and `type` must name a # real packaging type (a blank type returns no quotes). curl https://api.dyspach.com/v1/org/quote \ -H "x-api-key: $DYSPACH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "general": { "customerIds": ["ACME"] }, "origin": { "postalCode": "2000", "countryCode": "AU", "companyName": "", "contactName": "", "suburbName": "", "phone": "", "email": null, "street1": "", "street2": "", "street3": "", "instructions": "" }, "destination": { "postalCode": "3000", "countryCode": "AU", "companyName": "", "contactName": "", "suburbName": "", "phone": "", "email": null, "street1": "", "street2": "", "street3": "", "instructions": "" }, "items": [ { "type": "Carton/Box", "length": 0.3, "width": 0.3, "height": 0.3, "weight": 10, "quantity": 2, "palletSpaces": 0, "itemValue": 0, "commodityList": [], "customFieldValues": [] } ], "filters": { "carrierIds": [], "serviceTypes": [], "serviceClasses": [], "serviceIds": [], "rateCardIds": [] }, "customFieldValues": [] }' ``` ```json Body theme={null} { "general": { "customerIds": ["ACME"] }, "origin": { "postalCode": "2000", "countryCode": "AU", "companyName": "", "contactName": "", "suburbName": "", "phone": "", "email": null, "street1": "", "street2": "", "street3": "", "instructions": "" }, "destination": { "postalCode": "3000", "countryCode": "AU", "companyName": "", "contactName": "", "suburbName": "", "phone": "", "email": null, "street1": "", "street2": "", "street3": "", "instructions": "" }, "items": [ { "type": "Carton/Box", "length": 0.3, "width": 0.3, "height": 0.3, "weight": 10, "quantity": 2, "palletSpaces": 0, "itemValue": 0, "commodityList": [], "customFieldValues": [] } ], "filters": { "carrierIds": [], "serviceTypes": [], "serviceClasses": [], "serviceIds": [], "rateCardIds": [] }, "customFieldValues": [] } ``` ```json 200 theme={null} { "origin": { "postalCode": "2000", "suburb": "BARANGAROO", "city": "Sydney", "state": "NSW", "stateName": "New South Wales", "countryCode": "AU" }, "destination": { "postalCode": "3000", "suburb": "MELBOURNE", "city": "Melbourne", "state": "VIC", "stateName": "Victoria", "countryCode": "AU" }, "goods": [ { "type": "Carton/Box", "quantity": 2, "palletSpaces": 0, "length": 30, "width": 30, "height": 30, "weight": 10, "chargeableWeight": 20, "customFieldValues": [{ "key": "ITEM_CATEGORY", "value": "FRAGILE" }] } ], "customFieldValues": [{ "key": "ACCOUNT_TYPE", "value": "PREMIUM" }], "results": [ { "customerId": "ACME", "quoteId": "K541037RNBUQ", "noQuoteMessage": "", "quotes": [ { "rateId": "7cd6c7cf-84af-419d-b34d-6d270cc809ee", "serviceName": "Falcon Freight", "serviceId": "FALCON", "rateCardName": "Falcon Standard", "rateCardId": "FALCON_STD", "carrierName": "Falcon Freight", "carrierId": "FALCON", "isSpotRate": false, "spotRateMessage": "", "amount": 10, "totalAmount": 19.8, "gstAmount": 1.8, "deliveryTime": "1 - 5 business day(s)", "minTransitTime": 1, "maxTransitTime": 5, "transitTimeUnit": "D", "surcharges": [ { "name": "Remote Location", "description": "Remote Location", "key": "Remote Location", "amount": 8 } ] }, { "rateId": "ba9bc08c-0238-4c88-aff5-b4f26aaab790", "serviceName": "Zephyr Couriers", "serviceId": "ZEPHYR", "rateCardName": "Zephyr Express", "rateCardId": "ZEPHYR_EXP", "carrierName": "Zephyr Couriers", "carrierId": "ZEPHYR", "isSpotRate": false, "spotRateMessage": "", "amount": 9, "totalAmount": 11.09, "gstAmount": 1.01, "deliveryTime": "0 - 0 business day(s)", "minTransitTime": 0, "maxTransitTime": 0, "transitTimeUnit": "D", "surcharges": [ { "name": "Fuel Surcharge", "description": "Fuel Surcharge", "key": "FUEL", "amount": 1.08 } ] } ] } ] } ``` # Import activities (JSON) Source: https://apidocs.dyspach.com/api-reference/import-fulfilment-activities Push warehouse activity into your fulfilment ledger. **`POST`** `/v1/org/fulfilment/activities` Send up to **10,000 activities** in one call, in the [published structure](/guides/fulfilment-structure) — enough for a full day of warehouse activity in a single request. The data is turned into exactly the file your customer would have uploaded and handed to the same importer, so validation and error reporting are identical either way. The call returns as soon as the batch is accepted; [check the import](/api-reference/get-fulfilment-import) for the outcome. **Every push appends.** Activities have no identity, so sending the same batch twice records it twice — and each activity is billable. Send a run once and check the import rather than retrying blindly. ## Authorization Your organisation API key (`dzk_live_…`). ## Body 1–10,000 activities. See [Fulfilment activity structure](/guides/fulfilment-structure) for every field. `warehouseCode`, `chargeCode`, `quantity` and `activityTimestamp` are required; the rest are optional. Your own reference for this push — a batch id, run date or filename. Recorded on the import so a specific run can be found later. ## Response `202 Accepted` with the import's id. | Field | Description | | - | - | | `importId` | Check this for the outcome | | `status` | `queued` on acceptance | | `source` | `data` for this endpoint | | `activitiesReceived` | Activities accepted for processing. One activity is one row | | `statusUrl` | Convenience path to the status endpoint | ## Errors | Status | When | | - | - | | `400` | Malformed body, more than 10,000 activities, an activity missing one of the four required fields (or with `quantity: 0`), an unrecognised field, or too many imports already queued | | `401` | Missing or invalid API key | | `429` | Rate limit exceeded — see [Rate limits](/guides/rate-limits) | ```bash cURL theme={null} # Every accepted field. Required fields carry real values; optional fields are # shown with their empty default. quantity cannot be 0 (it is signed — negatives # are credits); activityTimestamp is ISO-8601; warehouseCode and chargeCode must # be non-empty. curl -X POST https://api.dyspach.com/v1/org/fulfilment/activities \ -H "x-api-key: $DYSPACH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sourceDetails": "", "activities": [ { "warehouseCode": "SYD01", "chargeCode": "Picking", "customerId": "", "chargeDescription": "", "quantity": 1, "reference": "", "additionalReference1": "", "additionalReference2": "", "additionalReference3": "", "activityTimestamp": "2026-08-10T16:00:00Z", "status": "" } ] }' ``` ```json 202 theme={null} { "importId": "7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31", "status": "queued", "source": "data", "activitiesReceived": 1, "statusUrl": "/v1/org/fulfilment/imports/7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31" } ``` # Import activities (file) Source: https://apidocs.dyspach.com/api-reference/import-fulfilment-activities-file Push warehouse activity as a CSV or Excel file. **`POST`** `/v1/org/fulfilment/activities/file` The same [published structure](/guides/fulfilment-structure) delivered as a file — one activity per row. Useful when a file is simply what your warehouse system already produces. ## Authorization Your organisation API key (`dzk_live_…`). ## Form fields `.csv` or `.xlsx`, up to **50 MB**. The first row must be the header row. Your own reference for this push. Defaults to the filename. ## The header row is checked before anything is queued A structure mistake is yours to fix, so you hear about it on the request: ```json theme={null} { "statusCode": 400, "message": "Invalid file structure — missing required column(s): chargeCode; unrecognised column(s): Tenant Name", "error": "Bad Request" } ``` Once the header passes, the file is queued and every row is validated during the import — see [Check an import](/api-reference/get-fulfilment-import). ## Errors | Status | When | | - | - | | `400` | File missing, empty, unreadable, an unsupported type, or a header row that does not match the structure | | `401` | Missing or invalid API key | | `413` | File larger than 50 MB — split it | | `429` | Rate limit exceeded | ```bash cURL theme={null} curl -X POST https://api.dyspach.com/v1/org/fulfilment/activities/file \ -H "x-api-key: $DYSPACH_API_KEY" \ -F "file=@activity-2026-08-17.csv" \ -F "sourceDetails=nightly-2026-08-17" ``` ```json 202 theme={null} { "importId": "7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31", "status": "queued", "source": "file", "statusUrl": "/v1/org/fulfilment/imports/7c1e2f90-2b44-4f0e-9a11-6d1f0c22ab31" } ``` # Import shipments (JSON) Source: https://apidocs.dyspach.com/api-reference/import-shipments Push a batch of shipments and get the result in the same call. **`POST`** `/v1/org/shipments/imports` Send up to **100 shipments** in one call, in the [published structure](/guides/shipment-structure), and get the outcome **in the same response** — no polling, no second call. By the time this returns, the shipments are in the ledger and you have a verdict for each one. Sized for how partners actually integrate: a step in your manifest-close flow, or an end-of-day scheduled job. For bigger runs send [a file](/api-reference/import-shipments-file), which is queued instead. **Sending the same shipment twice is safe.** A `shipmentId` already in the ledger is updated, not duplicated — its goods lines are replaced by the ones you send. Retries, re-runs and corrected re-sends all do the right thing. ## Authorization Your organisation API key (`dzk_live_…`). ## Body 1–100 shipments. See [Shipment structure](/guides/shipment-structure) for every field. Each shipment needs at least one entry in `items`, at most 500, and the batch as a whole may carry at most 10,000 goods lines — send bigger runs [as a file](/api-reference/import-shipments-file). Your own reference for this push — a batch id, run date or filename. It is echoed on the import and recorded against every shipment it creates, so a shipment can be traced back to the run that sent it. Unknown fields are **rejected**, not ignored. If you send `weight` where the structure says `itemWeight`, you get a `400` naming the field rather than a shipment that silently lost its weight. ## Response `200 OK` — the shipments are written. | Field | Description | | - | - | | `received` | Shipments in your request | | `created` | Shipments new to the ledger | | `updated` | Shipments that already existed and were replaced | | `failed` | Shipments rejected — each one's reason is in `results` | | `results` | One entry per shipment: `created`, `updated` or `failed` (+ `error`) | `created` and `updated` both mean the shipment is in the ledger; only `failed` needs action. Valid shipments are never held back because others in the batch were wrong. There is no import id to poll: this response is the outcome. Your customer still sees the submission in their Dyspach Activity Center, and your own `sourceDetails` is recorded against every shipment, so a specific push can always be traced back later. ## Errors | Status | When | | - | - | | `400` | Malformed body, more than 100 shipments, more than 10,000 goods lines in total, more than 500 lines on one shipment, a shipment with no items, an unrecognised field, or a custom field key your organisation has not configured | | `401` | Missing or invalid API key | | `429` | Rate limit exceeded — see [Rate limits](/guides/rate-limits) | ```bash cURL theme={null} # Every accepted field. Required fields carry real values; optional fields are # shown with their empty default. The four date fields (quoteDate, pickupDate, # completedDate, expectedCompletedDate) are ISO-8601 validated, so use null (or # omit) when blank — an empty string is rejected. Each item needs a real itemType. curl -X POST https://api.dyspach.com/v1/org/shipments/imports \ -H "x-api-key: $DYSPACH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sourceDetails": "", "shipments": [ { "shipmentId": "SHIP-0001", "customerId": "", "originPostCode": "3000", "originSuburb": "Melbourne", "originState": "VIC", "originCountryCode": "AU", "originCompanyName": "", "originContactName": "", "originContactPhone": "", "originContactEmail": "", "originAddressStreet1": "", "originAddressStreet2": "", "originAddressStreet3": "", "originLocationType": "", "originRequiresTailLift": false, "destinationPostCode": "2000", "destinationSuburb": "Sydney", "destinationState": "NSW", "destinationCountryCode": "AU", "destinationCompanyName": "", "destinationContactName": "", "destinationContactPhone": "", "destinationContactEmail": "", "destinationAddressStreet1": "", "destinationAddressStreet2": "", "destinationAddressStreet3": "", "destinationLocationType": "", "destinationRequiresTailLift": false, "destinationGivesAuthorityToLeave": false, "destinationSignatureOnGlass": false, "quoteDate": null, "pickupDate": null, "completedDate": null, "expectedCompletedDate": null, "rateCardAlias": "", "totalCostReceived": 0, "customField1": "", "customField2": "", "customField3": "", "customFields": {}, "items": [ { "itemType": "Carton/Box", "itemId": "", "itemQuantity": 0, "itemPalletSpaces": 0, "itemLength": 0, "itemWidth": 0, "itemHeight": 0, "itemWeight": 0, "itemChargeableWeight": 0, "itemVolumetricWeight": 0, "itemValue": 0, "dangerousGoods": false, "customFields": {} } ] } ] }' ``` ```json 200 theme={null} { "received": 2, "created": 1, "updated": 0, "failed": 1, "results": [ { "shipmentId": "CONN-88213", "status": "created" }, { "shipmentId": "CONN-88214", "status": "failed", "error": "Destination Zone(s) not found" } ] } ``` ```json 400 theme={null} { "statusCode": 400, "message": ["shipments.0.items should not be empty"], "error": "Bad Request" } ``` # Import shipments (file) Source: https://apidocs.dyspach.com/api-reference/import-shipments-file Push shipments as a CSV or Excel file in the published structure. **`POST`** `/v1/org/shipments/imports/file` The same [published structure](/guides/shipment-structure), delivered as a file — for nightly extracts and larger volumes, or simply because a file is what your system already produces. **One row per goods line.** Repeat the shipment columns on each line of a multi-line shipment and keep those lines together. Column order does not matter and optional columns may be omitted. The full column list, in canonical order, with a worked example is in [Shipment structure](/guides/shipment-structure) — copy it straight into a `.csv`. ## Authorization Your organisation API key (`dzk_live_…`). ## Form fields `.csv` or `.xlsx`, up to **50 MB**. The first row must be the header row. Your own reference for this push. Defaults to the filename. ## The header row is checked before anything is queued A structure mistake is yours to fix, so you hear about it on the request rather than minutes later. The response names exactly what is wrong: ```json theme={null} { "statusCode": 400, "message": "Invalid file structure — missing required column(s): itemType; unrecognised column(s): weight_kg", "error": "Bad Request" } ``` Once the header passes, the file is queued and every **row** is validated individually during the import — see [Check an import](/api-reference/get-import). ## Response `202 Accepted`, the same shape as the JSON lane, except `received` is absent — the row count is not known until the file is parsed. ## Errors | Status | When | | - | - | | `400` | File missing, empty, unreadable, an unsupported type, or a header row that does not match the structure | | `400` | `.xls` is not accepted — re-save as `.csv` or `.xlsx` | | `400` | Too many imports already queued | | `401` | Missing or invalid API key | | `413` | File larger than 50 MB — split it | | `429` | Rate limit exceeded | ```bash cURL theme={null} curl -X POST https://api.dyspach.com/v1/org/shipments/imports/file \ -H "x-api-key: $DYSPACH_API_KEY" \ -F "file=@shipments-2026-08-14.csv" \ -F "sourceDetails=nightly-2026-08-14" ``` ```json 202 theme={null} { "importId": "9f2b1c44-0d3e-4a71-9b52-0f4c1e77a8d1", "status": "queued", "source": "file", "statusUrl": "/v1/org/shipments/imports/9f2b1c44-0d3e-4a71-9b52-0f4c1e77a8d1" } ``` ```json 400 theme={null} { "statusCode": 400, "message": "Invalid file structure — missing required column(s): originSuburb", "error": "Bad Request" } ``` # Authentication Source: https://apidocs.dyspach.com/authentication Create an organisation API key and use it on every request. Every request is authenticated with an **organisation API key**, sent in the `x-api-key` header. ```bash theme={null} curl https://api.dyspach.com/v1/org/quote \ -H "x-api-key: dzk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ ... }' ``` ## Creating a key In the Dyspach portal, go to **Settings → API Keys** and choose **Create new key**. The full key (`dzk_live_…`) is shown **once**, at creation. Copy it and store it in your secrets manager right away. For security, only a hashed form of the key is retained — the full key cannot be retrieved after creation. If it is lost or compromised, revoke it and issue a replacement. Send the key in the `x-api-key` header on every request. ## Key handling Only the key's prefix (e.g. `dzk_live_7fe18dc`) is shown afterward, for identification. The server stores only a SHA-256 hash of the key. Create several keys (e.g. per integration) and revoke individually. Revoke a key from **Settings → API Keys** at any time; it stops working immediately. ## Errors Sent when the `x-api-key` header is absent or does not match an active key. Make sure you copied the **full** key, not the prefix. See [Errors](/guides/errors) for the full list. # Errors Source: https://apidocs.dyspach.com/guides/errors Status codes, shapes, and how to fix them. Errors use standard HTTP status codes with a JSON body. Every response carries `statusCode`, `message`, the `path` you called, a `timestamp`, and a `correlationId` identifying that exact request. The one exception is **401** — a missing or invalid key is answered before the request enters the API, so that body carries only `message`. Quote the `correlationId` when you contact support: it is the fastest way for us to find what happened on our side. Send your own `X-Request-Id` and we echo it back here, provided it is at most 128 characters of letters, digits, `.`, `_` or `-` — otherwise we generate one for you. The body failed validation — e.g. missing `customerIds`, empty `items`, or an unresolvable postcode. `message` names every field that is wrong. ```json theme={null} { "statusCode": 400, "message": "general.customerIds should not be empty", "path": "/v1/org/quotes", "timestamp": "2026-08-19T06:56:25.918Z", "correlationId": "req-6f1c9a2e-4b77-4a1e-9d33-2f0c5a8e71bd" } ``` For a field inside a batch, the message says which entry it was, so you can fix the record at source: ```json theme={null} { "statusCode": 400, "message": "activities.0: quantity must not be 0; activities.7: chargeCode should not be empty", "path": "/v1/org/fulfilment/activities", "timestamp": "2026-08-19T06:56:25.918Z", "correlationId": "req-6f1c9a2e-4b77-4a1e-9d33-2f0c5a8e71bd" } ``` More than ten problems are summarised — the first ten, then `and N more`. The `x-api-key` header is missing or invalid. Ensure you're sending the **full** key, not the `dzk_live_…` prefix shown in the portal. ```json theme={null} { "message": "Invalid API key" } ``` One or more `customerIds` don't belong to your organisation. The whole request is rejected — no partial results. ```json theme={null} { "statusCode": 403, "message": "One or more customers do not belong to this organisation", "path": "/v1/org/quotes", "timestamp": "2026-08-19T06:56:25.918Z", "correlationId": "req-6f1c9a2e-4b77-4a1e-9d33-2f0c5a8e71bd" } ``` `GET /v1/org/quote/{quoteId}` — no quote with that id. ```json theme={null} { "statusCode": 404, "message": "Quote not found", "path": "/v1/org/quote/8f2b1c4e-…", "timestamp": "2026-08-19T06:56:25.918Z", "correlationId": "req-6f1c9a2e-4b77-4a1e-9d33-2f0c5a8e71bd" } ``` You exceeded your per-minute limit. The seconds to wait are on the `Retry-After` response header — read them from there, not from the body. ```json theme={null} { "statusCode": 429, "message": "Rate limit exceeded", "path": "/v1/org/quotes", "timestamp": "2026-08-19T06:56:25.918Z", "correlationId": "req-6f1c9a2e-4b77-4a1e-9d33-2f0c5a8e71bd" } ``` See [Rate limits](/guides/rate-limits). An **empty** `quotes` array is **not** an error — it means nothing matched (e.g. filters excluded everything, or no rate applies to the lane). Check `noQuoteMessage`. # Filters Source: https://apidocs.dyspach.com/guides/filters Narrow which services and rate cards get priced. The optional `filters` block restricts which of a customer's service mappings are priced. **Filter by the same values you see in the response** — service and rate card are matched by their alias (`serviceId` / `rateCardId`). ```json theme={null} "filters": { "carrierIds": ["ZEPHYR"], // carrier alias "serviceIds": ["EXPRESS"], // = the serviceId in the response "rateCardIds": ["EX_RATES"], // = the rateCardId in the response "serviceTypes": ["DOMESTIC"], // service type "serviceClasses": ["EXPRESS"] // service class } ``` ## Dimensions Restrict to services supplied by these **carriers**, matched by **carrier alias**. Restrict to these services, matched by **service alias** — the same value returned as `serviceId` on each quote line (e.g. `ZEPHYR`, `FALCON`). Restrict to these rate cards, matched by **rate-card alias** — the same value returned as `rateCardId` (e.g. `ZEPHYR_EXP`, `FALCON_STD`). Restrict to these service types, e.g. `DOMESTIC`, `INTERNATIONAL`. Restrict to these service classes, e.g. `STANDARD`, `EXPRESS`. ## How matching works An empty or omitted array places no restriction on that dimension. Omit `filters` entirely to price everything. A rate card is priced only if it satisfies **every** non-empty dimension; within one dimension, matching **any** listed value passes. ### Example — combine dimensions Price only the **Zephyr** service **and** the **ZEPHYR\_EXP** rate card: ```json theme={null} "filters": { "serviceIds": ["ZEPHYR"], "rateCardIds": ["ZEPHYR_EXP"] } ``` ### Example — several services Price Zephyr or Falcon (drops everything else): ```json theme={null} "filters": { "serviceIds": ["ZEPHYR", "FALCON"] } ``` If a filter matches nothing, that customer's `quotes` array comes back empty (and `noQuoteMessage` may explain why) — the request still succeeds. # Fulfilment activity structure Source: https://apidocs.dyspach.com/guides/fulfilment-structure The structure every fulfilment activity push uses, as JSON fields or file columns. An **activity** is one warehouse charge event — a pick, a pack, a receipt, a storage charge. It is what your customer's 3PL bills for. There is a single published structure. You choose how to deliver it — [as data](/api-reference/import-fulfilment-activities) or [as a file](/api-reference/import-fulfilment-activities-file) — and the fields are the same either way. In a file it is **one activity per row**; column order does not matter and optional columns may be left out. A column we do not publish is rejected rather than ignored. **Starter template.** Copy this into a `.csv` and replace the example rows. ```csv template.csv theme={null} customerId,warehouseCode,chargeCode,chargeDescription,quantity,reference,orderId,additionalReference1,additionalReference2,additionalReference3,activityTimestamp,status ACME,SYD01,Picking,,3,PICK-5521,ORD-598782,,,,2026-08-10T16:00:00Z,Approved ACME,SYD01,Packing,,1,PICK-5521,ORD-598782,,,,2026-08-10T16:00:00Z,Approved ``` ## Fields Four fields are required. They are exactly the four an activity cannot be imported without, so anything missing is refused when you send it — named — rather than accepted and thrown away minutes later. Where the activity happened, as configured in Dyspach. The charge being recorded, matching your customer's rate card — `Picking`, `Packing`, `Receipt - Unit`, and so on. Free text. Units of the charge. **Signed** — see below. `0` is not a quantity and is refused. When it happened, ISO-8601. This decides which billing period the charge falls into. The rest are optional: | Field | Type | Notes | | - | - | - | | `customerId` | string | The customer this activity belongs to, using the customer code configured in Dyspach | | `chargeDescription` | string | Free text | | `reference` | string | Your reference for the *activity*: pick-slip or receipt number | | `orderId` | string | Your own order number — what the activity was performed against. Stored verbatim. Send the same value on shipment pushes to line up warehouse and freight charges for one order | | `additionalReference1` | string | | | `additionalReference2` | string | | | `additionalReference3` | string | | | `status` | string | Your own status, recorded as sent | On a file upload, all four required columns must be present in the header row. A row that leaves one of them **blank** is still refused, but per row, with the reason on the [errors endpoint](/api-reference/get-fulfilment-import-errors) — the file is not rejected whole because of one bad line. ## Value formats Negatives are expected, not an error — they are how credits and reversals are recorded. `-35` is as valid as `35`. `2026-08-10T16:00:00Z`. This decides which billing period the charge falls into, so it is never inferred. Do not send `8/10/26` — that is August to some systems and October to others. An activity has no identity: the same charge code, reference and timestamp can legitimately occur several times — four identical picks on one order in the same second is normal. So nothing is de-duplicated, and **sending a batch twice records it twice.** Each row is billable, so send a run once and check the import rather than retrying blindly. # Rate limits Source: https://apidocs.dyspach.com/guides/rate-limits How request throttling works and how to handle it. Requests are rate limited **per organisation** using a fixed 60-second window. **60 requests / minute** per organisation (across all your keys). Need more? Dyspach can raise the limit for your account. ## Response headers Every response includes your current window state: ```http theme={null} RateLimit-Limit: 60 RateLimit-Remaining: 42 RateLimit-Reset: 23 ``` Requests allowed in the current window. Requests remaining in the current window. Seconds until the window resets. ## When you're throttled You receive `429 Too Many Requests` with a `Retry-After` header: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 23 ``` ```json theme={null} { "message": "Rate limit exceeded", "limit": 60, "retryAfterSeconds": 23 } ``` Back off for `Retry-After` seconds, then retry. Read `RateLimit-Remaining` to pace yourself and avoid hitting the limit at all. # Shipment structure Source: https://apidocs.dyspach.com/guides/shipment-structure The one structure every shipment push uses, as JSON fields or file columns. There is a single published structure for shipments. You choose how to deliver it — [as a data batch](/api-reference/import-shipments) (written and answered in the same call, up to 100 shipments) or [as a file](/api-reference/import-shipments-file) (bigger runs, queued) — but the fields are the same either way, and the same for every organisation. * **JSON**: each shipment is an object with these field names; its goods lines go in `items`. * **File**: each column header is the same name, and there is **one row per goods line** — repeat the shipment columns on each line of a multi-line shipment, and keep the lines of a shipment together. `.csv` and `.xlsx` only; re-save `.xls` before sending. Column order does not matter and optional columns may be left out entirely. A column we do not publish is rejected rather than ignored, so a typo surfaces immediately instead of silently dropping data. **Starter template.** Copy this into a `.csv` and replace the example row — it is the full column list in canonical order. Optional columns you do not use can be deleted. ```csv template.csv theme={null} shipmentId,customerId,orderId,warehouseCode,originPostCode,originSuburb,originState,originCountryCode,originCompanyName,originContactName,originContactPhone,originContactEmail,originAddressStreet1,originAddressStreet2,originAddressStreet3,originLocationType,originRequiresTailLift,destinationPostCode,destinationSuburb,destinationState,destinationCountryCode,destinationCompanyName,destinationContactName,destinationContactPhone,destinationContactEmail,destinationAddressStreet1,destinationAddressStreet2,destinationAddressStreet3,destinationLocationType,destinationRequiresTailLift,destinationGivesAuthorityToLeave,destinationSignatureOnGlass,quoteDate,pickupDate,completedDate,expectedCompletedDate,rateCardAlias,totalCostReceived,customField1,customField2,customField3,itemType,itemId,itemQuantity,itemPalletSpaces,itemLength,itemWidth,itemHeight,itemWeight,itemChargeableWeight,itemVolumetricWeight,itemValue,dangerousGoods CONN-88213,ACME,ORD-1001,MEL01,3000,Melbourne,VIC,AU,Acme Distribution,,,,,,,,,2000,Sydney,NSW,AU,Globex Retail,,,,,,,,,Yes,,2026-08-14T02:15:00Z,,,,EXPRESS,412.55,,,,Pallet,,2,,120,100,150,240,,,,No ``` ## Identity Your connote / consignment number, up to 255 characters. This identifies the shipment permanently: **sending it again updates that shipment** instead of creating a second one, and replaces its goods lines with the ones in the new push. Matching ignores case. The customer this shipment belongs to, using the customer code configured in Dyspach — the same code you pass in `general.customerIds` when quoting. This is the link between the shipment and the customer at billing time. A shipment carrying a code nobody recognises still imports, but it will not be billed to anyone, so it is worth getting right. Your own order number for this shipment — whatever your system calls it. We store it verbatim and never generate, validate or reformat it, beyond trimming surrounding spaces. It is carried through to the invoice, so an invoiced shipment can be traced back to the order it fulfilled. Send the same value on your fulfilment activity pushes and the warehouse and freight charges for one order line up. One shipment carries one order. An order split across several shipments simply repeats on each of them. The warehouse the shipment went out of, using the warehouse code configured in Dyspach — the same code you send as `warehouseCode` on fulfilment activity pushes. Stored as sent, beyond trimming surrounding spaces; a code Dyspach does not know still imports. ## Origin `originPostCode`, `originSuburb`, `originState` and `originCountryCode` are **required**. **The postcode is what actually decides the zone.** If the suburb does not match the postcode it is not rejected — the closest matching suburb at that postcode is used, and failing that, the postcode's own zone. Only a postcode we do not recognise fails the shipment (`Origin Zone not found`). So a wrong suburb is silently corrected and the shipment is priced on the postcode. Send accurate postcodes; do not rely on the suburb being checked. | Field | Type | Notes | | - | - | - | | `originPostCode` | string | **Required** | | `originSuburb` | string | **Required** | | `originState` | string | **Required.** State/region code, e.g. `VIC` | | `originCountryCode` | string | **Required.** ISO country code, e.g. `AU` | | `originCompanyName` | string | | | `originContactName` | string | | | `originContactPhone` | string | | | `originContactEmail` | string | | | `originAddressStreet1` | string | | | `originAddressStreet2` | string | | | `originAddressStreet3` | string | | | `originLocationType` | string | e.g. `Residential`, `Business` | | `originRequiresTailLift` | boolean | `Yes` / `No` in a file | ## Destination Same four required fields, resolved the same way — the postcode decides, and an unrecognised one fails the shipment with `Destination Zone not found`. | Field | Type | Notes | | - | - | - | | `destinationPostCode` | string | **Required** | | `destinationSuburb` | string | **Required** | | `destinationState` | string | **Required** | | `destinationCountryCode` | string | **Required** | | `destinationCompanyName` | string | | | `destinationContactName` | string | | | `destinationContactPhone` | string | | | `destinationContactEmail` | string | | | `destinationAddressStreet1` | string | | | `destinationAddressStreet2` | string | | | `destinationAddressStreet3` | string | | | `destinationLocationType` | string | | | `destinationRequiresTailLift` | boolean | | | `destinationGivesAuthorityToLeave` | boolean | | | `destinationSignatureOnGlass` | boolean | | ## Dates and commercials | Field | Type | Notes | | - | - | - | | `quoteDate` | date-time | When the shipment was booked/manifested. Defaults to the import date if omitted | | `pickupDate` | date-time | | | `completedDate` | date-time | | | `expectedCompletedDate` | date-time | | | `rateCardAlias` | string | The service / rate card the shipment went on, as configured in Dyspach | | `totalCostReceived` | number | The cost you were charged, if you pass one through | | `customField1` | string | Free-text slot, kept for compatibility with file uploads | | `customField2` | string | | | `customField3` | string | | ## Goods Every shipment needs at least one goods line — at most 500, and at most 10,000 across a whole data batch (a file has no line limit beyond its 50 MB size). In JSON they are the `items` array; in a file, each line is its own row. What the line is — `Carton`, `Pallet`, `Satchel`, and so on. A row with no `itemType` contributes no goods line at all, which is why it is required. | Field | Type | Notes | | - | - | - | | `itemId` | string | Your reference for the line (SKU, item number) | | `itemQuantity` | number | Defaults to 1 | | `itemPalletSpaces` | number | | | `itemLength` | number | In your organisation's configured dimension unit | | `itemWidth` | number | | | `itemHeight` | number | | | `itemWeight` | number | | | `itemChargeableWeight` | number | Pass through if you have it; otherwise it is calculated | | `itemVolumetricWeight` | number | | | `itemValue` | number | | | `dangerousGoods` | boolean | | ## Custom fields Your customer's carrier may configure extra fields on shipments or goods. They are addressed **by key** — the same identifier you use for custom fields when quoting. A key is stable; the field's display name can be changed in the portal at any time, which is why it is not what you send. * **JSON** — a `customFields` object on the shipment, and another on each item: ```json theme={null} { "shipmentId": "CONN-88213", "customFields": { "PURCHASE_ORDER": "PO-99187" }, "items": [ { "itemType": "Pallet", "customFields": { "BATCH_NUMBER": "B-2291" } } ] } ``` * **File** — a column per field, prefixed `CF:` for shipment-level and `ITEM_CF:` for goods-level: ```csv theme={null} shipmentId,...,itemType,CF:PURCHASE_ORDER,ITEM_CF:BATCH_NUMBER CONN-88213,...,Pallet,PO-99187,B-2291 ``` A key that is not configured for that organisation is rejected — you will not silently lose the value. Fields marked **mandatory** must carry a value or the whole shipment is rejected with `Missing mandatory custom field(s): …`. ## Value formats In JSON, use real booleans (`true` / `false`). **In a file, write `Yes` or `No`** — a file cell containing `true` is read as "not yes", which is almost never what you meant. ISO-8601, e.g. `2026-08-14T02:15:00Z`. Dates without a time are accepted. Plain decimals — `412.55`. No currency symbols, no thousands separators. Repeat the shipment columns on every row of the shipment and keep those rows together. Rows are grouped by consecutive matching `shipmentId`: ```csv theme={null} shipmentId,originSuburb,...,itemType,itemQuantity CONN-88213,Melbourne,...,Pallet,2 CONN-88213,Melbourne,...,Carton,6 CONN-88214,Melbourne,...,Satchel,1 ``` A blank `shipmentId` is not carried down from the row above — it is rejected with `No Shipment ID found`. # Introduction Source: https://apidocs.dyspach.com/introduction Quote freight and push shipments programmatically. The **Dyspach API** does two things: * **Quote** — send an origin, destination and items, and get back priced carrier services to allocate against. * **Push shipments** — send the shipments you have booked into your Dyspach shipment ledger, as a data batch or as a file, so reconciliation and invoicing run on complete, current data without anyone uploading a spreadsheet by hand. Make your first quote request in a few minutes. Create and use an organisation API key. The one structure every shipment push uses. Push a batch of shipments and track the import. ## Base URL ```text theme={null} https://api.dyspach.com ``` All endpoints live under `/v1/org`. ## Core concepts You authenticate as your **organisation** (the tenant). A **customer** is one of your shippers. You reference a customer by its business **internal id** (e.g. `ACME`) — never a database UUID. `general.customerIds` is a list. The response returns one entry under `results` per customer, with shared `origin` / `destination` / `goods` at the top level. An optional `filters` block narrows which of a customer's services and rate cards are priced — by carrier, service, service type/class, or rate card. See [Filters](/guides/filters). Every response includes a `quoteId` per customer. Pass it to [Fetch a quote](/api-reference/fetch-quote) to retrieve it later. A **data** batch (up to 100 shipments) is written and answered in the same call — you get a verdict per shipment, no polling. A **file** is for bigger runs: it is queued and you get an import id to check. Either way, shipments are identified by your `shipmentId`, so re-sending one updates it rather than duplicating it. The API is server-to-server. Keep your API key secret and never expose it in a browser or mobile client. # Quickstart Source: https://apidocs.dyspach.com/quickstart Request your first quote in a few minutes. Create one in **Settings → API Keys** (see [Authentication](/authentication)). Use the customer's business **internal id** (e.g. `ACME`), not a UUID. Post an origin, destination, items, and the customer id(s). ## Request a quote ```bash cURL theme={null} curl https://api.dyspach.com/v1/org/quote \ -H "x-api-key: $DYSPACH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "general": { "customerIds": ["ACME"] }, "origin": { "postalCode": "2000", "countryCode": "AU" }, "destination": { "postalCode": "3000", "countryCode": "AU" }, "items": [ { "type": "Carton/Box", "quantity": 2, "length": 0.3, "width": 0.3, "height": 0.3, "weight": 10 } ] }' ``` ```js Node theme={null} const res = await fetch("https://api.dyspach.com/v1/org/quote", { method: "POST", headers: { "x-api-key": process.env.DYSPACH_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ general: { customerIds: ["ACME"] }, origin: { postalCode: "2000", countryCode: "AU" }, destination: { postalCode: "3000", countryCode: "AU" }, items: [ { type: "Carton/Box", quantity: 2, length: 0.3, width: 0.3, height: 0.3, weight: 10 }, ], }), }); const data = await res.json(); ``` ```python Python theme={null} import os, requests res = requests.post( "https://api.dyspach.com/v1/org/quote", headers={"x-api-key": os.environ["DYSPACH_API_KEY"], "Content-Type": "application/json"}, json={ "general": {"customerIds": ["ACME"]}, "origin": {"postalCode": "2000", "countryCode": "AU"}, "destination": {"postalCode": "3000", "countryCode": "AU"}, "items": [ {"type": "Carton/Box", "quantity": 2, "length": 0.3, "width": 0.3, "height": 0.3, "weight": 10} ], }, ) data = res.json() ``` ## Response Origin, destination and goods are shared; each customer gets a `results` entry with its priced `quotes` (one per eligible service — two shown here). ```json theme={null} { "origin": { "postalCode": "2000", "suburb": "BARANGAROO", "city": "Sydney", "state": "NSW", "stateName": "New South Wales", "countryCode": "AU" }, "destination": { "postalCode": "3000", "suburb": "MELBOURNE", "city": "Melbourne", "state": "VIC", "stateName": "Victoria", "countryCode": "AU" }, "goods": [ { "type": "Carton/Box", "quantity": 2, "length": 30, "width": 30, "height": 30, "weight": 10, "chargeableWeight": 20 } ], "results": [ { "customerId": "ACME", "quoteId": "K541037RNBUQ", "noQuoteMessage": "", "quotes": [ { "rateId": "7cd6c7cf-84af-419d-b34d-6d270cc809ee", "serviceName": "Falcon Freight", "serviceId": "FALCON", "rateCardName": "Falcon Standard", "rateCardId": "FALCON_STD", "carrierName": "Falcon Freight", "carrierId": "FALCON", "isSpotRate": false, "spotRateMessage": "", "amount": 10, "totalAmount": 19.8, "gstAmount": 1.8, "deliveryTime": "1 - 5 business day(s)", "minTransitTime": 1, "maxTransitTime": 5, "transitTimeUnit": "D", "originZoneId": "SYD", "destinationZoneId": "MEL", "originZoneDescription": "Sydney Metro", "destinationZoneDescription": "Melbourne Metro", "surcharges": [ { "name": "Remote Location", "description": "Remote Location", "key": "Remote Location", "amount": 8 } ] }, { "rateId": "ba9bc08c-0238-4c88-aff5-b4f26aaab790", "serviceName": "Zephyr Couriers", "serviceId": "ZEPHYR", "rateCardName": "Zephyr Express", "rateCardId": "ZEPHYR_EXP", "carrierName": "Zephyr Couriers", "carrierId": "ZEPHYR", "isSpotRate": false, "spotRateMessage": "", "amount": 9, "totalAmount": 11.09, "gstAmount": 1.01, "deliveryTime": "0 - 0 business day(s)", "minTransitTime": 0, "maxTransitTime": 0, "transitTimeUnit": "D", "originZoneId": "SYD", "destinationZoneId": "MEL", "originZoneDescription": "Sydney Metro", "destinationZoneDescription": "Melbourne Metro", "surcharges": [ { "name": "Fuel Surcharge", "description": "Fuel Surcharge", "key": "FUEL", "amount": 1.08 } ] } ] } ] } ``` `amount` is the freight sell price; `totalAmount` is the total including surcharges and GST. Every money field is a number. ## Quote for multiple customers Pass more than one id — you get a `results` entry per customer: ```json theme={null} { "general": { "customerIds": ["ACME", "GLOBEX"] }, "origin": { "...": "..." } } ``` ## Narrow the results Add a `filters` block to restrict which services or rate cards are priced: ```json theme={null} { "general": { "customerIds": ["ACME"] }, "origin": { "postalCode": "2000", "countryCode": "AU" }, "destination": { "postalCode": "3000", "countryCode": "AU" }, "items": [{ "type": "Carton/Box", "quantity": 2, "length": 0.3, "width": 0.3, "height": 0.3, "weight": 10 }], "filters": { "serviceIds": ["ZEPHYR", "FALCON"] } } ``` See [Filters](/guides/filters) for every dimension. ## Fetch a quote later Use the `quoteId` from any result: ```bash theme={null} curl https://api.dyspach.com/v1/org/quote/K541037RNBUQ \ -H "x-api-key: $DYSPACH_API_KEY" ```