35 — Developer

Pagination

Every list endpoint paginates the same way: cursor-based, newest first, with an object id as the cursor. No offsets, no page numbers — pages stay stable even while new payments stream in.

The list envelope

List endpoints (GET /v1/payments, /v1/refunds, /v1/events, …) return a common wrapper:

200 OK — list envelope
{
  "object": "list",
  "url": "/v1/payments",
  "data": [
    { "id": "pay_8Q2mX4nT1cVb", "object": "payment", "amount": 4900, "currency": "EUR",  },
    { "id": "pay_7Lw9cR5uY2Ze", "object": "payment", "amount": 1250, "currency": "EUR",  }
  ],
  "has_more": true
}
FieldMeaning
objectAlways "list".
urlThe endpoint that produced this page.
dataThe page of results, newest first. May be empty.
has_moretrue if results exist beyond this page in the direction you're paging. Your loop condition.

Parameters

ParameterTypeMeaning
limitintegerPage size, 1–100. Default 20.
starting_afterobject idCursor for the next page: return objects older than this id. Use the last id of the current page.
ending_beforeobject idCursor for the previous page: return objects newer than this id. Use the first id of the current page.

starting_after and ending_before are mutually exclusive — pass one or neither. Cursors compose with any endpoint filters (?status=succeeded&limit=50…); keep the filters identical from page to page.

Iterating a full listing

Fetch a page, follow the last id, stop when has_more is false:

Page 1, then page 2
# Page 1 — newest 20 succeeded EUR payments
curl -G https://api.finscale.dev/v1/payments \
  -H "Authorization: Bearer sk_test_51FinscaleDemo…" \
  -d "limit=20" -d "status=succeeded"

# …response ends with "id": "pay_8Q2mX4nT1cVb" and "has_more": true

# Page 2 — everything older than the last id of page 1
curl -G https://api.finscale.dev/v1/payments \
  -H "Authorization: Bearer sk_test_51FinscaleDemo…" \
  -d "limit=20" -d "status=succeeded" \
  -d "starting_after=pay_8Q2mX4nT1cVb"

The same loop in code:

list-all.js
async function listAllPayments() {
  const all = [];
  let cursor = null;
  do {
    const params = new URLSearchParams({ limit: "100" });
    if (cursor) params.set("starting_after", cursor);
    const res = await fetch(`https://api.finscale.dev/v1/payments?${params}`, {
      headers: { "Authorization": `Bearer ${process.env.FINSCALE_SECRET_KEY}` }
    });
    const page = await res.json();
    all.push(...page.data);
    cursor = page.has_more ? page.data[page.data.length - 1].id : null;
  } while (cursor);
  return all;
}

To page backwards — say, from a bookmarked payment toward newer ones — use ending_before with the first id you have. has_more then refers to the newer direction.

Why cursors, not offsets

With ?page=7, a payment created mid-iteration shifts every subsequent page and you skip or double-read rows. An id cursor pins your position in the stream — new arrivals can't move it. That matters when you're reconciling money; for bulk end-of-day work, prefer settlement reports over paging the world.