Bulk shipping with batches
This tutorial imports a large set of orders as shipments in one call, polls the batch until it finishes, handles the items that failed, and pulls a CSV of the results. Reference pages: Batches, Exports, Shipments.
Batches vs sequential POST /shipments
Sequential POST /shipments | POST /batches | |
|---|---|---|
| Response | Synchronous 201 with the created shipment (or an error) per request | Immediate 202 with a batch to poll; shipments are created by queue workers |
| Volume | Fine for a handful of shipments | Up to 2000 items per request, processed in parallel chunks |
| Failure isolation | One request = one result | A malformed item becomes a failed item; it never rejects the rest of the batch |
| Licenses | One license per request | A default license for the batch, plus an optional per-item license_key override |
Each batch item is created through the same guarded path as a single POST /shipments (same validation rules, same duplicate-protection lock, same carrier translators), so behavior per item is identical — you just get the results asynchronously.
Use a batch when you are importing a CSV, syncing a night's orders, or migrating a store. Use single creates when the caller needs the tracking code in the same HTTP round-trip.
Step 1 — Create the batch
POST /batches accepts either envelope shape — {"shipments": [ ... ]} or a bare top-level JSON array (it is normalized to shipments internally). Each item is a full shipment body — the same {ship_data, order} payload as POST /shipments — and may carry its own license_key to target a different license you own (it must belong to your account, otherwise that item fails with forbidden; items without one use the batch's default license).
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": {
"contact_name": "Dana Levi",
"contact_phone": "0521234567",
"street": "Herzl",
"number": "10",
"city": "Tel Aviv",
"type": "1",
"return": "1",
"packages": 1
},
"order": {
"id": "1042",
"number": "1042",
"shipping": { "first_name": "Dana", "last_name": "Levi", "phone": "0521234567", "city": "Tel Aviv" }
}
},
{
"license_key": "{other_license_key}",
"ship_data": {
"contact_name": "Yossi Cohen",
"contact_phone": "0549876543",
"street": "Weizmann",
"number": "14",
"city": "Kfar Saba",
"type": "1",
"return": "1",
"packages": 2
},
"order": {
"id": "1043",
"number": "1043",
"shipping": { "first_name": "Yossi", "last_name": "Cohen", "phone": "0549876543", "city": "Kfar Saba" }
}
}
]
}'// Node.js 18+ / browsers — no dependencies
const res = 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: licenseKey,
shipments: shipmentPayloads, // up to 2000 {ship_data, order} items
}),
})
const { data: batch } = await res.json() // res.status === 202
const batchId = batch.id // poll this UUID<?php
// composer require guzzlehttp/guzzle
$client = new \GuzzleHttp\Client(['base_uri' => 'https://app.shipos.co.il/api/v2/']);
$response = $client->post('batches', [
'headers' => [
'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
'Accept' => 'application/json',
],
'json' => [
'license_key' => $licenseKey,
'shipments' => $shipmentPayloads, // up to 2000 {ship_data, order} items
],
]);
$batch = json_decode((string) $response->getBody(), true)['data'];
$batchId = $batch['id']; // poll this UUID<?php
use Illuminate\Support\Facades\Http;
$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' => $licenseKey,
'shipments' => $shipmentPayloads, // up to 2000 {ship_data, order} items
])
->throw()
->json('data');
$batchId = $batch['id']; // poll this UUID# pip install httpx
import os
import httpx
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": shipment_payloads, # up to 2000 {ship_data, order} items
},
)
response.raise_for_status()
batch = response.json()["data"] # response.status_code == 202
batch_id = batch["id"] # poll this UUIDpackage main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"license_key": os.Getenv("SHIPOS_LICENSE_KEY"),
"shipments": shipmentPayloads, // up to 2000 {ship_data, order} items
})
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()
var payload struct {
Data struct {
ID string `json:"id"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&payload)
fmt.Println("poll batch", payload.Data.ID) // res.StatusCode == 202
}// Java 17+ — java.net.http, no dependencies (serialize items with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CreateBatch {
public static void main(String[] args) throws Exception {
String shipments = shipmentPayloadsAsJson(); // up to 2000 {ship_data, order} items
String body = "{\"license_key\":\"" + System.getenv("SHIPOS_LICENSE_KEY")
+ "\",\"shipments\":" + shipments + "}";
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());
System.out.println(response.body()); // 202 {"data":{"id":"<uuid>", ...}}
}
}// .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 response = await http.PostAsJsonAsync("batches", new
{
license_key = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY"),
shipments = shipmentPayloads, // up to 2000 {ship_data, order} items
});
response.EnsureSuccessStatusCode(); // 202
var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
var batchId = payload!.RootElement.GetProperty("data").GetProperty("id").GetString();
Console.WriteLine($"poll batch {batchId}");require "net/http"
require "json"
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: ENV.fetch("SHIPOS_LICENSE_KEY"),
shipments: shipment_payloads, # up to 2000 {ship_data, order} items
)
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
batch_id = batch["id"] # poll this UUID// [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 shipment_payloads = json!([/* up to 2000 {ship_data, order} items */]);
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": std::env::var("SHIPOS_LICENSE_KEY")?,
"shipments": shipment_payloads,
}))
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("poll batch {}", payload["data"]["id"]); // 202
Ok(())
}Response 202 Accepted — nothing is created yet:
{
"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"
}
}Item validation is deferred
The create call only validates the envelope (1–2000 items). Each item's body is validated during processing — an invalid item shows up later as a failed item with code: "validation_failed", not as a 422 on the create call.
Step 2 — Poll the batch
Poll GET /batches/{uuid} until status is completed. Statuses:
| Status | Meaning |
|---|---|
queued | Created; no item resolved yet. |
processing | Some items resolved, others still pending. |
completed | Every item is either created or failed. |
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 = `https://app.shipos.co.il/api/v2/batches/${batchId}?license_key=${licenseKey}`
const headers = {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
Accept: 'application/json',
}
let batch
do {
await new Promise((resolve) => setTimeout(resolve, 3000))
const res = await fetch(url, { headers })
batch = (await res.json()).data
const { total, created, failed } = batch.summary
console.log(`${created + failed}/${total}`)
} while (batch.status !== 'completed')<?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';
do {
sleep(3);
$batch = json_decode((string) $client->get("batches/{$batchId}", [
'query' => ['license_key' => $licenseKey],
])->getBody(), true)['data'];
$summary = $batch['summary'];
echo $summary['created'] + $summary['failed'], '/', $summary['total'], PHP_EOL;
} while ($batch['status'] !== 'completed');<?php
use Illuminate\Support\Facades\Http;
$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';
do {
sleep(3);
$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' => $licenseKey,
])
->throw()
->json('data');
logger()->info($batch['status'], $batch['summary']);
} while ($batch['status'] !== 'completed');# pip install httpx
import os
import time
import httpx
batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"
client = httpx.Client(
base_url="https://app.shipos.co.il/api/v2",
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
},
)
while True:
time.sleep(3)
response = client.get(f"/batches/{batch_id}", params={"license_key": license_key})
response.raise_for_status()
batch = response.json()["data"]
summary = batch["summary"]
print(f"{summary['created'] + summary['failed']}/{summary['total']}")
if batch["status"] == "completed":
breakpackage main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type batchResponse struct {
Data struct {
Status string `json:"status"`
Summary map[string]int `json:"summary"`
} `json:"data"`
}
func main() {
url := "https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55" +
"?license_key=" + os.Getenv("SHIPOS_LICENSE_KEY")
for {
time.Sleep(3 * time.Second)
req, _ := http.NewRequest("GET", url, 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)
}
var batch batchResponse
json.NewDecoder(res.Body).Decode(&batch)
res.Body.Close()
s := batch.Data.Summary
fmt.Printf("%d/%d\n", s["created"]+s["failed"], s["total"])
if batch.Data.Status == "completed" {
return
}
}
}// 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 PollBatch {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/batches/"
+ "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55?license_key="
+ System.getenv("SHIPOS_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();
String body;
do {
Thread.sleep(3_000);
body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(body); // {"data":{"status":"...","summary":{...}}}
} while (!body.contains("\"status\":\"completed\""));
}
}// .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 licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");
var url = $"batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55?license_key={licenseKey}";
JsonElement batch;
do
{
await Task.Delay(3_000);
var payload = await http.GetFromJsonAsync<JsonDocument>(url);
batch = payload!.RootElement.GetProperty("data");
var summary = batch.GetProperty("summary");
Console.WriteLine($"{summary.GetProperty("created").GetInt32()
+ summary.GetProperty("failed").GetInt32()}/{summary.GetProperty("total").GetInt32()}");
} while (batch.GetProperty("status").GetString() != "completed");require "net/http"
require "json"
uri = URI("https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55")
uri.query = URI.encode_www_form(license_key: ENV.fetch("SHIPOS_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"
loop do
sleep 3
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
batch = JSON.parse(response.body).fetch("data")
summary = batch["summary"]
puts "#{summary["created"] + summary["failed"]}/#{summary["total"]}"
break if batch["status"] == "completed"
end// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let url = "https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55";
loop {
tokio::time::sleep(Duration::from_secs(3)).await;
let payload: Value = client
.get(url)
.query(&[("license_key", std::env::var("SHIPOS_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"];
println!("{} {}", batch["status"], batch["summary"]);
if batch["status"] == "completed" {
return Ok(());
}
}
}{
"data": {
"id": "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55",
"status": "completed",
"summary": {
"total": 2,
"created": 1,
"failed": 1,
"pending": 0
},
"created_at": "2026-07-29T08:00:00.000000Z",
"updated_at": "2026-07-29T08:00:06.000000Z"
}
}The summary counters update live, so you can show progress (created + failed out of total) while it runs. A simple poll loop with a few seconds between requests is enough; batches are processed in parallel 50-item chunks across queue workers.
Step 3 — Inspect the items
GET /batches/{uuid}/items pages through the per-item results (cursor pagination, per_page default 50, max 100), in the order you submitted them (position is the zero-based index). Item statuses are pending, created, or failed.
curl --location 'https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items?license_key={license_key}&per_page=100' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'// Node.js 18+ / browsers — follows the cursor until the last page
const batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55'
const headers = {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
Accept: 'application/json',
}
let url = `https://app.shipos.co.il/api/v2/batches/${batchId}/items?license_key=${licenseKey}&per_page=100`
const failedItems = []
while (url) {
const res = await fetch(url, { headers })
const { data, links } = await res.json()
failedItems.push(...data.filter((item) => item.status === 'failed'))
url = links.next
}
console.log(`${failedItems.length} failed items`)<?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';
$query = ['license_key' => $licenseKey, 'per_page' => 100];
$failedItems = [];
do {
$page = json_decode((string) $client->get("batches/{$batchId}/items", [
'query' => $query,
])->getBody(), true);
foreach ($page['data'] as $item) {
if ($item['status'] === 'failed') {
$failedItems[] = $item;
}
}
$query['cursor'] = $page['meta']['next_cursor'];
} while ($query['cursor'] !== null);
echo count($failedItems), ' failed items', PHP_EOL;<?php
use Illuminate\Support\Facades\Http;
$batchId = '9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55';
$query = ['license_key' => $licenseKey, 'per_page' => 100];
$failedItems = [];
do {
$page = 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", $query)
->throw()
->json();
$failedItems = array_merge(
$failedItems,
array_filter($page['data'], fn (array $item): bool => $item['status'] === 'failed'),
);
$query['cursor'] = $page['meta']['next_cursor'];
} while ($query['cursor'] !== null);
logger()->info(count($failedItems).' failed items');# pip install httpx
import os
import httpx
batch_id = "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55"
client = httpx.Client(
base_url="https://app.shipos.co.il/api/v2",
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
},
)
params = {"license_key": license_key, "per_page": 100}
failed_items = []
while True:
page = client.get(f"/batches/{batch_id}/items", params=params).raise_for_status().json()
failed_items += [item for item in page["data"] if item["status"] == "failed"]
cursor = page["meta"]["next_cursor"]
if cursor is None:
break
params["cursor"] = cursor
print(len(failed_items), "failed items")package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
type itemsPage struct {
Data []struct {
Position int `json:"position"`
Status string `json:"status"`
ShipmentID *string `json:"shipment_id"`
Error json.RawMessage `json:"error"`
} `json:"data"`
Meta struct {
NextCursor *string `json:"next_cursor"`
} `json:"meta"`
}
func main() {
// Repeat with &cursor=<meta.next_cursor> until next_cursor is null.
url := "https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items" +
"?license_key=" + os.Getenv("SHIPOS_LICENSE_KEY") + "&per_page=100"
req, _ := http.NewRequest("GET", url, 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 page itemsPage
json.NewDecoder(res.Body).Decode(&page)
for _, item := range page.Data {
if item.Status == "failed" {
fmt.Printf("item %d failed: %s\n", item.Position, item.Error)
}
}
}// Java 17+ — java.net.http (parse with Jackson/Gson, follow meta.next_cursor)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class BatchItems {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/batches/"
+ "9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items?license_key="
+ System.getenv("SHIPOS_LICENSE_KEY") + "&per_page=100"))
.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());
// {"data":[{"position":0,"status":"created","shipment_id":"..."}, ...],"meta":{...}}
System.out.println(response.body());
}
}// .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 licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");
var url = "batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items"
+ $"?license_key={licenseKey}&per_page=100";
// Repeat with &cursor={meta.next_cursor} until next_cursor is null.
var page = await http.GetFromJsonAsync<JsonDocument>(url)
?? throw new InvalidOperationException("Empty response");
foreach (var item in page.RootElement.GetProperty("data").EnumerateArray())
{
if (item.GetProperty("status").GetString() == "failed")
{
Console.WriteLine($"item {item.GetProperty("position").GetInt32()} failed: "
+ item.GetProperty("error").GetRawText());
}
}require "net/http"
require "json"
base = "https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items"
query = { license_key: ENV.fetch("SHIPOS_LICENSE_KEY"), per_page: 100 }
failed_items = []
loop do
uri = URI(base)
uri.query = URI.encode_www_form(query)
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) { |http| http.request(request) }
page = JSON.parse(response.body)
failed_items += page["data"].select { |item| item["status"] == "failed" }
break if page.dig("meta", "next_cursor").nil?
query[:cursor] = page.dig("meta", "next_cursor")
end
puts "#{failed_items.size} failed items"// [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 client = reqwest::Client::new();
let url = "https://app.shipos.co.il/api/v2/batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items";
let license_key = std::env::var("SHIPOS_LICENSE_KEY")?;
let mut cursor: Option<String> = None;
loop {
let mut query = vec![("license_key", license_key.clone()), ("per_page", "100".into())];
if let Some(c) = &cursor {
query.push(("cursor", c.clone()));
}
let page: Value = client
.get(url)
.query(&query)
.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 page["data"].as_array().unwrap_or(&vec![]) {
if item["status"] == "failed" {
println!("item {} failed: {}", item["position"], item["error"]);
}
}
cursor = page["meta"]["next_cursor"].as_str().map(str::to_string);
if cursor.is_none() {
return Ok(());
}
}
}{
"data": [
{
"id": 4101,
"position": 0,
"status": "created",
"shipment_id": "b7d3a1e0-9f2c-4a8b-8e11-6c5d4b3a2f10",
"payload": { "...": "the exact body you submitted for this item" },
"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 exact body you submitted for this item" },
"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": { "prev": null, "next": null, "first": null, "last": null },
"meta": { "path": "...", "per_page": 100, "next_cursor": null, "prev_cursor": null }
}A created item carries the new shipment's UUID in shipment_id — use it with GET /shipments/{shipment}, the label endpoints, and so on.
Handling failed items
Every failed item has an error object with a stable code plus a message and/or field-level errors:
error.code | Meaning | What to do |
|---|---|---|
validation_failed | The item's body failed the shipment validation rules; field errors included. | Fix the listed fields and resubmit that item. |
forbidden | The item's license_key does not belong to your account. | Correct the key (list yours via GET /licenses). |
carrier_error | The carrier rejected the shipment. | Fix the data the carrier complained about, or contact support. |
server_error | An unexpected error while processing the item. | Retry the item; contact support if it persists. |
To retry, collect the failed items' payload values, fix them, and submit them as a new batch (or as individual POST /shipments calls when only a few failed). Successfully created items are real shipments — do not resubmit those.
Step 4 — Export the results to CSV
Once the batch is done you can pull a CSV of your shipments. Exports are asynchronous too: queue one, poll it, download it.
Exports are license-wide
An export covers the license's shipments matching the filters (currently filter[active]), not one specific batch — the same rows you would see listing GET /shipments. Run it after your import to get a spreadsheet that includes everything the batch created.
Queue the export
POST /exports responds 202 with the export record. Only one export per license may be in flight — a second request while one is pending/processing returns 409 with code: "duplicate_request", so just keep polling the first.
curl --location 'https://app.shipos.co.il/api/v2/exports' \
--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}",
"filter": { "active": true }
}'// Node.js 18+ / browsers — no dependencies
const res = await fetch('https://app.shipos.co.il/api/v2/exports', {
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: licenseKey,
filter: { active: true },
}),
})
const { data: export_ } = await res.json() // res.status === 202
const exportId = export_.id // poll this integer id<?php
// composer require guzzlehttp/guzzle
$client = new \GuzzleHttp\Client(['base_uri' => 'https://app.shipos.co.il/api/v2/']);
$response = $client->post('exports', [
'headers' => [
'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
'Accept' => 'application/json',
],
'json' => [
'license_key' => $licenseKey,
'filter' => ['active' => true],
],
]);
$export = json_decode((string) $response->getBody(), true)['data'];
$exportId = $export['id']; // poll this integer id<?php
use Illuminate\Support\Facades\Http;
$export = 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/exports', [
'license_key' => $licenseKey,
'filter' => ['active' => true],
])
->throw()
->json('data');
$exportId = $export['id']; // poll this integer id# pip install httpx
import os
import httpx
response = httpx.post(
"https://app.shipos.co.il/api/v2/exports",
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,
"filter": {"active": True},
},
)
response.raise_for_status()
export = response.json()["data"] # response.status_code == 202
export_id = export["id"] # poll this integer idpackage main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"license_key": os.Getenv("SHIPOS_LICENSE_KEY"),
"filter": map[string]bool{"active": true},
})
req, _ := http.NewRequest("POST", "https://app.shipos.co.il/api/v2/exports", 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()
var payload struct {
Data struct {
ID int `json:"id"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&payload)
fmt.Println("poll export", payload.Data.ID) // res.StatusCode == 202
}// 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 QueueExport {
public static void main(String[] args) throws Exception {
String body = """
{"license_key": "%s", "filter": {"active": true}}
""".formatted(System.getenv("SHIPOS_LICENSE_KEY"));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/exports"))
.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());
System.out.println(response.body()); // 202 {"data":{"id":512, ...}}
}
}// .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 response = await http.PostAsJsonAsync("exports", new
{
license_key = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY"),
filter = new { active = true },
});
response.EnsureSuccessStatusCode(); // 202
var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
var exportId = payload!.RootElement.GetProperty("data").GetProperty("id").GetInt32();
Console.WriteLine($"poll export {exportId}");require "net/http"
require "json"
uri = URI("https://app.shipos.co.il/api/v2/exports")
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: ENV.fetch("SHIPOS_LICENSE_KEY"),
filter: { active: true },
)
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)
export = JSON.parse(response.body).fetch("data") # 202
export_id = export["id"] # poll this integer id// [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 payload: Value = reqwest::Client::new()
.post("https://app.shipos.co.il/api/v2/exports")
.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": std::env::var("SHIPOS_LICENSE_KEY")?,
"filter": { "active": true },
}))
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("poll export {}", payload["data"]["id"]); // 202
Ok(())
}Response 202:
{
"data": {
"id": 512,
"type": "shipments",
"status": "pending",
"platform": null,
"row_count": null,
"filename": null,
"error": null,
"download_url": null,
"created_at": "2026-07-29T08:10:00.000000Z",
"updated_at": "2026-07-29T08:10:00.000000Z"
}
}Poll until completed
GET /exports/{export} — the status moves pending → processing → completed (or failed, with the reason in error). When completed, download_url, filename, and row_count are populated:
curl --location 'https://app.shipos.co.il/api/v2/exports/512?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 exportId = 512
const url = `https://app.shipos.co.il/api/v2/exports/${exportId}?license_key=${licenseKey}`
const headers = {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
Accept: 'application/json',
}
let exportRecord
do {
await new Promise((resolve) => setTimeout(resolve, 3000))
const res = await fetch(url, { headers })
exportRecord = (await res.json()).data
console.log(exportRecord.status)
} while (!['completed', 'failed'].includes(exportRecord.status))
const downloadUrl = exportRecord.download_url // null when status is failed<?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',
],
]);
$exportId = 512;
do {
sleep(3);
$export = json_decode((string) $client->get("exports/{$exportId}", [
'query' => ['license_key' => $licenseKey],
])->getBody(), true)['data'];
echo $export['status'], PHP_EOL;
} while (! in_array($export['status'], ['completed', 'failed'], true));
$downloadUrl = $export['download_url']; // null when status is failed<?php
use Illuminate\Support\Facades\Http;
$exportId = 512;
do {
sleep(3);
$export = 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/exports/{$exportId}", [
'license_key' => $licenseKey,
])
->throw()
->json('data');
logger()->info($export['status']);
} while (! in_array($export['status'], ['completed', 'failed'], true));
$downloadUrl = $export['download_url']; // null when status is failed# pip install httpx
import os
import time
import httpx
export_id = 512
client = httpx.Client(
base_url="https://app.shipos.co.il/api/v2",
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
},
)
while True:
time.sleep(3)
response = client.get(f"/exports/{export_id}", params={"license_key": license_key})
response.raise_for_status()
export = response.json()["data"]
print(export["status"])
if export["status"] in ("completed", "failed"):
break
download_url = export["download_url"] # None when status is failedpackage main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type exportResponse struct {
Data struct {
Status string `json:"status"`
DownloadURL string `json:"download_url"`
} `json:"data"`
}
func main() {
url := "https://app.shipos.co.il/api/v2/exports/512" +
"?license_key=" + os.Getenv("SHIPOS_LICENSE_KEY")
for {
time.Sleep(3 * time.Second)
req, _ := http.NewRequest("GET", url, 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)
}
var export exportResponse
json.NewDecoder(res.Body).Decode(&export)
res.Body.Close()
fmt.Println(export.Data.Status)
if export.Data.Status == "completed" || export.Data.Status == "failed" {
fmt.Println(export.Data.DownloadURL) // empty when status is failed
return
}
}
}// 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 PollExport {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/exports/512"
+ "?license_key=" + System.getenv("SHIPOS_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();
String body;
do {
Thread.sleep(3_000);
body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(body); // {"data":{"status":"...","download_url":...}}
} while (!body.contains("\"status\":\"completed\"")
&& !body.contains("\"status\":\"failed\""));
}
}// .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 licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");
var url = $"exports/512?license_key={licenseKey}";
JsonElement export;
string? status;
do
{
await Task.Delay(3_000);
var payload = await http.GetFromJsonAsync<JsonDocument>(url);
export = payload!.RootElement.GetProperty("data");
status = export.GetProperty("status").GetString();
Console.WriteLine(status);
} while (status is not ("completed" or "failed"));
Console.WriteLine(export.GetProperty("download_url").GetString());require "net/http"
require "json"
uri = URI("https://app.shipos.co.il/api/v2/exports/512")
uri.query = URI.encode_www_form(license_key: ENV.fetch("SHIPOS_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"
loop do
sleep 3
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
export = JSON.parse(response.body).fetch("data")
puts export["status"]
if %w[completed failed].include?(export["status"])
puts export["download_url"] # nil when status is failed
break
end
end// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let url = "https://app.shipos.co.il/api/v2/exports/512";
loop {
tokio::time::sleep(Duration::from_secs(3)).await;
let payload: Value = client
.get(url)
.query(&[("license_key", std::env::var("SHIPOS_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 export = &payload["data"];
println!("{}", export["status"]);
if export["status"] == "completed" || export["status"] == "failed" {
println!("{}", export["download_url"]); // null when status is failed
return Ok(());
}
}
}{
"data": {
"id": 512,
"type": "shipments",
"status": "completed",
"platform": null,
"row_count": 1873,
"filename": "shipments-20260729081000.csv",
"error": null,
"download_url": "https://app.shipos.co.il/api/v2/exports/512/download",
"created_at": "2026-07-29T08:10:00.000000Z",
"updated_at": "2026-07-29T08:10:42.000000Z"
}
}Download the CSV
GET /exports/{export}/download streams the file. Before the export completes it returns 404 with code: "not_found" and message Export file is not ready. — poll status first.
curl --location 'https://app.shipos.co.il/api/v2/exports/512/download?license_key={license_key}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--output shipments.csv// Node.js 18+ — top-level await in an ESM module
import { writeFile } from 'node:fs/promises'
const exportId = 512
const res = await fetch(
`https://app.shipos.co.il/api/v2/exports/${exportId}/download?license_key=${licenseKey}`,
{
headers: {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
},
},
)
if (!res.ok) {
const { error } = await res.json()
throw new Error(`${error.code}: ${error.message}`) // not_found until it is ready
}
// Write the raw bytes — the UTF-8 BOM is part of the file.
await writeFile('shipments.csv', Buffer.from(await res.arrayBuffer()))<?php
// composer require guzzlehttp/guzzle
$client = new \GuzzleHttp\Client(['base_uri' => 'https://app.shipos.co.il/api/v2/']);
$exportId = 512;
// 'sink' streams straight to disk — the raw bytes keep the UTF-8 BOM intact.
$client->get("exports/{$exportId}/download", [
'headers' => [
'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
],
'query' => ['license_key' => $licenseKey],
'sink' => 'shipments.csv',
]);<?php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
$exportId = 512;
$response = Http::withHeaders([
'X-Client-Id' => config('services.shipos.client_id'),
'X-Client-Secret' => config('services.shipos.client_secret'),
])
->get("https://app.shipos.co.il/api/v2/exports/{$exportId}/download", [
'license_key' => $licenseKey,
])
->throw();
// Store the raw body — the UTF-8 BOM is part of the file.
Storage::put('shipments.csv', $response->body());# pip install httpx
import os
import httpx
export_id = 512
with httpx.stream(
"GET",
f"https://app.shipos.co.il/api/v2/exports/{export_id}/download",
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
},
params={"license_key": license_key},
) as response:
response.raise_for_status()
# Binary mode — the UTF-8 BOM is part of the file.
with open("shipments.csv", "wb") as file:
for chunk in response.iter_bytes():
file.write(chunk)package main
import (
"io"
"net/http"
"os"
)
func main() {
url := "https://app.shipos.co.il/api/v2/exports/512/download" +
"?license_key=" + os.Getenv("SHIPOS_LICENSE_KEY")
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
file, err := os.Create("shipments.csv")
if err != nil {
panic(err)
}
defer file.Close()
// Copy the raw bytes — the UTF-8 BOM is part of the file.
if _, err := io.Copy(file, res.Body); err != nil {
panic(err)
}
}// 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;
import java.nio.file.Path;
public class DownloadExport {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/exports/512/download"
+ "?license_key=" + System.getenv("SHIPOS_LICENSE_KEY")))
.header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
.header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
.build();
// ofFile writes the raw bytes — the UTF-8 BOM is part of the file.
HttpResponse<Path> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofFile(Path.of("shipments.csv")));
System.out.println(response.statusCode()); // 404 until the export is ready
}
}// .NET 8+
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 licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");
var response = await http.GetAsync($"exports/512/download?license_key={licenseKey}");
response.EnsureSuccessStatusCode(); // 404 until the export is ready
// Raw byte stream — the UTF-8 BOM is part of the file.
await using var file = File.Create("shipments.csv");
await response.Content.CopyToAsync(file);require "net/http"
uri = URI("https://app.shipos.co.il/api/v2/exports/512/download")
uri.query = URI.encode_www_form(license_key: ENV.fetch("SHIPOS_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")
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request) do |response|
raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
# Binary mode — the UTF-8 BOM is part of the file.
File.open("shipments.csv", "wb") do |file|
response.read_body { |chunk| file.write(chunk) }
end
end
end// [dependencies]
// reqwest = { version = "0.12" }
// tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = reqwest::Client::new()
.get("https://app.shipos.co.il/api/v2/exports/512/download")
.query(&[("license_key", std::env::var("SHIPOS_LICENSE_KEY")?)])
.header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
.header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
.send()
.await?
.error_for_status()? // 404 until the export is ready
.bytes()
.await?;
// Write the raw bytes — the UTF-8 BOM is part of the file.
tokio::fs::write("shipments.csv", &bytes).await?;
Ok(())
}The CSV columns are: shipping_code, short_tracking_code, type, status, is_active, first_name, last_name, phone, city, address_1, created_at. The file starts with a UTF-8 BOM so Hebrew renders correctly in Excel.
Quick error reference
| Status | code | Where | When |
|---|---|---|---|
| 403 | forbidden | all | No usable license (inactive/expired/not owned). |
| 404 | not_found | GET /batches/{uuid}, GET /exports/{export}, download | The resource does not belong to your license, or the export file is not ready yet. |
| 409 | duplicate_request | POST /exports | An export for this license is already being prepared — poll it instead. |
| 422 | validation_failed | POST /batches | shipments missing/empty or over 2000 items; or multiple active licenses and no license_key. |