Updating and cancelling
A request your systems created through the API can be changed and cancelled through the API by exactly the rules the app applies to its owner. The API is a door into the app’s own commands, not a second rulebook: what the app refuses, the API refuses, with the same code.
Both calls need the requests:write scope and an X-Idempotency-Key (see Idempotency). A request your company does not own answers 404.
Updating a request
Section titled “Updating a request”PATCH /shipment-requests/{id} takes a JSON merge patch (RFC 7396), sent as application/merge-patch+json or application/json:
- A field you leave out stays as it is.
- A field set to
nullis cleared, where the app lets an owner clear it (see the table below). Where it does not, the answer is400withIntegration.FieldNotClearableand the field inargs.field. - A field the request does not have (a typo) answers
400withIntegration.UnknownField; a read-only field such asexternalReferenceorstatusanswers400withIntegration.FieldNotUpdatable. Nothing is ever silently ignored. - Text may not contain control characters (
U+0000and the like; tab, line feed and carriage return are fine) or a broken surrogate escape (such as"\ud800"):400withIntegration.InvalidBodyand the field inargs.field, orIntegration.MetadataInvalidand the key inargs.keyfor a metadata value. - The body is at most 64 KB; a larger one answers
413withIntegration.BodyTooLarge.
curl -X PATCH "https://app.quatlasglobal.com/api/integrations/v1/shipment-requests/<request-id>" \ -H "Authorization: Bearer $QUATLAS_KEY" \ -H "Content-Type: application/merge-patch+json" \ -H "X-Idempotency-Key: EBA-2026-000123-update-1" \ -H 'If-Match: "<etag>"' \ -d '{ "cargo": { "weightKg": 9100, "packageCount": 14 }, "notes": null, "reason": "Two more pallets", "metadata": { "costCenter": "LOJ-215" } }'The answer is 200 with the same body as GET /shipment-requests/{id}, and the request’s new ETag.
What can change, and when
Section titled “What can change, and when”The request’s own fields (its listing) change only while it is Published, as in the app: a draft is edited in the app before it is published, a closed listing is reopened in the app first, and an awarded or cancelled request is settled. Your own bookkeeping changes in any state.
| State | Listing fields | metadata, purchaseOrderNumber, costCenter |
|---|---|---|
| Draft | 409 ShipmentRequest.InvalidTransition |
Yes |
| Published, no offers | Yes | Yes |
| Published, with offers | Yes; a change that affects the price holds every offer for its carrier’s reconfirmation | Yes |
| BiddingClosed | 409 ShipmentRequest.InvalidTransition |
Yes |
| Awarded | 409 ShipmentRequest.InvalidTransition |
Yes |
| Cancelled | 409 ShipmentRequest.InvalidTransition |
Yes |
A patch is written in one transaction, or not at all: when any part of it is refused (the listing change in the wrong state, a metadata limit, a stale If-Match), nothing of it lands, the bookkeeping included.
Offers on a published request
Section titled “Offers on a published request”A change to the route, the mode, the cargo, the schedule or the Incoterm moves the price, so every standing offer is held until its carrier reconfirms it, re-prices it or withdraws it; you cannot accept it in the meantime. The bidding stays open for at least 12 more hours, so the carriers can answer. A change of the notes alone is not material. reason (optional, at most 500 characters) is shown to the carriers asked to look again; it is not stored on the request’s fields. The carriers hear of the change only once every part of your update is written; when a change that moves the price cannot reach them, nothing is applied (503, ShipmentRequest.AmendmentNotDelivered) and you can retry.
A patch that matches the listing in every field changes nothing and cuts no new revision. revision in the body counts the listing’s changes, in the app or through the API.
The fields, and what null does
Section titled “The fields, and what null does”| Field | null |
|---|---|
origin, destination (the objects) |
Refused. Send the fields inside that change. |
origin.countryCode, destination.countryCode |
Refused. |
city, locationType, unLocode, addressLine, postalCode, contactName, contactPhone, handling (in origin or destination) |
Clears it. |
windowFrom, windowTo |
Refused. |
transportMode |
Refused: send "modePolicy": "Open" to leave the mode open. |
modePolicy |
Refused. Open clears the mode; Required needs one. |
cargo (the object), cargo.description |
Refused. |
cargo.standardType, cargo.dimensions |
Clears it. |
cargo.maxStackLayers, cargo.maxTopLoadKg |
Clears both stacking limits: the app keeps or drops them together. |
cargo.weightKg, volumeM3, packageCount, temperatureMinC, temperatureMaxC |
Refused. |
cargo.hazardous, stackable, fragile, exceedsStandard, outOfGauge |
Refused: send true or false. |
preferredPickupDate, preferredDeliveryDate, preferredDeliveryTime, isFlexibleSchedule |
Refused. |
incoterm |
Refused. |
notes |
Clears it (as does ""). |
purchaseOrderNumber, costCenter |
Clears it. |
metadata |
Removes every key. See Metadata. |
reason |
The same as leaving it out. |
Field values follow the create’s rules and words (transportMode Road, Sea, …; incoterm FCA, …). A field that breaks a rule answers 422 with errors keyed by its path, for example cargo.description.
Optimistic concurrency: ETag and If-Match
Section titled “Optimistic concurrency: ETag and If-Match”Every read of a request (GET /shipment-requests/{id}, the find by externalReference, and the answers of update and cancel) carries an ETag header: the request’s version. Send it back as If-Match on an update or a cancel, and the write is refused with 412 if the request changed since you read it, so you never overwrite a change made in the app in between.
HTTP/1.1 412 Precondition FailedContent-Type: application/problem+json
{ "title": "Integration.PreconditionFailed", "status": 412, "detail": "The request changed since you read it: If-Match does not match its current ETag. Read it again, then retry.", "code": "Integration.PreconditionFailed"}If-Matchis optional. Without it, the write goes through.- The tag is compared with the request as it stands under its lock, in the same transaction as the write: two writers sending the same tag at the same moment cannot both win; the second gets
412. - The version moves when the request’s status, its listing, its cancellation or your bookkeeping changes. It does not move when an offer arrives: the market is not the request.
If-Match: *matches any request. A weak tag (W/"...") never matches.- A replayed answer (same
X-Idempotency-Key) carries the sameETagas the first one. - A
412is never replayed: read the request again and retry with the freshIf-Match; the sameX-Idempotency-Keyis fine, and the retry really runs.
Cancelling a request
Section titled “Cancelling a request”POST /shipment-requests/{id}/cancel, with an optional reason (at most 500 characters):
curl "https://app.quatlasglobal.com/api/integrations/v1/shipment-requests/<request-id>/cancel" \ -H "Authorization: Bearer $QUATLAS_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: EBA-2026-000123-cancel" \ -d '{ "reason": "Cancelled in the eBA process" }'- It works on a request created through the API, where the app lets an owner cancel: a Draft, Published or BiddingClosed request. Once the cancellation is saved, the pending offers expire and their carriers are told, as in the app (and, should that step be interrupted, shortly after: it is retried until it is done, once); a cancel that fails tells nobody.
- Cancelling a cancelled request answers it again (
200), with its first cancellation time and reason. - The answer is the read’s body and the new
ETag.If-Matchworks as on an update.
An awarded request is a deal. Withdrawing from a deal in the closing room, or cancelling its operation once it is live, are separate flows that stay in the app. The API answers 409 and names the deal:
{ "title": "Integration.RequestAwarded", "status": 409, "detail": "This request was awarded and is a deal now. The API does not cancel deals: withdrawing a deal in the closing room, or cancelling its operation once it is live, is done in the app.", "code": "Integration.RequestAwarded", "dealId": "<deal-id>", "args": { "dealId": "<deal-id>" }}Webhooks
Section titled “Webhooks”request.updatedtells the owner that the request changed: its listing (in the app or through the API) or its bookkeeping (through the API).data.changedFieldslists the groups that moved (origin,destination,transportMode,modePolicy,cargo,schedule,incoterm,notes,purchaseOrderNumber,costCenter,metadata) anddata.revisionthe listing’s revision. OnePATCHraises exactly onerequest.updated, naming everything it moved; a patch that moves nothing raises none.request.cancelledcarries the owner’sreason(ornull).
See Events.
