Lists, filters & pagination
Every paginated v2 list uses cursor pagination: instead of page numbers you follow an opaque cursor token from page to page. This page documents the paging and filter parameters of every list endpoint, then walks a complete pagination loop.
How cursor pagination works
Request parameters (query string):
| Param | Description |
|---|---|
per_page | Items per page. Each endpoint has its own default; all are capped at 100. |
cursor | Opaque token pointing at the next (or previous) page. Omit it for the first page; take it from the previous response's meta.next_cursor (or just follow links.next, which already carries it plus your other query params). |
Paginated responses share this envelope:
{
"data": [ { "...": "resources" } ],
"links": {
"first": null,
"last": null,
"prev": null,
"next": "https://app.shipos.co.il/api/v2/orders?cursor=eyJpZCI6OTg3NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ&per_page=25"
},
"meta": {
"path": "https://app.shipos.co.il/api/v2/orders",
"per_page": 25,
"next_cursor": "eyJpZCI6OTg3NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"prev_cursor": null
}
}meta.next_cursor— token for the next page;nullon the last page. That null is your loop's stop condition.meta.prev_cursor— token for the previous page;nullon the first page.links.next/links.prev— ready-made URLs carrying the cursor and your query params;links.first/links.lastare alwaysnull(cursors have no "jump to last page").- There is no total count — cursor pagination doesn't count the full result set (that is what makes it fast on large tables). If you need a full extract, loop to the end or use an export.
Treat the cursor as an opaque string: pass it back exactly as received, URL-encoded (the links.next URL already is).
Endpoint-by-endpoint
GET /shipments — reference
Cursor-paginated. per_page default 25, max 100.
| Param | Type | Description |
|---|---|---|
filter[active] | boolean | true → only active shipments, false → only inactive (cancelled) ones. Omit for both. |
sort | string | Allowlisted sort key: created_at or id. Prefix with - for descending (sort=-created_at). Unknown keys fall back to the default. Default: newest first (id descending). A stable id tiebreaker is always applied. |
per_page, cursor | As above. |
curl --location 'https://app.shipos.co.il/api/v2/shipments?filter[active]=true&sort=-created_at&per_page=50' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'// Node 18+ / browsers — no dependencies
const params = new URLSearchParams({
'filter[active]': 'true',
sort: '-created_at',
per_page: '50',
})
const response = await fetch(`https://app.shipos.co.il/api/v2/shipments?${params}`, {
headers: {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
Accept: 'application/json',
},
})
if (!response.ok) {
const { error } = await response.json()
throw new Error(`${error.code}: ${error.message}`)
}
const page = await response.json()
console.log(page.data.length, 'shipments; next cursor:', page.meta.next_cursor)<?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',
],
]);
$page = json_decode($client->get('shipments', [
'query' => [
'filter[active]' => 'true',
'sort' => '-created_at',
'per_page' => 50,
],
])->getBody()->getContents(), true);
printf("%d shipments\n", count($page['data']));
echo 'next cursor: ', $page['meta']['next_cursor'] ?? 'null', PHP_EOL;<?php
use Illuminate\Support\Facades\Http;
$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/shipments', [
'filter[active]' => 'true',
'sort' => '-created_at',
'per_page' => 50,
])
->throw()
->json();
logger()->info(count($page['data']).' shipments', [
'next_cursor' => $page['meta']['next_cursor'],
]);# pip install httpx
import os
import httpx
response = httpx.get(
"https://app.shipos.co.il/api/v2/shipments",
params={"filter[active]": "true", "sort": "-created_at", "per_page": 50},
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
},
)
response.raise_for_status()
page = response.json()
print(len(page["data"]), "shipments; next cursor:", page["meta"]["next_cursor"])package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type shipmentPage struct {
Data []json.RawMessage `json:"data"`
Meta struct {
NextCursor *string `json:"next_cursor"`
} `json:"meta"`
}
func main() {
query := url.Values{
"filter[active]": {"true"},
"sort": {"-created_at"},
"per_page": {"50"},
}
req, _ := http.NewRequest("GET",
"https://app.shipos.co.il/api/v2/shipments?"+query.Encode(), 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 shipmentPage
if err := json.NewDecoder(res.Body).Decode(&page); err != nil {
panic(err)
}
fmt.Println(len(page.Data), "shipments")
}// Java 17+ — java.net.http, no dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ShipOsShipments {
public static void main(String[] args) throws Exception {
String url = "https://app.shipos.co.il/api/v2/shipments"
+ "?filter%5Bactive%5D=true&sort=-created_at&per_page=50";
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
.header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
.header("Accept", "application/json")
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("ShipOS error: " + response.body());
}
// {"data":[...],"links":{...},"meta":{...}} — map with Jackson/Gson
System.out.println(response.body());
}
}// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;
using var http = new HttpClient
{
BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));
var page = await http.GetFromJsonAsync<JsonDocument>(
"shipments?filter%5Bactive%5D=true&sort=-created_at&per_page=50")
?? throw new InvalidOperationException("Empty response");
Console.WriteLine($"{page.RootElement.GetProperty("data").GetArrayLength()} shipments");
Console.WriteLine(
page.RootElement.GetProperty("meta").GetProperty("next_cursor").GetString() ?? "null");require "net/http"
require "json"
uri = URI("https://app.shipos.co.il/api/v2/shipments")
uri.query = URI.encode_www_form(
"filter[active]" => "true", "sort" => "-created_at", "per_page" => 50
)
request = Net::HTTP::Get.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
page = JSON.parse(response.body)
puts "#{page["data"].size} shipments; next cursor: #{page.dig("meta", "next_cursor")}"// [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 page: Value = reqwest::Client::new()
.get("https://app.shipos.co.il/api/v2/shipments")
.query(&[
("filter[active]", "true"),
("sort", "-created_at"),
("per_page", "50"),
])
.header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
.header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
.header("Accept", "application/json")
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("{} shipments", page["data"].as_array().map_or(0, Vec::len));
println!("next cursor: {}", page["meta"]["next_cursor"]);
Ok(())
}GET /orders — reference
Cursor-paginated, newest first. per_page default 25, max 100. No filters.
| Param | Type | Description |
|---|---|---|
per_page, cursor | As above. |
GET /orders/{order}/shipments (the shipments of one order) is not paginated — it returns all of that order's shipments in one response.
Returns — GET /shipments?filter[type]=2
There is no separate returns endpoint. A return is a shipment with the collection service type, so list them with the filter[type] parameter documented under GET /shipments above.
GET /batches — reference
Cursor-paginated, newest first, each batch with its items eager-loaded. per_page default 25, max 100. No filters.
GET /batches/{uuid}/items — reference
Cursor-paginated in submission order (position, then id). per_page default 50, max 100. No filters.
GET /notifications/sms-logs — reference
Cursor-paginated, newest first. per_page default 25, max 100.
| Param | Type | Description |
|---|---|---|
filter[shipping_id] | integer | Only logs for a single shipping. |
filter[success] | boolean | true → only successful sends, false → only failed ones. Omit for both. |
per_page, cursor | As above. |
curl --location 'https://app.shipos.co.il/api/v2/notifications/sms-logs?filter[success]=false&per_page=50' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'// Node 18+ / browsers — no dependencies
const params = new URLSearchParams({
'filter[success]': 'false',
per_page: '50',
})
const response = await fetch(
`https://app.shipos.co.il/api/v2/notifications/sms-logs?${params}`,
{
headers: {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
Accept: 'application/json',
},
},
)
if (!response.ok) {
const { error } = await response.json()
throw new Error(`${error.code}: ${error.message}`)
}
const page = await response.json()
console.log(page.data.length, 'failed sends; next cursor:', page.meta.next_cursor)<?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',
],
]);
$page = json_decode($client->get('notifications/sms-logs', [
'query' => [
'filter[success]' => 'false',
'per_page' => 50,
],
])->getBody()->getContents(), true);
printf("%d failed sends\n", count($page['data']));
echo 'next cursor: ', $page['meta']['next_cursor'] ?? 'null', PHP_EOL;<?php
use Illuminate\Support\Facades\Http;
$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/notifications/sms-logs', [
'filter[success]' => 'false',
'per_page' => 50,
])
->throw()
->json();
logger()->info(count($page['data']).' failed sends', [
'next_cursor' => $page['meta']['next_cursor'],
]);# pip install httpx
import os
import httpx
response = httpx.get(
"https://app.shipos.co.il/api/v2/notifications/sms-logs",
params={"filter[success]": "false", "per_page": 50},
headers={
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
},
)
response.raise_for_status()
page = response.json()
print(len(page["data"]), "failed sends; next cursor:", page["meta"]["next_cursor"])package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type smsLogPage struct {
Data []json.RawMessage `json:"data"`
Meta struct {
NextCursor *string `json:"next_cursor"`
} `json:"meta"`
}
func main() {
query := url.Values{
"filter[success]": {"false"},
"per_page": {"50"},
}
req, _ := http.NewRequest("GET",
"https://app.shipos.co.il/api/v2/notifications/sms-logs?"+query.Encode(), 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 smsLogPage
if err := json.NewDecoder(res.Body).Decode(&page); err != nil {
panic(err)
}
fmt.Println(len(page.Data), "failed sends")
}// Java 17+ — java.net.http, no dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ShipOsSmsLogs {
public static void main(String[] args) throws Exception {
String url = "https://app.shipos.co.il/api/v2/notifications/sms-logs"
+ "?filter%5Bsuccess%5D=false&per_page=50";
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
.header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
.header("Accept", "application/json")
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("ShipOS error: " + response.body());
}
// {"data":[...],"links":{...},"meta":{...}} — map with Jackson/Gson
System.out.println(response.body());
}
}// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;
using var http = new HttpClient
{
BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));
var page = await http.GetFromJsonAsync<JsonDocument>(
"notifications/sms-logs?filter%5Bsuccess%5D=false&per_page=50")
?? throw new InvalidOperationException("Empty response");
Console.WriteLine($"{page.RootElement.GetProperty("data").GetArrayLength()} failed sends");
Console.WriteLine(
page.RootElement.GetProperty("meta").GetProperty("next_cursor").GetString() ?? "null");require "net/http"
require "json"
uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-logs")
uri.query = URI.encode_www_form("filter[success]" => "false", "per_page" => 50)
request = Net::HTTP::Get.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
page = JSON.parse(response.body)
puts "#{page["data"].size} failed sends; next cursor: #{page.dig("meta", "next_cursor")}"// [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 page: Value = reqwest::Client::new()
.get("https://app.shipos.co.il/api/v2/notifications/sms-logs")
.query(&[("filter[success]", "false"), ("per_page", "50")])
.header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
.header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
.header("Accept", "application/json")
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("{} failed sends", page["data"].as_array().map_or(0, Vec::len));
println!("next cursor: {}", page["meta"]["next_cursor"]);
Ok(())
}GET /webhooks/{uuid}/deliveries — reference
Cursor-paginated, newest first. per_page default 25, max 100. No filters.
Endpoints with NO pagination
These return the complete result set in a single data array — there is no cursor, per_page, links, or meta:
| Endpoint | Notes |
|---|---|
GET /pickup-points — reference | Returns the carrier's full point list. city (substring filter) and limit (cap, max 500) trim the result, and lat/lng/address rank it nearest-first, but there are no pages — limit is a cap, not a page size. |
GET /licenses — reference | All of the account's licenses. |
GET /webhooks — reference | All of the license's webhook subscriptions. |
GET /orders/{order}/shipments — reference | All shipments of one order. |
GET /notifications/sms-templates, GET /reference/* | Full lists. |
Boolean filter values
Boolean filters (filter[active], filter[success]) accept the usual truthy/falsy forms: 1/0, true/false.
Worked example — walking all orders
Fetch every order, page by page, stopping when meta.next_cursor is null.
#!/usr/bin/env bash
# Requires jq. Walks /orders page by page.
BASE='https://app.shipos.co.il/api/v2'
CURSOR=''
while :; do
URL="$BASE/orders?per_page=100"
[ -n "$CURSOR" ] && URL="$URL&cursor=$CURSOR"
PAGE=$(curl --silent --location "$URL" \
--header "X-Client-Id: $CLIENT_ID" \
--header "X-Client-Secret: $CLIENT_SECRET" \
--header 'Accept: application/json')
echo "$PAGE" | jq -r '.data[] | "\(.order_id)\t\(.number)\t\(.total) \(.currency)"'
CURSOR=$(echo "$PAGE" | jq -r '.meta.next_cursor // empty')
[ -z "$CURSOR" ] && break
done// Node 18+ / browsers — no dependencies
const headers = {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
'Accept': 'application/json',
}
const orders = []
let cursor = null
do {
const params = new URLSearchParams({ per_page: '100' })
if (cursor) params.set('cursor', cursor)
const res = await fetch(`https://app.shipos.co.il/api/v2/orders?${params}`, { headers })
const page = await res.json()
orders.push(...page.data)
cursor = page.meta.next_cursor // null on the last page
} while (cursor)
console.log(`Fetched ${orders.length} orders`)<?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',
];
$cursor = null;
$orders = [];
do {
$response = $client->get('orders', [
'headers' => $headers,
'query' => array_filter([
'per_page' => 100,
'cursor' => $cursor,
]),
]);
$page = json_decode((string) $response->getBody(), true);
foreach ($page['data'] as $order) {
$orders[] = $order;
}
$cursor = $page['meta']['next_cursor'] ?? null;
} while ($cursor !== null);
printf("Fetched %d orders\n", count($orders));<?php
use Illuminate\Support\Facades\Http;
$client = Http::withHeaders([
'X-Client-Id' => config('services.shipos.client_id'),
'X-Client-Secret' => config('services.shipos.client_secret'),
])->acceptJson()->baseUrl('https://app.shipos.co.il/api/v2');
$cursor = null;
$orders = [];
do {
$page = $client->get('orders', array_filter([
'per_page' => 100,
'cursor' => $cursor,
]))->throw()->json();
$orders = array_merge($orders, $page['data']);
$cursor = $page['meta']['next_cursor'] ?? null;
} while ($cursor !== null);
logger()->info('Fetched '.count($orders).' orders');# pip install httpx
import os
import httpx
headers = {
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
}
orders = []
cursor = None
with httpx.Client(base_url="https://app.shipos.co.il/api/v2", headers=headers) as client:
while True:
params = {"per_page": 100}
if cursor:
params["cursor"] = cursor
page = client.get("/orders", params=params).raise_for_status().json()
orders.extend(page["data"])
cursor = page["meta"]["next_cursor"] # None on the last page
if not cursor:
break
print(f"Fetched {len(orders)} orders")package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type ordersPage struct {
Data []json.RawMessage `json:"data"`
Meta struct {
NextCursor *string `json:"next_cursor"`
} `json:"meta"`
}
func main() {
cursor, total := "", 0
for {
query := url.Values{"per_page": {"100"}}
if cursor != "" {
query.Set("cursor", cursor)
}
req, _ := http.NewRequest("GET",
"https://app.shipos.co.il/api/v2/orders?"+query.Encode(), 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 page ordersPage
err = json.NewDecoder(res.Body).Decode(&page)
res.Body.Close()
if err != nil {
panic(err)
}
total += len(page.Data)
if page.Meta.NextCursor == nil { // null on the last page
break
}
cursor = *page.Meta.NextCursor
}
fmt.Printf("Fetched %d orders\n", total)
}// Java 17+ — java.net.http (cursor read with Jackson)
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class ShipOsWalkOrders {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
ObjectMapper mapper = new ObjectMapper();
String cursor = null;
int total = 0;
do {
String url = "https://app.shipos.co.il/api/v2/orders?per_page=100"
+ (cursor == null ? ""
: "&cursor=" + URLEncoder.encode(cursor, StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
.header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
.header("Accept", "application/json")
.build();
JsonNode page = mapper.readTree(
client.send(request, HttpResponse.BodyHandlers.ofString()).body());
total += page.get("data").size();
JsonNode next = page.at("/meta/next_cursor"); // null on the last page
cursor = next.isNull() || next.isMissingNode() ? null : next.asText();
} while (cursor != null);
System.out.println("Fetched " + total + " orders");
}
}// .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"));
string? cursor = null;
var total = 0;
do
{
var url = "orders?per_page=100"
+ (cursor is null ? "" : $"&cursor={Uri.EscapeDataString(cursor)}");
var page = await http.GetFromJsonAsync<JsonDocument>(url)
?? throw new InvalidOperationException("Empty response");
total += page.RootElement.GetProperty("data").GetArrayLength();
var next = page.RootElement.GetProperty("meta").GetProperty("next_cursor");
cursor = next.ValueKind == JsonValueKind.Null ? null : next.GetString();
} while (cursor is not null);
Console.WriteLine($"Fetched {total} orders");require "net/http"
require "json"
cursor = nil
total = 0
loop do
uri = URI("https://app.shipos.co.il/api/v2/orders")
params = { "per_page" => 100 }
params["cursor"] = cursor if cursor
uri.query = URI.encode_www_form(params)
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) { |h| h.request(request) }
raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
page = JSON.parse(response.body)
total += page["data"].size
cursor = page.dig("meta", "next_cursor") # nil on the last page
break unless cursor
end
puts "Fetched #{total} orders"// [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 mut cursor: Option<String> = None;
let mut total = 0usize;
loop {
let mut query = vec![("per_page", "100".to_string())];
if let Some(c) = &cursor {
query.push(("cursor", c.clone()));
}
let page: Value = client
.get("https://app.shipos.co.il/api/v2/orders")
.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?;
total += page["data"].as_array().map_or(0, Vec::len);
cursor = page["meta"]["next_cursor"].as_str().map(str::to_owned);
if cursor.is_none() {
break;
}
}
println!("Fetched {total} orders");
Ok(())
}The same loop works for /shipments, /orders, /batches, batch items, SMS logs, and webhook deliveries — only the endpoint, filters, and per_page default change.
Cursors don't survive query changes
A cursor encodes the position within one specific ordering. Keep per_page, sort, and filters identical across the loop — changing them mid-walk invalidates the position. Start a fresh walk (no cursor) whenever the query changes.