Pagination
Every list endpoint is offset-paginated and returns the same envelope: a metaobject describing the page, and a results array of records. Readmeta.hasMore to decide whether to fetch the next page — don't infer "done" from a short or empty result set.
The envelope
{
"meta": {
"limit": 25,
"offset": 0,
"hasMore": true
},
"results": [
{ "id": "prp_01HX0G5T8N3KEWBYV2QMR4DCFA" }
]
}Note: it's hasMore, camelCase. The fields:
| Field | Type | Meaning |
|---|---|---|
| meta.limit | integer | Page size used for this response. |
| meta.offset | integer | Records skipped before this page. |
| meta.hasMore | boolean | Whether more records exist past this page. |
| results | array | The records for this page. |
limit and offset
limit— page size. Defaults to 25, maximum 100.offset— how many records to skip. Defaults to 0.
Request a limit above 100 and you'll get a 400 — the value isn't silently clamped. Keep it at or below 100, and use the max for backfills so you make the fewest round trips.
curl "https://api.propsocket.io/v1/units?limit=100&offset=200" \
-H "Authorization: Bearer ps_test_YOUR_TEST_KEY"Loop until hasMore is false
The canonical pattern: fetch a page, process it, stop when meta.hasMore isfalse, otherwise advance offset by your page size. The versions below are generators so you can stream through large result sets without holding everything in memory.
# Page through manually: bump offset by limit until hasMore is false.
curl "https://api.propsocket.io/v1/units?limit=100&offset=0&order-by=created_at:asc" \
-H "Authorization: Bearer ps_test_YOUR_TEST_KEY"
# ...then offset=100, offset=200, ... until meta.hasMore == falseimport os, requests
BASE = "https://api.propsocket.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROPSOCKET_API_KEY']}"}
def list_all(path):
offset, limit = 0, 100 # max page size
while True:
resp = requests.get(
f"{BASE}/{path}",
headers=HEADERS,
params={"limit": limit, "offset": offset, "order-by": "created_at:asc"},
)
resp.raise_for_status()
body = resp.json()
yield from body["results"]
if not body["meta"]["hasMore"]:
return
offset += limit
units = list(list_all("units"))
print(len(units))const BASE = "https://api.propsocket.io/v1";
const HEADERS = { Authorization: `Bearer ${process.env.PROPSOCKET_API_KEY}` };
async function* listAll(path) {
let offset = 0;
const limit = 100; // max page size
for (;;) {
const url = new URL(`${BASE}/${path}`);
url.searchParams.set("limit", String(limit));
url.searchParams.set("offset", String(offset));
url.searchParams.set("order-by", "created_at:asc");
const resp = await fetch(url, { headers: HEADERS });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = await resp.json();
yield* body.results;
if (!body.meta.hasMore) return;
offset += limit;
}
}
const units = [];
for await (const u of listAll("units")) units.push(u);
console.log(units.length);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
const base = "https://api.propsocket.io/v1"
func listAll(path string, yield func(map[string]any)) {
offset, limit := 0, 100 // max page size
key := os.Getenv("PROPSOCKET_API_KEY")
for {
q := url.Values{}
q.Set("limit", fmt.Sprint(limit))
q.Set("offset", fmt.Sprint(offset))
q.Set("order-by", "created_at:asc")
req, _ := http.NewRequest("GET", base+"/"+path+"?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
panic(fmt.Sprintf("HTTP %d", resp.StatusCode))
}
var body struct {
Meta struct{ HasMore bool `json:"hasMore"` } `json:"meta"`
Results []map[string]any `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
resp.Body.Close()
panic(err)
}
resp.Body.Close()
for _, r := range body.Results {
yield(r)
}
if !body.Meta.HasMore {
return
}
offset += limit
}
}
func main() {
n := 0
listAll("units", func(_ map[string]any) { n++ })
fmt.Println(n)
}require "net/http"
require "json"
require "uri"
BASE = "https://api.propsocket.io/v1"
KEY = ENV.fetch("PROPSOCKET_API_KEY")
def list_all(path)
return enum_for(:list_all, path) unless block_given?
offset, limit = 0, 100 # max page size
loop do
uri = URI("#{BASE}/#{path}")
uri.query = URI.encode_www_form(
limit: limit, offset: offset, "order-by": "created_at:asc"
)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{KEY}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
raise "HTTP #{res.code}" unless res.code.to_i == 200
body = JSON.parse(res.body)
body["results"].each { |r| yield r }
return unless body.dig("meta", "hasMore")
offset += limit
end
end
puts list_all("units").count<?php
$base = "https://api.propsocket.io/v1";
$key = getenv("PROPSOCKET_API_KEY");
function list_all(string $base, string $key, string $path): Generator
{
$offset = 0;
$limit = 100; // max page size
while (true) {
$q = http_build_query([
"limit" => $limit,
"offset" => $offset,
"order-by" => "created_at:asc",
]);
$ch = curl_init("$base/$path?$q");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("HTTP $status");
}
$body = json_decode($raw, true);
yield from $body["results"];
if (!$body["meta"]["hasMore"]) {
return;
}
$offset += $limit;
}
}
echo iterator_count(list_all($base, $key, "units")), "\n";import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*; // Jackson: the one JSON dependency
var base = "https://api.propsocket.io/v1";
var key = System.getenv("PROPSOCKET_API_KEY");
var client = HttpClient.newHttpClient();
var mapper = new ObjectMapper();
String path = "units";
int offset = 0, limit = 100, count = 0; // max page size
while (true) {
var uri = URI.create(base + "/" + path + "?limit=" + limit
+ "&offset=" + offset + "&order-by=created_at:asc");
var request = HttpRequest.newBuilder(uri)
.header("Authorization", "Bearer " + key).build();
var resp = client.send(request, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) throw new RuntimeException("HTTP " + resp.statusCode());
JsonNode body = mapper.readTree(resp.body());
count += body.get("results").size();
if (!body.get("meta").get("hasMore").asBoolean()) break;
offset += limit;
}
System.out.println(count);using System.Text.Json;
var BASE = "https://api.propsocket.io/v1";
var key = Environment.GetEnvironmentVariable("PROPSOCKET_API_KEY");
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
string path = "units";
int offset = 0, limit = 100, count = 0; // max page size
while (true)
{
var url = $"{BASE}/{path}?limit={limit}&offset={offset}&order-by=created_at:asc";
var resp = await client.GetAsync(url);
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
count += doc.RootElement.GetProperty("results").GetArrayLength();
if (!doc.RootElement.GetProperty("meta").GetProperty("hasMore").GetBoolean()) break;
offset += limit;
}
Console.WriteLine(count);Sort interaction
Pagination and sorting compose with the order-by query parameter, formattedfield:direction (asc or desc). The default sort iscreated_at:desc.
For any loop that walks the full collection — backfills, reconciliation, warehouse loads — sort by created_at:asc. New records always append to the end, so an ascending walk by creation time never shifts the records you've already paged past. Sorting descending (the default) means a new insert lands at offset 0 and pushes everything down by one, which can cause you to re-read or skip a record across page boundaries.
Anti-pattern: parallel offsets
It's tempting to speed up a backfill by firing requests foroffset=0, offset=100, offset=200 all at once. Don't. The underlying collection is live — a record inserted (or soft-deleted) between two of those requests shifts the window, so adjacent pages can overlap or leave a gap. You'll silently duplicate or drop records.
Page sequentially, sorted by created_at:asc, and dedupe on the record's stableid if you must parallelize at a higher level (e.g., one worker per entity type). For throughput planning under the rate limit, see rate limits.
Next
Narrow what you page through with filtering, or see the full list response shapes in the API reference.