Skip to content

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/api

All 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/json on every request, and Content-Type: application/json when you send a body. Without Accept: application/json, a validation failure is rendered as an HTTP redirect instead of JSON.

  • Verb — the shipping endpoints are POST even when they only read (get_order_details, get_shipping_status, pickup_locations). Four of them are GET and take their parameters as query string values. There are no path parameters and no resource URLs.

  • Credentials travel in the payload — license_key and domain are 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": ""
    }
    KeyTypeDescription
    dataobject/arrayThe payload. An empty JSON array ([]), not {}, when the endpoint has nothing to return — for example on every error response.
    messagestringHuman-readable prose, often the carrier's own text. Empty on many successes. Some messages are localised, so never branch on the string.
    successboolWhether the operation succeeded. This — not the HTTP status — is the authoritative signal.
    warningstringNon-fatal note. Always present; empty ("") on the v1 shipping endpoints.

    There is no code field to branch on. v1 predates the machine-readable error codes v2 introduced; a caller distinguishes failures by HTTP status plus success. See Errors.

  • success: false can arrive with HTTP 200 — some failure paths (notably a change_order_status that cannot find the shipment) return the envelope at the default 200. Always read success before reading data.

  • data.code is always a string — the response of every endpoint in the shipping group passes through a normalisation layer that casts data.code to 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_order response is cached for one hour under the license key plus order id. A repeat call within that window returns the original response verbatim with data.idempotent: true added, 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 its license_key.

  • Rate limit — 500 requests per minute, keyed on the caller's IP address (or the authenticated user when a token is sent). Exceeding it returns HTTP 429.


Authentication in one line ​

Every request to a shipping/* endpoint carries its credentials in the payload:

json
{ "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.

bash
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"
}'
js
// 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
<?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:

json
{
  "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:

json
{
  "data": [],
  "message": "Invalid License Key",
  "success": false,
  "warning": ""
}

Finding your way around ​

  • Authentication — the license_key / domain / token payload 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.