Skip to content

משלוחים בכמויות גדולות עם אצוות (Batches) ​

מדריך זה מייבא סט גדול של הזמנות כמשלוחים בקריאה אחת, מבצע polling לאצווה עד שהיא מסתיימת, מטפל בפריטים שנכשלו, ומושך CSV של התוצאות. עמודי ייחוס: Batches, Exports, Shipments.

אצוות לעומת POST /shipments סדרתי ​

POST /shipments סדרתיPOST /batches
תגובה201 סינכרוני עם המשלוח שנוצר (או שגיאה) לכל בקשה202 מיידי עם אצווה ל-polling; המשלוחים נוצרים על ידי queue workers
נפחמתאים לקומץ משלוחיםעד 2000 פריטים לבקשה, מעובדים במקטעים מקביליים
בידוד כשליםבקשה אחת = תוצאה אחתפריט שגוי הופך לפריט failed; הוא לעולם לא דוחה את שאר האצווה
רישיונותרישיון אחד לבקשהרישיון ברירת מחדל לאצווה, בתוספת דריסה אופציונלית של license_key לכל פריט

כל פריט באצווה נוצר דרך אותו נתיב מוגן כמו POST /shipments בודד (אותם כללי ולידציה, אותו מנעול הגנה מפני כפילויות, אותם מתרגמי חברות שילוח), כך שההתנהגות לכל פריט זהה — פשוט מקבלים את התוצאות באופן אסינכרוני.

השתמשו באצווה כשאתם מייבאים CSV, מסנכרנים את הזמנות הלילה, או מבצעים מיגרציה לחנות. השתמשו ביצירות בודדות כשהקורא זקוק לקוד המעקב באותו סבב HTTP.

שלב 1 — יצירת האצווה ​

POST /batches מקבל כל אחת משתי צורות המעטפת — {"shipments": [ ... ]} או מערך JSON חשוף ברמה העליונה (הוא מנורמל פנימית ל-shipments). כל פריט הוא גוף משלוח מלא — אותו payload של {ship_data, order} כמו ב-POST /shipments — והוא רשאי לשאת license_key משלו כדי לכוון לרישיון אחר שבבעלותכם (הוא חייב להשתייך לחשבון שלכם, אחרת הפריט הזה נכשל עם forbidden; פריטים בלעדיו משתמשים ברישיון ברירת המחדל של האצווה).

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": {
        "contact_name": "דנה לוי",
        "contact_phone": "0521234567",
        "street": "הרצל",
        "number": "10",
        "city": "תל אביב",
        "type": "1",
        "return": "1",
        "packages": 1
      },
      "order": {
        "id": "1042",
        "number": "1042",
        "shipping": { "first_name": "דנה", "last_name": "לוי", "phone": "0521234567", "city": "תל אביב" }
      }
    },
    {
      "license_key": "{other_license_key}",
      "ship_data": {
        "contact_name": "יוסי כהן",
        "contact_phone": "0549876543",
        "street": "ויצמן",
        "number": "14",
        "city": "כפר סבא",
        "type": "1",
        "return": "1",
        "packages": 2
      },
      "order": {
        "id": "1043",
        "number": "1043",
        "shipping": { "first_name": "יוסי", "last_name": "כהן", "phone": "0549876543", "city": "כפר סבא" }
      }
    }
  ]
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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, // עד 2000 פריטי {ship_data, order}
  }),
})

const { data: batch } = await res.json() // res.status === 202
const batchId = batch.id // בצעו polling ל-UUID הזה
php
<?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, // עד 2000 פריטי {ship_data, order}
    ],
]);

$batch = json_decode((string) $response->getBody(), true)['data'];
$batchId = $batch['id']; // בצעו polling ל-UUID הזה
php
<?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, // עד 2000 פריטי {ship_data, order}
    ])
    ->throw()
    ->json('data');

$batchId = $batch['id']; // בצעו polling ל-UUID הזה
python
# 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,  # עד 2000 פריטי {ship_data, order}
    },
)
response.raise_for_status()

batch = response.json()["data"]  # response.status_code == 202
batch_id = batch["id"]  # בצעו polling ל-UUID הזה
go
package 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, // עד 2000 פריטי {ship_data, order}
	})

	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
// Java 17+ — java.net.http, ללא תלויות (סריאליזציה של הפריטים עם 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(); // עד 2000 פריטי {ship_data, order}
        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>", ...}}
    }
}
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 response = await http.PostAsJsonAsync("batches", new
{
    license_key = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY"),
    shipments = shipmentPayloads, // עד 2000 פריטי {ship_data, order}
});
response.EnsureSuccessStatusCode(); // 202

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
var batchId = payload!.RootElement.GetProperty("data").GetProperty("id").GetString();
Console.WriteLine($"poll batch {batchId}");
ruby
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, # עד 2000 פריטי {ship_data, order}
)

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"] # בצעו polling ל-UUID הזה
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 shipment_payloads = json!([/* עד 2000 פריטי {ship_data, order} */]);

    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(())
}

תגובת 202 Accepted — עדיין לא נוצר דבר:

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"
  }
}

ולידציית פריטים נדחית לשלב העיבוד

קריאת היצירה מאמתת רק את המעטפת (1–2000 פריטים). גוף כל פריט מאומת במהלך העיבוד — פריט לא תקין יופיע מאוחר יותר כפריט failed עם code: "validation_failed", לא כ-422 בקריאת היצירה.

שלב 2 — Polling לאצווה ​

בצעו polling ל-GET /batches/{uuid} עד ש-status הוא completed. סטטוסים:

סטטוסמשמעות
queuedנוצרה; אף פריט טרם הוכרע.
processingחלק מהפריטים הוכרעו, אחרים עדיין ממתינים.
completedכל פריט הוא created או failed.
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+ / דפדפנים — ללא תלויות
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
<?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
<?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');
python
# 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":
        break
go
package 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
// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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\""));
    }
}
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 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");
ruby
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
rust
// [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(());
        }
    }
}
json
{
  "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"
  }
}

מוני ה-summary מתעדכנים בזמן אמת, כך שאפשר להציג התקדמות (created + failed מתוך total) בזמן הריצה. לולאת polling פשוטה עם כמה שניות בין בקשות מספיקה; אצוות מעובדות במקביל במקטעים של 50 פריטים על פני queue workers.

שלב 3 — בחינת הפריטים ​

GET /batches/{uuid}/items מדפדף בתוצאות ברמת הפריט (עימוד cursor, ברירת מחדל של per_page היא 50, מקסימום 100), בסדר שבו שלחתם אותם (position הוא האינדקס מבוסס-האפס). סטטוסי פריט הם pending, created או failed.

bash
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'
js
// Node.js 18+ / דפדפנים — עוקב אחר ה-cursor עד העמוד האחרון
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
<?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
<?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');
python
# 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")
go
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() {
	// חזרו עם &cursor=<meta.next_cursor> עד ש-next_cursor הוא 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
// Java 17+ — java.net.http (פענוח עם Jackson/Gson, עקבו אחר 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());
    }
}
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 licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");
var url = "batches/9b1f7c2e-3a4d-4f18-9c7a-2b6e1d0f8a55/items"
    + $"?license_key={licenseKey}&per_page=100";

// חזרו עם &cursor={meta.next_cursor} עד ש-next_cursor הוא 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());
    }
}
ruby
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"
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 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(());
        }
    }
}
json
{
  "data": [
    {
      "id": 4101,
      "position": 0,
      "status": "created",
      "shipment_id": "b7d3a1e0-9f2c-4a8b-8e11-6c5d4b3a2f10",
      "payload": { "...": "הגוף המדויק ששלחתם עבור פריט זה" },
      "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": { "...": "הגוף המדויק ששלחתם עבור פריט זה" },
      "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 }
}

פריט created נושא את ה-UUID של המשלוח החדש ב-shipment_id — השתמשו בו עם GET /shipments/{shipment}, עם endpoints של תוויות המשלוח, וכן הלאה.

טיפול בפריטים שנכשלו ​

לכל פריט failed יש אובייקט error עם code יציב בתוספת message ו/או שגיאות ברמת השדה:

error.codeמשמעותמה לעשות
validation_failedגוף הפריט נכשל בכללי הוולידציה של משלוח; שגיאות השדות מצורפות.תקנו את השדות המפורטים ושלחו את הפריט מחדש.
forbiddenה-license_key של הפריט אינו שייך לחשבון שלכם.תקנו את המפתח (רשימת הרישיונות שלכם דרך GET /licenses).
carrier_errorחברת השילוח דחתה את המשלוח.תקנו את הנתונים שחברת השילוח התלוננה עליהם, או פנו לתמיכה.
server_errorשגיאה בלתי צפויה בעת עיבוד הפריט.נסו שוב את הפריט; פנו לתמיכה אם זה נמשך.

כדי לנסות שוב, אספו את ערכי ה-payload של הפריטים שנכשלו, תקנו אותם, ושלחו אותם כאצווה חדשה (או כקריאות POST /shipments בודדות כשרק מעטים נכשלו). פריטים שנוצרו בהצלחה הם משלוחים אמיתיים — אל תשלחו אותם מחדש.

שלב 4 — ייצוא התוצאות ל-CSV ​

לאחר שהאצווה הסתיימה אפשר למשוך CSV של המשלוחים שלכם. גם ייצואים הם אסינכרוניים: מכניסים אחד לתור, מבצעים לו polling, ומורידים אותו.

ייצואים חלים על כל הרישיון

ייצוא מכסה את משלוחי הרישיון התואמים לסינונים (נכון לעכשיו filter[active]), לא אצווה ספציפית אחת — אותן שורות שהייתם רואים ברשימת GET /shipments. הריצו אותו אחרי הייבוא שלכם כדי לקבל גיליון שכולל את כל מה שהאצווה יצרה.

הכנסת הייצוא לתור ​

POST /exports מגיב 202 עם רשומת הייצוא. רק ייצוא אחד לרישיון רשאי להיות בתהליך — בקשה שנייה בזמן שאחד הוא pending/processing מחזירה 409 עם code: "duplicate_request", אז פשוט המשיכו לבצע polling לראשון.

bash
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 }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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 // בצעו polling ל-id המספרי הזה
php
<?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']; // בצעו polling ל-id המספרי הזה
php
<?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']; // בצעו polling ל-id המספרי הזה
python
# 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"]  # בצעו polling ל-id המספרי הזה
go
package 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
// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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, ...}}
    }
}
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 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}");
ruby
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"] # בצעו polling ל-id המספרי הזה
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 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(())
}

תגובת 202:

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

Polling עד לסיום ​

GET /exports/{export} — הסטטוס נע pending ← processing ← completed (או failed, עם הסיבה ב-error). בסיום, download_url, filename ו-row_count מאוכלסים:

bash
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'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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 כאשר הסטטוס הוא failed
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',
    ],
]);

$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 כאשר הסטטוס הוא failed
php
<?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 כאשר הסטטוס הוא failed
python
# 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 כאשר הסטטוס הוא failed
go
package 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) // ריק כאשר הסטטוס הוא failed
			return
		}
	}
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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\""));
    }
}
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 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());
ruby
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 כאשר הסטטוס הוא failed
    break
  end
end
rust
// [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 כאשר הסטטוס הוא failed
            return Ok(());
        }
    }
}
json
{
  "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"
  }
}

הורדת ה-CSV ​

GET /exports/{export}/download מזרים את הקובץ. לפני שהייצוא הושלם הוא מחזיר 404 עם code: "not_found" וההודעה Export file is not ready. — בצעו polling ל-status תחילה.

bash
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
js
// Node.js 18+ — top-level await במודול ESM
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 עד שהוא מוכן
}

// כתבו את הבייטים הגולמיים — ה-BOM של UTF-8 הוא חלק מהקובץ.
await writeFile('shipments.csv', Buffer.from(await res.arrayBuffer()))
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client(['base_uri' => 'https://app.shipos.co.il/api/v2/']);

$exportId = 512;

// 'sink' מזרים ישירות לדיסק — הבייטים הגולמיים שומרים על ה-BOM של UTF-8.
$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
<?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();

// שמרו את הגוף הגולמי — ה-BOM של UTF-8 הוא חלק מהקובץ.
Storage::put('shipments.csv', $response->body());
python
# 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()

    # מצב בינארי — ה-BOM של UTF-8 הוא חלק מהקובץ.
    with open("shipments.csv", "wb") as file:
        for chunk in response.iter_bytes():
            file.write(chunk)
go
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()

	// העתיקו את הבייטים הגולמיים — ה-BOM של UTF-8 הוא חלק מהקובץ.
	if _, err := io.Copy(file, res.Body); err != nil {
		panic(err)
	}
}
java
// Java 17+ — java.net.http, ללא תלויות
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 כותב את הבייטים הגולמיים — ה-BOM של UTF-8 הוא חלק מהקובץ.
        HttpResponse<Path> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofFile(Path.of("shipments.csv")));

        System.out.println(response.statusCode()); // 404 עד שהייצוא מוכן
    }
}
csharp
// .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 עד שהייצוא מוכן

// זרם בייטים גולמי — ה-BOM של UTF-8 הוא חלק מהקובץ.
await using var file = File.Create("shipments.csv");
await response.Content.CopyToAsync(file);
ruby
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)

    # מצב בינארי — ה-BOM של UTF-8 הוא חלק מהקובץ.
    File.open("shipments.csv", "wb") do |file|
      response.read_body { |chunk| file.write(chunk) }
    end
  end
end
rust
// [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 עד שהייצוא מוכן
        .bytes()
        .await?;

    // כתבו את הבייטים הגולמיים — ה-BOM של UTF-8 הוא חלק מהקובץ.
    tokio::fs::write("shipments.csv", &bytes).await?;
    Ok(())
}

עמודות ה-CSV הן: shipping_code, short_tracking_code, type, status, is_active, first_name, last_name, phone, city, address_1, created_at. הקובץ מתחיל ב-UTF-8 BOM כדי שעברית תוצג נכון ב-Excel.

מדריך שגיאות מהיר ​

סטטוסקודהיכןמתי
403forbiddenהכולאין רישיון שמיש (לא פעיל/פג תוקף/לא בבעלותכם).
404not_foundGET /batches/{uuid}, GET /exports/{export}, הורדההמשאב אינו שייך לרישיון שלכם, או שקובץ הייצוא עדיין לא מוכן.
409duplicate_requestPOST /exportsייצוא לרישיון זה כבר בהכנה — בצעו לו polling במקום.
422validation_failedPOST /batchesshipments חסר/ריק או מעל 2000 פריטים; או מספר רישיונות פעילים ללא license_key.