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} — הסטטוס נע pendingprocessingcompleted (או 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.