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:
| Status | Meaning |
|---|---|
queued | Created; chunk jobs dispatched, no item resolved yet. |
processing | At least one item resolved, but not all. |
completed | Every 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
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | required* | The licenses.key selecting the carrier account. Optional when the caller has exactly one active license. |
per_page | integer | no | Items per page. Default 25, capped at 100. |
Example request
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'// 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
// 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
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']);
}# 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"])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 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
}
}// .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()}");
}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// [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.
{
"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
| Status | code | When |
|---|---|---|
| 403 | forbidden | No usable license (inactive/expired/not owned). |
| 422 | validation_failed | Multiple 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 undershipments).
| Field | Type | Required | Description |
|---|---|---|---|
shipments | array | yes | 1 to 2000 shipment payloads. |
license_key | string | required* | 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 field | Type | Required | Description |
|---|---|---|---|
license_key | string | no | Per-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
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}" }
]
}'// 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
// 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
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']}");# 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"])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 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",...}}
}
}// .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()}");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")}"// [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
{
"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
| Status | code | When |
|---|---|---|
| 403 | forbidden | No usable license (inactive/expired/not owned). |
| 422 | validation_failed | shipments 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
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | The batch id (UUID) returned by create. |
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | required* | Selects the carrier account. Optional with a single active license. |
Example request
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'// 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
// 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
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']);# 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"])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 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
}
}// .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");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"]}"// [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
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Batch UUID. |
status | string | queued, processing, or completed. |
summary.total | integer | Total items submitted. |
summary.created | integer | Items that produced a shipment. |
summary.failed | integer | Items that failed. |
summary.pending | integer | Items not yet resolved (total - created - failed, floored at 0). |
items | array | Only present on the index endpoint; omitted here. |
created_at / updated_at | string | ISO-8601 timestamps. |
Errors
| Status | code | When |
|---|---|---|
| 403 | forbidden | No usable license. |
| 404 | not_found | No batch with that UUID belongs to the caller's license (Batch not found.). |
| 422 | validation_failed | Multiple 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
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | The batch id (UUID). |
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | required* | Selects the carrier account. Optional with a single active license. |
per_page | integer | no | Items per page. Default 50, capped at 100. |
Example request
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'// 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
// 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
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']]);
}# 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"])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 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
}
}// .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")}");
}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// [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
{
"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)
| Field | Type | Description |
|---|---|---|
id | integer | Batch item id. |
position | integer | Zero-based index within the submitted batch. |
status | string | pending, created, or failed. |
shipment_id | string | null | UUID of the created shipment when status is created; otherwise null. |
payload | object | The exact per-item body that was submitted. |
error | object | null | Failure 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_at | string | ISO-8601 timestamps. |
Errors
| Status | code | When |
|---|---|---|
| 403 | forbidden | No usable license. |
| 404 | not_found | No batch with that UUID belongs to the caller's license (Batch not found.). |
| 422 | validation_failed | Multiple active licenses and no license_key. |