Skip to content

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.

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 null is cleared, where the app lets an owner clear it (see the table below). Where it does not, the answer is 400 with Integration.FieldNotClearable and the field in args.field.
  • A field the request does not have (a typo) answers 400 with Integration.UnknownField; a read-only field such as externalReference or status answers 400 with Integration.FieldNotUpdatable. Nothing is ever silently ignored.
  • Text may not contain control characters (U+0000 and the like; tab, line feed and carriage return are fine) or a broken surrogate escape (such as "\ud800"): 400 with Integration.InvalidBody and the field in args.field, or Integration.MetadataInvalid and the key in args.key for a metadata value.
  • The body is at most 64 KB; a larger one answers 413 with Integration.BodyTooLarge.
Terminal window
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.

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.

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.

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.

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 Failed
Content-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-Match is 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 same ETag as the first one.
  • A 412 is never replayed: read the request again and retry with the fresh If-Match; the same X-Idempotency-Key is fine, and the retry really runs.

POST /shipment-requests/{id}/cancel, with an optional reason (at most 500 characters):

Terminal window
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-Match works 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>" }
}
  • request.updated tells the owner that the request changed: its listing (in the app or through the API) or its bookkeeping (through the API). data.changedFields lists the groups that moved (origin, destination, transportMode, modePolicy, cargo, schedule, incoterm, notes, purchaseOrderNumber, costCenter, metadata) and data.revision the listing’s revision. One PATCH raises exactly one request.updated, naming everything it moved; a patch that moves nothing raises none.
  • request.cancelled carries the owner’s reason (or null).

See Events.