Pagination and filtering
Every list endpoint pages the same way, so a client that can page one can page all of them.
Cursors, not page numbers
A list response carries a cursor:
{
"shipments": [
{ "reference": "C3AT-XHM0" }
],
"cursor": {
"after": "eyJpZCI6NjUxfQ"
}
}
Pass after back to get the next page:
curl -H 'X-Cargoplot-Key: <token>' \
'https://api.cargoplot.com/v1/shipment?after=eyJpZCI6NjUxfQ'
cursor.after is absent when there is no next page. That absence is how you
know you have reached the end — not an empty array, which you will not get.
cursor.before works the same way going backwards, and is absent on the first
page.
Treat a cursor as opaque. It is a token that means "resume from here", and its contents are not a contract — decoding one and constructing your own will break.
Why cursors rather than ?page=3: an offset re-counts rows on every request, so a
new shipment arriving mid-pass shifts everything down and you silently skip a
record. A cursor resumes from a fixed point instead.
Page size
limit accepts 50 by default and
100 at most.
Asking for more than the maximum is refused with a 400, not quietly reduced.
That is deliberate: a silently truncated page looks exactly like the end of the
data, and a client that trusts it skips records without ever knowing.
Sorting
order_by chooses the field, order the direction (ORDER_ASC or ORDER_DESC,
newest first by default). The available order_by values differ per resource —
see the spec for each endpoint's list.
The sort is part of the cursor. Once you start paging, changing order or
order_by invalidates the cursor you were given. Pick the sort for the whole
pass, and start again from no cursor if you need to change it.
Filtering and search
Filters narrow by field. They are repeatable where a resource can be in one of several states:
'?filter.status=STATUS_ACTIVE&filter.status=STATUS_ON_HOLD'
Date filters
Every filter on a date accepts four shapes, so a caller can express an open or a closed window without a second parameter:
| Value | Matches |
|---|---|
2026-07-01 | that whole day |
gte:2026-07-01 | that day and everything after it |
lte:2026-07-31 | that day and everything before it |
2026-07-01..2026-07-31 | both days and everything between them |
'?filter.etd=gte:2026-07-01&filter.eta=2026-07-01..2026-07-31'
Every bound is a whole day, and both ends are inclusive. lte:2026-07-31
keeps a shipment arriving at 23:50 on the 31st, and a closed range keeps both of
its end days. Days are UTC, so a bound is not nudged by the caller's own offset.
Sub-day precision is not accepted; to narrow within a day, filter on the day and
compare timestamps client-side.
A record whose field is empty is not matched. A shipment with no ETA is
outside filter.eta=gte:2026-07-01 and outside lte:2026-07-01, so the two do
not add up to the full list.
A value that is not one of the four shapes is refused with a 400 naming
the forms that are accepted, rather than dropped. That is deliberate: an ignored
filter answers a broader question than the one asked, and a page of everything
looks much like a page of matches.
A backwards closed range is a shape the four forms allow, so it is not
refused. 2026-07-31..2026-07-01 asks for records on or after the 31st and on or
before the 1st, which nothing satisfies, and the result is an empty page rather
than an error.
Which fields are dates differs by resource, and the spec is the
list: a shipment filters on etd and eta, an invoice on issue_date and
due_date, an inquiry on cargo_ready_date.
search is free text across the fields a human would search, which differ by
resource: a shipment's reference, description and origin or destination city; an
invoice's reference. It narrows the result set; it does not reorder it, so a
better match does not float to the top.
Filters and search combine, and combine with paging. Applying them server-side is almost always cheaper than fetching everything and filtering locally — and it spends far less of your rate limit.
Walking a whole list
cursor=''
while :; do
page=$(curl -sH "X-Cargoplot-Key: $TOKEN" \
"https://api.cargoplot.com/v1/shipment?limit=100&after=$cursor")
echo "$page" | jq -r '.shipments[].reference'
cursor=$(echo "$page" | jq -r '.cursor.after // empty')
[ -n "$cursor" ] || break
done
The loop ends because cursor.after disappears, which is the only reliable
signal. Stopping when a page comes back shorter than limit is a common habit and
it is wrong here — a filtered page can be short and still have more after it.