Introduction
Legacy API
This is the legacy v1 API. It is maintained for the existing WooCommerce, Shopify and Wix plugin integrations that already depend on it, and its request and response shapes will not change. New integrations should use API v2, which has header-based credentials, a versioned envelope with machine-readable error codes, idempotency keys, pagination and webhooks. Nothing on these v1 pages is being extended.
The ShipOS API v1 is the original shipment-creation surface. It is a flat, action-per-path RPC-style API: every call posts a JSON body that carries both its parameters and its credentials, and every response comes back in the same four-key envelope regardless of the operation.
Base URL
https://app.shipos.co.il/apiAll paths in this reference are relative to that base — the v1 routes are registered under the plain api prefix (POST /api/shipping/open_new_order, and so on). The v2 API lives under api/v2 and shares no code with v1.
Conventions
Format — JSON only. Send
Accept: application/jsonon every request, andContent-Type: application/jsonwhen you send a body. WithoutAccept: application/json, a validation failure is rendered as an HTTP redirect instead of JSON.Verb — the shipping endpoints are
POSTeven when they only read (get_order_details,get_shipping_status,pickup_locations). Four of them areGETand take their parameters as query string values. There are no path parameters and no resource URLs.Credentials travel in the payload —
license_keyanddomainare ordinary request fields, not headers. See Authentication.One envelope for everything — success and failure share the same four keys:
json{ "data": { "...": "..." }, "message": "", "success": true, "warning": "" }Key Type Description dataobject/array The payload. An empty JSON array ( []), not{}, when the endpoint has nothing to return — for example on every error response.messagestring Human-readable prose, often the carrier's own text. Empty on many successes. Some messages are localised, so never branch on the string. successbool Whether the operation succeeded. This — not the HTTP status — is the authoritative signal. warningstring Non-fatal note. Always present; empty ( "") on the v1 shipping endpoints.There is no
codefield to branch on. v1 predates the machine-readable error codes v2 introduced; a caller distinguishes failures by HTTP status plussuccess. See Errors.success: falsecan arrive with HTTP 200 — some failure paths (notably achange_order_statusthat cannot find the shipment) return the envelope at the default200. Always readsuccessbefore readingdata.data.codeis always a string — the response of every endpoint in the shipping group passes through a normalisation layer that castsdata.codeto a string, because carriers return the tracking identifier inconsistently (some as an integer). Deserialise it as a string.Replayed creations are flagged — a successful
open_new_orderresponse is cached for one hour under the license key plus order id. A repeat call within that window returns the original response verbatim withdata.idempotent: trueadded, rather than creating a second shipment. That flag is the only difference between the replay and the original.Identifiers — a shipment is addressed by its carrier tracking code, sent as
shipping_id. The carrier account is identified by itslicense_key.Rate limit — 500 requests per minute, keyed on the caller's IP address (or the authenticated user when a
tokenis sent). Exceeding it returns HTTP429.
Authentication in one line
Every request to a shipping/* endpoint carries its credentials in the payload:
{ "license_key": "{license_key}", "domain": "your-store.example.com" }Both fields are required. The full model — what is verified, the optional Sanctum token field, and the exact failure responses — is in Authentication.
Quick start
List the carrier pickup points available to a license. It is the cheapest call that exercises the whole path: credentials, license resolution and the envelope.
curl --location 'https://app.shipos.co.il/api/shipping/pickup_locations' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"license_key": "{license_key}",
"domain": "your-store.example.com"
}'// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://app.shipos.co.il/api/shipping/pickup_locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
license_key: process.env.SHIPOS_LICENSE_KEY,
domain: process.env.SHIPOS_DOMAIN,
}),
})
const body = await response.json()
if (!response.ok || !body.success) {
throw new Error(`ShipOS v1: ${body.message}`) // 403 on a failed license check
}
// Note the nesting: the array sits at data.locations.spots.spot_detail.
for (const location of body.data.locations.spots.spot_detail) {
console.log(location.name, location.city, location.n_code)
}<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->post('https://app.shipos.co.il/api/shipping/pickup_locations', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}
// Note the nesting: the array sits at data.locations.spots.spot_detail.
$points = $body['data']['locations']['spots']['spot_detail'];
logger()->info('Pickup locations', ['count' => count($points)]);A success returns the carrier's point list nested under data.locations.spots.spot_detail, not directly on data.locations. Every carrier's transformer wraps the array that way:
{
"data": {
"locations": {
"spots": {
"spot_detail": [
{
"name": "Example point",
"city": "תל אביב",
"street": "דיזנגוף",
"house": "50",
"n_code": "1234",
"street_code": "",
"latitude": "32.0809",
"longitude": "34.7736",
"remarks": ""
}
]
}
}
},
"message": "",
"success": true,
"warning": ""
}Absent values are empty strings rather than null. See Pickup locations for the per-carrier extras and for the surprising failure mode when a carrier has no pickup network.
If the license cannot be resolved the same call returns HTTP 403 and:
{
"data": [],
"message": "Invalid License Key",
"success": false,
"warning": ""
}Finding your way around
- Authentication — the
license_key/domain/tokenpayload model, what is actually verified, and the failure responses. - Errors — the envelope on the failure path, the HTTP statuses a caller must handle, and the three different shapes a validation error can take.
- Endpoints — the reference for every v1 route, with parameters and response shapes.
- API v2 — where new integrations should start.