Skip to content

Errors ​

v1 has no dedicated error envelope. A failure comes back in the same four-key body as a success, with success flipped to false and the reason in message:

json
{
  "data": [],
  "message": "Invalid License Key",
  "success": false,
  "warning": ""
}
KeyTypeDescription
dataarray/objectAn empty JSON array ([]) on most failures. Some endpoints put detail here — see Validation errors and the change_order_status note below.
messagestringHuman-readable reason, frequently the carrier's own text, sometimes localised.
successboolfalse on failure. The authoritative signal.
warningstringAlways present, empty on the v1 shipping endpoints.

There is no machine-readable error code

v1 predates the stable code field that v2 errors carry. Nothing in a v1 failure is designed to be matched programmatically: message is prose and may change or arrive translated. Branch on the HTTP status plus success, and treat message as something to log or show to a human.

success: false does not imply a non-2xx status

Several v1 paths return a failure envelope at the default HTTP 200. The clearest case is change_order_status: when the tracking code cannot be found under the calling license it responds 200 with success: false and message: "Something went wrong". A client that only checks response.ok will read that as a successful cancellation. Always check success.


HTTP statuses to handle ​

StatusWhen it happensRetry?
200Success — and also some handled failures (see the warning above). Check success.—
400Rejected before the carrier was called: the account's shipment package limit is exhausted, the order's status is excluded from shipment creation by the license's order-status filter, the operation is not supported by this license's carrier, or a quote/express-delivery lookup failed.No — the condition is about the account or the request.
403License check failed. Unknown key, deactivated license, or premium gating. See Authentication.No.
404The addressed thing does not exist under the calling license. Two shapes: the envelope (order-label, e.g. Order not found, No active shippings found for this order) and Laravel's bare {"message": "..."} for the endpoints that fetch with firstOrFail (get_order_details, get_shipping_status, label).No.
409open_new_order only. Another request for the same license key and order id is mid-flight and held the lock for its full window. message asks the caller to retry.Yes, after a pause — but see the replay note below first.
422Request validation failed. See Validation errors.No, not unchanged.
429Rate limit: 500 requests per minute per IP (per user when a token is sent).Yes, with backoff.
500The shipment creation itself failed — a carrier rejection is reported as 500 with the carrier's text in message — or an unhandled server error, or Error Retrieving Locations from pickup_locations.Only for the unhandled/pickup_locations cases; a carrier rejection will fail identically until the payload changes.

Retrying open_new_order is safe for one hour ​

A successful open_new_order response is cached for an hour keyed on license_key + order.id, and a repeat call inside that window returns the cached response verbatim with data.idempotent: true added instead of creating a second shipment. Failures are not cached, so a failed creation stays retryable. This is why a 409 or a network timeout can be retried without producing a duplicate: whichever request finished first owns the tracking code, and the retry receives it.

Note that this dedup is time-boxed, not permanent — creating a second shipment for the same order is permitted once the hour has passed, because merchants legitimately ship the rest of an order later.


Validation errors ​

Which body a 422 carries depends on which endpoint you called, because v1 never unified this.

EndpointShapeStatus
open_new_orderThe v1 envelope, with the field errors under data.errors and the first message copied into message.422
get_order_details, get_shipping_status, change_order_status, label, order-label, bulk-label, same_day_delivery_quote, and the missing-license_key/domain case in the license middlewareLaravel's default validation body: {"message": ..., "errors": {...}}. No success key.422
order (bulk shipping lookup)A third shape: {"errors": {...}, "message": "Validation Failed", "success": false} — returned with HTTP 500, not 422.500

open_new_order, the endpoint most integrations care about, looks like this:

json
{
  "data": {
    "errors": {
      "ship_data.city": ["City is required"],
      "order_items.0.quantity": ["Each order item must have a quantity"]
    }
  },
  "message": "City is required",
  "success": false,
  "warning": ""
}

Its structural rules are that ship_data and order must both be arrays, and that any order_items entry needs a name, an integer quantity of at least 1, and a numeric total. On top of those it runs cross-field checks that accept the value from either location: street from ship_data.street or order.billing.address_1, city from ship_data.city or order.billing.city, and a contact name from ship_data.contact_name or order.billing.first_name/last_name. Only a genuinely empty value fails — "0" is a legitimate street value and is accepted. An empty ship_data: {} also fails, because Laravel's required treats an empty array as absent.

A single error body can mix two languages

Verified against a live instance: the framework's own messages are returned in the server's locale (Hebrew on the Israeli instances), while the rules declared by this endpoint carry English messages. One response can contain both:

json
{
  "data": {
    "errors": {
      "ship_data": ["שדה ship data הוא חובה."],
      "ship_data.city": ["City is required"]
    }
  },
  "message": "שדה ship data הוא חובה.",
  "success": false,
  "warning": ""
}

message is only ever a copy of the first error, so it inherits whichever language that one happened to be. Never branch on message text. Match on the HTTP status and on the field keys in data.errors, both of which are stable and locale-independent.

Always send Accept: application/json

Only open_new_order forces a JSON body on validation failure. The other endpoints rely on Laravel's content negotiation, so a request without Accept: application/json gets an HTTP redirect instead of a 422 body. Send the header on every call.


Two shapes that are not the envelope ​

Worth special-casing in a client, because neither carries success:

  • Bare {"message": ...} at 404 — the firstOrFail lookups listed in the status table.
  • Bare {"error": "Invalid response from shipping company"} at HTTP 200 — label and bulk-label when no label URL could be produced. A client that reads data.url without checking for error first will see an undefined URL rather than an error.

A defensive v1 client therefore treats a response as successful only when the HTTP status is 2xx and success === true and no top-level error key is present.


Where to look when a call fails ​

Every request through the shipping group is written to the api_request_logs table before it is dispatched — URL, headers, body, order number and tracking code — and the response is written back onto the same row, including the exact normalised body the caller received. Carrier-side calls are logged there too. When a merchant reports a shipment that did not appear, that row is the record of what was sent and what came back; quote the tracking code or order number when raising it with ShipOS support.