Skip to content

Introduction

The ShipOS API v2 is a self-contained, versioned REST API for creating and managing shipments across every carrier ShipOS supports. It is carrier-agnostic: the same request and response shapes work whether the underlying carrier is Cargo, HFD, Run Software, or any other — no carrier-specific fields appear in the contract.

Base URL

https://app.shipos.co.il/api/v2

All paths in this reference are relative to that base. The API is registered under the api/v2 prefix and is completely independent of the legacy v1 API.

Conventions

  • Format — JSON only. Send Accept: application/json, and Content-Type: application/json on requests with a body.

  • Success envelope — every resource response is wrapped in a top-level data key: an object for a single resource, an array for a collection.

    json
    { "data": { "id": 1, "...": "..." } }
  • Error envelope — failures return a top-level error object with a machine-readable code, a human message, and the HTTP status. See Errors.

    json
    { "error": { "code": "forbidden", "message": "This license is inactive.", "status": 403 } }
  • Identifiers — shipments are addressed by their uuid. A carrier account is identified by its license_key.

Authentication in one line

Send your client credentials on every non-public request:

X-Client-Id: {client_id}
X-Client-Secret: {client_secret}

Full details, including how to obtain and rotate credentials, are in Authentication. How to target a specific carrier account is covered in Licenses & license_key.

Quick start

Check the API is reachable (no auth required):

bash
curl --location 'https://app.shipos.co.il/api/v2/ping' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies, no credentials
const response = await fetch('https://app.shipos.co.il/api/v2/ping', {
  headers: { Accept: 'application/json' },
})

if (!response.ok) {
  throw new Error(`ShipOS unreachable: HTTP ${response.status}`)
}

console.log(await response.json())
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client();

$response = $client->get('https://app.shipos.co.il/api/v2/ping', [
    'headers' => ['Accept' => 'application/json'],
]);

print_r(json_decode($response->getBody()->getContents(), true));
php
<?php

use Illuminate\Support\Facades\Http;

$ping = Http::acceptJson()
    ->get('https://app.shipos.co.il/api/v2/ping')
    ->throw()
    ->json();

logger()->info('ShipOS ping', $ping);
python
# pip install httpx
import httpx

response = httpx.get(
    "https://app.shipos.co.il/api/v2/ping",
    headers={"Accept": "application/json"},
)
response.raise_for_status()

print(response.json())
go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.shipos.co.il/api/v2/ping", nil)
	req.Header.Set("Accept", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("ShipOS unreachable: HTTP %d", res.StatusCode))
	}

	body, _ := io.ReadAll(res.Body)
	fmt.Println(string(body))
}
java
// Java 17+ — java.net.http, no dependencies
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsPing {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/ping"))
            .header("Accept", "application/json")
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("ShipOS unreachable: HTTP " + response.statusCode());
        }

        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Accept", "application/json");

var ping = await http.GetFromJsonAsync<JsonDocument>(
        "https://app.shipos.co.il/api/v2/ping")
    ?? throw new InvalidOperationException("Empty response");

Console.WriteLine(ping.RootElement.ToString());
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/ping")
request = Net::HTTP::Get.new(uri)
request["Accept"] = "application/json"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS unreachable: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body)
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ping: Value = reqwest::Client::new()
        .get("https://app.shipos.co.il/api/v2/ping")
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{ping}");
    Ok(())
}

Then confirm your credentials by fetching your account:

bash
curl --location 'https://app.shipos.co.il/api/v2/account' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://app.shipos.co.il/api/v2/account', {
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    Accept: 'application/json',
  },
})

if (!response.ok) {
  const { error } = await response.json()
  throw new Error(`${error.code}: ${error.message}`)
}

const { data: account } = await response.json()

console.log(account.company_name, '—', account.licenses.length, 'license(s)')
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client();

$response = $client->get('https://app.shipos.co.il/api/v2/account', [
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

$account = json_decode($response->getBody()->getContents(), true)['data'];

echo $account['company_name'], ' — ', count($account['licenses']), ' license(s)', PHP_EOL;
php
<?php

use Illuminate\Support\Facades\Http;

$account = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->get('https://app.shipos.co.il/api/v2/account')
    ->throw()
    ->json('data');

logger()->info($account['company_name'].' — '.count($account['licenses']).' license(s)');
python
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://app.shipos.co.il/api/v2/account",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()
account = response.json()["data"]

print(account["company_name"], "—", len(account["licenses"]), "license(s)")
go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.shipos.co.il/api/v2/account", nil)
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("ShipOS error: HTTP %d", res.StatusCode))
	}

	var payload struct {
		Data struct {
			CompanyName string            `json:"company_name"`
			Licenses    []json.RawMessage `json:"licenses"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.CompanyName, "—", len(payload.Data.Licenses), "license(s)")
}
java
// Java 17+ — java.net.http, no dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsAccount {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/account"))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("ShipOS error: " + response.body());
        }

        System.out.println(response.body()); // {"data":{...}}
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));
http.DefaultRequestHeaders.Add("Accept", "application/json");

var payload = await http.GetFromJsonAsync<JsonDocument>(
        "https://app.shipos.co.il/api/v2/account")
    ?? throw new InvalidOperationException("Empty response");

var account = payload.RootElement.GetProperty("data");

Console.WriteLine($"{account.GetProperty("company_name").GetString()} — " +
    $"{account.GetProperty("licenses").GetArrayLength()} license(s)");
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/account")
request = Net::HTTP::Get.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

account = JSON.parse(response.body).fetch("data")

puts "#{account["company_name"]}#{account["licenses"].size} license(s)"
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let payload: Value = reqwest::Client::new()
        .get("https://app.shipos.co.il/api/v2/account")
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    let account = &payload["data"];
    let licenses = account["licenses"].as_array().map_or(0, Vec::len);
    println!("{} — {} license(s)", account["company_name"], licenses);
    Ok(())
}

From there, follow the step-by-step Create your first shipment walkthrough, or jump straight to the Shipments reference.

Finding your way around