Skip to content

Batches

Batches are asynchronous bulk shipment creation. You submit up to 2000 shipment payloads in a single request; the API persists them, queues the work across parallel workers, and immediately returns 202 Accepted with a batch you can poll. Each item is created through the same cache+lock-guarded path as a single POST /shipments, so a malformed item fails on its own rather than rejecting the whole batch.

All batch endpoints are license-scoped: pick the carrier account with license_key (query on GET, body on the create call) = the licenses.key value. A single active license is used by default; multiple active licenses without a license_key return 422; none/inactive/expired/not-owned return 403.

Lifecycle

A batch moves through these statuses:

StatusMeaning
queuedCreated; chunk jobs dispatched, no item resolved yet.
processingAt least one item resolved, but not all.
completedEvery item resolved (each is either created or failed).

Each item has its own status: pending (not yet processed), created (shipment made — see shipment_id), or failed (see error). Poll GET /batches/{uuid} for aggregate counts and GET /batches/{uuid}/items for per-item results.


GET /batches

List the caller's batches, newest first, each with its items eager-loaded. Cursor-paginated. Auth: client credentials. License: required.

Parameters

Query

FieldTypeRequiredDescription
license_keystringrequired*The licenses.key selecting the carrier account. Optional when the caller has exactly one active license.
per_pageintegernoItems per page. Default 25, capped at 100.

Example request

bash
curl --location 'https://app.shipos.co.il/api/v2/batches?license_key={license_key}&per_page=25' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const url = new URL('https://app.shipos.co.il/api/v2/batches')
url.searchParams.set('license_key', '{license_key}')
url.searchParams.set('per_page', '25')

const response = await fetch(url, {
  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: batches } = await response.json()

for (const batch of batches) {
  console.log(batch.id, batch.status, batch.summary.created, 'created')
}
php
<?php
// composer require guzzlehttp/guzzle

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

$response = $client->get('batches', [
    'query' => ['license_key' => '{license_key}', 'per_page' => 25],
]);

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

foreach ($batches as $batch) {
    echo $batch['id'], ' ', $batch['status'], ' ', $batch['summary']['created'], PHP_EOL;
}
php
<?php

use Illuminate\Support\Facades\Http;

$batches = 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/batches', [
        'license_key' => '{license_key}',
        'per_page' => 25,
    ])
    ->throw()
    ->json('data');

foreach ($batches as $batch) {
    logger()->info($batch['id'].' '.$batch['status'].' '.$batch['summary']['created']);
}
python
# pip install httpx
import os

import httpx

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

for batch in response.json()["data"]:
    print(batch["id"], batch["status"], batch["summary"]["created"])
go
package main

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

type batchList struct {
	Data []struct {
		ID      string `json:"id"`
		Status  string `json:"status"`
		Summary struct {
			Created int `json:"created"`
			Failed  int `json:"failed"`
		} `json:"summary"`
	} `json:"data"`
}

func main() {
	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/batches?license_key={license_key}&per_page=25", 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()

	var list batchList
	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
		panic(err)
	}

	for _, batch := range list.Data {
		fmt.Println(batch.ID, batch.Status, batch.Summary.Created, batch.Summary.Failed)
	}
}
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 ShipOsListBatches {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/batches"
                + "?license_key={license_key}&per_page=25"))
            .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":[...]} — map with Jackson/Gson
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var payload = await http.GetFromJsonAsync<JsonDocument>(
    "batches?license_key={license_key}&per_page=25")
    ?? throw new InvalidOperationException("Empty response");

foreach (var batch in payload.RootElement.GetProperty("data").EnumerateArray())
{
    Console.WriteLine($"{batch.GetProperty("id").GetString()} " +
        $"{batch.GetProperty("status").GetString()}");
}
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/batches")
uri.query = URI.encode_www_form(license_key: "{license_key}", per_page: 25)

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)

JSON.parse(response.body).fetch("data").each do |batch|
  puts "#{batch["id"]} #{batch["status"]} #{batch.dig("summary", "created")}"
end
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/batches")
        .query(&[("license_key", "{license_key}"), ("per_page", "25")])
        .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?;

    for batch in payload["data"].as_array().unwrap_or(&Vec::new()) {
        println!("{} {} {}", batch["id"], batch["status"], batch["summary"]["created"]);
    }
    Ok(())
}

Response 200

Each batch includes its items array (only present on this index endpoint, where the relation is eager-loaded). See the item shape under GET /batches/{uuid}/items.

json
{
  "data": [
    {
      "id": "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55",
      "status": "completed",
      "summary": {
        "total": 2,
        "created": 1,
        "failed": 1,
        "pending": 0
      },
      "items": [
        {
          "id": 4101,
          "position": 0,
          "status": "created",
          "shipment_id": "b7d3a1e0-9f2c-4a8b-8e11-6c5d4b3a2f10",
          "payload": { "...": "the item's submitted body" },
          "error": null,
          "created_at": "2026-07-29T08:00:00.000000Z",
          "updated_at": "2026-07-29T08:00:04.000000Z"
        }
      ],
      "created_at": "2026-07-29T08:00:00.000000Z",
      "updated_at": "2026-07-29T08:00:06.000000Z"
    }
  ],
  "links": { "...": "cursor pagination links" },
  "meta": { "...": "cursor pagination meta" }
}

Errors

StatuscodeWhen
403forbiddenNo usable license (inactive/expired/not owned).
422validation_failedMultiple active licenses and no license_key given.

POST /batches

Queue an async bulk-shipment batch. Auth: client credentials. License: required.

Returns 202 Accepted — the shipments are not created yet. Poll the returned batch id.

Parameters

Body

The request accepts either envelope shape, both equivalent:

  • { "shipments": [ <item>, <item>, ... ] }, or
  • a bare top-level JSON array [ <item>, <item>, ... ] (normalized under shipments).
FieldTypeRequiredDescription
shipmentsarrayyes1 to 2000 shipment payloads.
license_keystringrequired*Default license for every item. Body field. Optional when the caller has exactly one active license.

Each item (shipments[]) is a full shipment body — the same payload accepted by POST /shipments — and is validated per-item during processing (invalid items become failed, they do not reject the batch). Additionally:

Item fieldTypeRequiredDescription
license_keystringnoPer-item override selecting a different license (must belong to the same customer, or the item fails with forbidden). Falls back to the batch's default license when omitted.

Example request

bash
curl --location 'https://app.shipos.co.il/api/v2/batches' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "license_key": "{license_key}",
  "shipments": [
    { "ship_data": { "...": "shipment fields" }, "order": { "...": "order fields" } },
    { "ship_data": { "...": "shipment fields" }, "order": { "...": "order fields" }, "license_key": "{other_license_key}" }
  ]
}'
js
// Node.js 18+ / browsers — no dependencies
const item = {
  ship_data: { /* shipment fields — same as POST /shipments */ },
  order: { /* order fields */ },
}

const response = await fetch('https://app.shipos.co.il/api/v2/batches', {
  method: 'POST',
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    license_key: '{license_key}',
    shipments: [item, { ...item, license_key: '{other_license_key}' }],
  }),
})

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

const { data: batch } = await response.json() // 202 Accepted

console.log(batch.id, batch.status, batch.summary.pending)
php
<?php
// composer require guzzlehttp/guzzle

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

// Each item is a full POST /shipments body.
$item = ['ship_data' => [/* shipment fields */], 'order' => [/* order fields */]];

$response = $client->post('batches', [
    'json' => [
        'license_key' => '{license_key}',
        'shipments' => [
            $item,
            $item + ['license_key' => '{other_license_key}'],
        ],
    ],
]);

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

echo $batch['id'], ' ', $batch['status'], ' pending: ', $batch['summary']['pending'], PHP_EOL;
php
<?php

use Illuminate\Support\Facades\Http;

// Each item is a full POST /shipments body.
$item = ['ship_data' => [/* shipment fields */], 'order' => [/* order fields */]];

$batch = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->post('https://app.shipos.co.il/api/v2/batches', [
        'license_key' => '{license_key}',
        'shipments' => [$item, $item + ['license_key' => '{other_license_key}']],
    ])
    ->throw()
    ->json('data');

logger()->info("batch {$batch['id']} is {$batch['status']}");
python
# pip install httpx
import os

import httpx

item = {"ship_data": {}, "order": {}}  # a full POST /shipments body

response = httpx.post(
    "https://app.shipos.co.il/api/v2/batches",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={
        "license_key": "{license_key}",
        "shipments": [item, {**item, "license_key": "{other_license_key}"}],
    },
)
response.raise_for_status()
batch = response.json()["data"]  # 202 Accepted

print(batch["id"], batch["status"], batch["summary"]["pending"])
go
package main

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

func main() {
	// Each item is a full POST /shipments body.
	item := map[string]any{"ship_data": map[string]any{}, "order": map[string]any{}}
	override := map[string]any{"ship_data": map[string]any{}, "order": map[string]any{},
		"license_key": "{other_license_key}"}

	body, _ := json.Marshal(map[string]any{
		"license_key": "{license_key}",
		"shipments":   []any{item, override},
	})

	req, _ := http.NewRequest("POST", "https://app.shipos.co.il/api/v2/batches",
		bytes.NewReader(body))
	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")
	req.Header.Set("Content-Type", "application/json")

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

	out, _ := io.ReadAll(res.Body)
	fmt.Println(res.StatusCode, string(out)) // 202 {"data":{"status":"queued",...}}
}
java
// Java 17+ — java.net.http, no dependencies (build JSON with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsCreateBatch {
    public static void main(String[] args) throws Exception {
        // Each item is a full POST /shipments body.
        String body = """
            {
              "license_key": "{license_key}",
              "shipments": [
                {"ship_data": {}, "order": {}},
                {"ship_data": {}, "order": {}, "license_key": "{other_license_key}"}
              ]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/batches"))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

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

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

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

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

// Each item is a full POST /shipments body.
var response = await http.PostAsJsonAsync("batches", new
{
    license_key = "{license_key}",
    shipments = new object[]
    {
        new { ship_data = new { }, order = new { } },
        new { ship_data = new { }, order = new { }, license_key = "{other_license_key}" },
    },
});
response.EnsureSuccessStatusCode(); // 202 Accepted

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>()
    ?? throw new InvalidOperationException("Empty response");
var batch = payload.RootElement.GetProperty("data");

Console.WriteLine($"{batch.GetProperty("id").GetString()} " +
    $"{batch.GetProperty("status").GetString()}");
ruby
require "net/http"
require "json"

item = { ship_data: {}, order: {} } # a full POST /shipments body

uri = URI("https://app.shipos.co.il/api/v2/batches")
request = Net::HTTP::Post.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  license_key: "{license_key}",
  shipments: [item, item.merge(license_key: "{other_license_key}")],
})

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)

batch = JSON.parse(response.body).fetch("data") # 202 Accepted
puts "#{batch["id"]} #{batch["status"]} #{batch.dig("summary", "pending")}"
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let item = json!({ "ship_data": {}, "order": {} }); // a full POST /shipments body

    let payload: Value = reqwest::Client::new()
        .post("https://app.shipos.co.il/api/v2/batches")
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .json(&json!({
            "license_key": "{license_key}",
            "shipments": [
                item,
                { "ship_data": {}, "order": {}, "license_key": "{other_license_key}" }
            ],
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    let batch = &payload["data"];
    println!("{} {}", batch["id"], batch["status"]);
    Ok(())
}

Response 202

json
{
  "data": {
    "id": "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55",
    "status": "queued",
    "summary": {
      "total": 2,
      "created": 0,
      "failed": 0,
      "pending": 2
    },
    "created_at": "2026-07-29T08:00:00.000000Z",
    "updated_at": "2026-07-29T08:00:00.000000Z"
  }
}

items is omitted here (the relation is only eager-loaded on the index endpoint).

Errors

StatuscodeWhen
403forbiddenNo usable license (inactive/expired/not owned).
422validation_failedshipments missing/empty, more than 2000 items, or multiple active licenses and no license_key.

GET /batches/

Return a single batch owned by the caller's license — the primary polling endpoint for aggregate progress. Auth: client credentials. License: required.

Parameters

Path

FieldTypeRequiredDescription
uuidstringyesThe batch id (UUID) returned by create.

Query

FieldTypeRequiredDescription
license_keystringrequired*Selects the carrier account. Optional with a single active license.

Example request

bash
curl --location 'https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55?license_key={license_key}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55'
const url = new URL(`https://app.shipos.co.il/api/v2/batches/${batchId}`)
url.searchParams.set('license_key', '{license_key}')

const response = await fetch(url, {
  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: batch } = await response.json()

// Poll until batch.status === 'completed'.
console.log(batch.status, batch.summary.created, batch.summary.pending)
php
<?php
// composer require guzzlehttp/guzzle

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

$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';

$response = $client->get("batches/{$batchId}", [
    'query' => ['license_key' => '{license_key}'],
]);

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

// Poll until $batch['status'] === 'completed'.
echo $batch['status'], ' ', $batch['summary']['created'], '/', $batch['summary']['total'], PHP_EOL;
php
<?php

use Illuminate\Support\Facades\Http;

$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';

$batch = 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/batches/{$batchId}", [
        'license_key' => '{license_key}',
    ])
    ->throw()
    ->json('data');

// Poll until $batch['status'] === 'completed'.
logger()->info($batch['status'], $batch['summary']);
python
# pip install httpx
import os

import httpx

batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

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

# Poll until batch["status"] == "completed".
print(batch["status"], batch["summary"])
go
package main

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

type batchResponse struct {
	Data struct {
		ID      string `json:"id"`
		Status  string `json:"status"`
		Summary struct {
			Total   int `json:"total"`
			Created int `json:"created"`
			Failed  int `json:"failed"`
			Pending int `json:"pending"`
		} `json:"summary"`
	} `json:"data"`
}

func main() {
	batchID := "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/batches/"+batchID+"?license_key={license_key}", 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()

	var batch batchResponse
	if err := json.NewDecoder(res.Body).Decode(&batch); err != nil {
		panic(err)
	}

	// Poll until batch.Data.Status == "completed".
	fmt.Println(batch.Data.Status, batch.Data.Summary.Created, batch.Data.Summary.Pending)
}
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 ShipOsShowBatch {
    public static void main(String[] args) throws Exception {
        String batchId = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/batches/" + batchId
                + "?license_key={license_key}"))
            .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());
        }

        // Poll until data.status is "completed".
        System.out.println(response.body()); // {"data":{...}} — map with Jackson/Gson
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var batchId = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

var payload = await http.GetFromJsonAsync<JsonDocument>(
    $"batches/{batchId}?license_key={{license_key}}")
    ?? throw new InvalidOperationException("Empty response");
var batch = payload.RootElement.GetProperty("data");

// Poll until status is "completed".
Console.WriteLine($"{batch.GetProperty("status").GetString()} " +
    $"{batch.GetProperty("summary").GetProperty("pending").GetInt32()} pending");
ruby
require "net/http"
require "json"

batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

uri = URI("https://app.shipos.co.il/api/v2/batches/#{batch_id}")
uri.query = URI.encode_www_form(license_key: "{license_key}")

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)

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

# Poll until batch["status"] == "completed".
puts "#{batch["status"]} #{batch["summary"]}"
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 batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

    let payload: Value = reqwest::Client::new()
        .get(format!("https://app.shipos.co.il/api/v2/batches/{batch_id}"))
        .query(&[("license_key", "{license_key}")])
        .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 batch = &payload["data"];
    // Poll until batch["status"] == "completed".
    println!("{} {}", batch["status"], batch["summary"]);
    Ok(())
}

Response 200

json
{
  "data": {
    "id": "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55",
    "status": "processing",
    "summary": {
      "total": 2,
      "created": 1,
      "failed": 0,
      "pending": 1
    },
    "created_at": "2026-07-29T08:00:00.000000Z",
    "updated_at": "2026-07-29T08:00:04.000000Z"
  }
}

Response fields

FieldTypeDescription
idstringBatch UUID.
statusstringqueued, processing, or completed.
summary.totalintegerTotal items submitted.
summary.createdintegerItems that produced a shipment.
summary.failedintegerItems that failed.
summary.pendingintegerItems not yet resolved (total - created - failed, floored at 0).
itemsarrayOnly present on the index endpoint; omitted here.
created_at / updated_atstringISO-8601 timestamps.

Errors

StatuscodeWhen
403forbiddenNo usable license.
404not_foundNo batch with that UUID belongs to the caller's license (Batch not found.).
422validation_failedMultiple active licenses and no license_key.

GET /batches/{uuid}/items

Return the paginated per-item results of a batch. Auth: client credentials. License: required.

Parameters

Path

FieldTypeRequiredDescription
uuidstringyesThe batch id (UUID).

Query

FieldTypeRequiredDescription
license_keystringrequired*Selects the carrier account. Optional with a single active license.
per_pageintegernoItems per page. Default 50, capped at 100.

Example request

bash
curl --location 'https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items?license_key={license_key}&per_page=50' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55'
const url = new URL(`https://app.shipos.co.il/api/v2/batches/${batchId}/items`)
url.searchParams.set('license_key', '{license_key}')
url.searchParams.set('per_page', '50')

const response = await fetch(url, {
  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: items } = await response.json()

for (const item of items) {
  console.log(item.position, item.status, item.shipment_id ?? item.error?.code)
}
php
<?php
// composer require guzzlehttp/guzzle

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

$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';

$response = $client->get("batches/{$batchId}/items", [
    'query' => ['license_key' => '{license_key}', 'per_page' => 50],
]);

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

foreach ($items as $item) {
    echo $item['position'], ' ', $item['status'], ' ',
        $item['shipment_id'] ?? ($item['error']['code'] ?? ''), PHP_EOL;
}
php
<?php

use Illuminate\Support\Facades\Http;

$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';

$items = 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/batches/{$batchId}/items", [
        'license_key' => '{license_key}',
        'per_page' => 50,
    ])
    ->throw()
    ->json('data');

foreach ($items as $item) {
    logger()->info($item['position'].' '.$item['status'], ['error' => $item['error']]);
}
python
# pip install httpx
import os

import httpx

batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

response = httpx.get(
    f"https://app.shipos.co.il/api/v2/batches/{batch_id}/items",
    params={"license_key": "{license_key}", "per_page": 50},
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()

for item in response.json()["data"]:
    print(item["position"], item["status"], item["shipment_id"] or item["error"])
go
package main

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

type itemList struct {
	Data []struct {
		Position   int             `json:"position"`
		Status     string          `json:"status"`
		ShipmentID *string         `json:"shipment_id"`
		Error      json.RawMessage `json:"error"`
	} `json:"data"`
}

func main() {
	batchID := "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/batches/"+batchID+
			"/items?license_key={license_key}&per_page=50", 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()

	var list itemList
	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
		panic(err)
	}

	for _, item := range list.Data {
		fmt.Println(item.Position, item.Status, string(item.Error))
	}
}
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 ShipOsBatchItems {
    public static void main(String[] args) throws Exception {
        String batchId = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/batches/" + batchId
                + "/items?license_key={license_key}&per_page=50"))
            .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":[...]} — map with Jackson/Gson
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var batchId = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

var payload = await http.GetFromJsonAsync<JsonDocument>(
    $"batches/{batchId}/items?license_key={{license_key}}&per_page=50")
    ?? throw new InvalidOperationException("Empty response");

foreach (var item in payload.RootElement.GetProperty("data").EnumerateArray())
{
    Console.WriteLine($"{item.GetProperty("position").GetInt32()} " +
        $"{item.GetProperty("status").GetString()} {item.GetProperty("error")}");
}
ruby
require "net/http"
require "json"

batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"

uri = URI("https://app.shipos.co.il/api/v2/batches/#{batch_id}/items")
uri.query = URI.encode_www_form(license_key: "{license_key}", per_page: 50)

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)

JSON.parse(response.body).fetch("data").each do |item|
  puts "#{item["position"]} #{item["status"]} #{item["shipment_id"] || item["error"]}"
end
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 batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";

    let payload: Value = reqwest::Client::new()
        .get(format!("https://app.shipos.co.il/api/v2/batches/{batch_id}/items"))
        .query(&[("license_key", "{license_key}"), ("per_page", "50")])
        .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?;

    for item in payload["data"].as_array().unwrap_or(&Vec::new()) {
        println!("{} {} {}", item["position"], item["status"], item["error"]);
    }
    Ok(())
}

Response 200

json
{
  "data": [
    {
      "id": 4101,
      "position": 0,
      "status": "created",
      "shipment_id": "b7d3a1e0-9f2c-4a8b-8e11-6c5d4b3a2f10",
      "payload": { "...": "the item's submitted body" },
      "error": null,
      "created_at": "2026-07-29T08:00:00.000000Z",
      "updated_at": "2026-07-29T08:00:04.000000Z"
    },
    {
      "id": 4102,
      "position": 1,
      "status": "failed",
      "shipment_id": null,
      "payload": { "...": "the item's submitted body" },
      "error": {
        "code": "validation_failed",
        "ship_data.city": ["The ship_data.city field is required."]
      },
      "created_at": "2026-07-29T08:00:00.000000Z",
      "updated_at": "2026-07-29T08:00:05.000000Z"
    }
  ],
  "links": { "...": "cursor pagination links" },
  "meta": { "...": "cursor pagination meta" }
}

Response fields (per item)

FieldTypeDescription
idintegerBatch item id.
positionintegerZero-based index within the submitted batch.
statusstringpending, created, or failed.
shipment_idstring | nullUUID of the created shipment when status is created; otherwise null.
payloadobjectThe exact per-item body that was submitted.
errorobject | nullFailure detail when status is failed: always a code (e.g. validation_failed, forbidden, carrier_error, server_error) plus a message and/or field-level errors. null otherwise.
created_at / updated_atstringISO-8601 timestamps.

Errors

StatuscodeWhen
403forbiddenNo usable license.
404not_foundNo batch with that UUID belongs to the caller's license (Batch not found.).
422validation_failedMultiple active licenses and no license_key.