Browse the docs

Pagination

How list endpoints page their results, and which parameters each one takes.

Every endpoint that returns a collection returns one page of it, described by the same pagination object. The table below gives the page parameters, the size limits and the list key for each endpoint.

One pair works everywhere

Every paginated endpoint accepts current_page and per_page — the same spelling the pagination object uses in the response, so you can page with the value you just read:

cURL

curl --request GET      --url 'https://api.themooncard.com/v2/api-gateway/cards?current_page=2&per_page=25'      --header 'accept: application/json'      --header 'x-api-key: YOUR_API_KEY'

Each endpoint also keeps the parameter names it has always accepted, listed below. They mean exactly the same thing, carry the same limits, and keep working. If you send both spellings in one request, current_page and per_page win.

Which parameters each list takes

Swap the host for https://api.themooncard.com in production.

EndpointPage parametersSize limitsList key in the response
V1 GET /cardpage, limit, current_page, per_pagepage default 1; limit 1–49, default 10cards
V2 GET /cardspage, limit, current_page, per_pagepage default 1; limit 1–49, default 10cards
V1 GET /controls/velocitypage, current_page, per_pagepage default 1; per_page applied as at most 100, default 10velocityControls
V2 GET /controls/velocitypage, current_page, per_pagepage default 1; per_page applied as at most 100, default 10velocityControls
V2 GET /accountspage, current_page, per_pagepage minimum 1; per_page 1–100accounts
V2 GET /accounts/{id}/cardspage, current_page, per_pagepage minimum 1; per_page 1–100cards
V2 GET /cardholderspage, current_page, per_pagepage default 1; per_page 1–100, default 10cardholders
V2 GET /cardholders/{id}/cardspage, current_page, per_pagepage default 1; per_page 1–100, default 10cards
V2 GET /organizationspage, current_page, per_pagepage default 1; per_page below 50, default 10organizations
V2 GET /organizations/{id}/childrenpage, current_page, per_pagepage default 1; per_page below 50, default 10organizations
V1 GET /card/card-productscurrent_page, per_pagecurrent_page default 1; per_page minimum 1, default 10card_products
V1 GET /card/{card_id}/transactionscurrent_page, per_pagecurrent_page default 1; per_page 1–50, default 10transactions
V2 GET /cards/{card_id}/transactionscurrent_page, per_pagecurrent_page default 1; per_page 1–50, default 10transactions
V2 GET /accounts/{id}/transactionscurrent_page, per_pagecurrent_page minimum 1; per_page 1–50transactions

Elsewhere the cap is validated and a larger value is rejected with a 400: GET /accounts at 100, the organization listings at 49, the card listings at 49, and the transaction feeds at 50.

Three details worth reading off the table before you write a loop:

  • The transaction feeds also answer to currentPage and perPage, the spelling they carried before the canonical pair existed. GET /accounts/{id}/transactions takes its date filters as start_date and end_date, and still accepts startDate and endDate.
  • The two card-product listings use current_page and per_page. Those are the parameters they read; send the page you want with those two keys.
  • velocityControls is the list key on both velocity-control listings. The rest of the envelope is snake_case.

Lists that take no page parameters

The velocity status endpoints return the full set as a bare JSON array, with no wrapper key and no pagination object:

  • V1 and V2 GET /controls/velocity/card/{card_id}/status
  • V1 and V2 GET /controls/velocity/cardholder/{external_id}/status
  • V2 GET /controls/velocity/status

GET /controls/velocity/{id}/status is not a list at all: it returns a single status object for one control.

The pagination object

Every paginated endpoint returns the same object next to its list. All eight fields are always present.

FieldTypeMeaning
totalintegerTotal number of items across all pages.
per_pageintegerPage size that was applied.
current_pageintegerThe page you are on, 1-based.
last_pageintegerNumber of the last page. With no items at all it is 0 on some listings and 1 on others, so branch on total or on next_page, not on last_page.
prev_pageinteger or nullPrevious page number; null on the first page.
next_pageinteger or nullNext page number; null on the last page.
frominteger1-based index of the first item on this page; 0 when the page is empty.
tointeger1-based index of the last item on this page; 0 when the page is empty.

The pagination object is snake_case on every endpoint.

cURL

curl --request GET \
     --url 'https://api.themooncard.com/v1/api-gateway/card/card-products?current_page=1&per_page=10' \
     --header 'accept: application/json' \
     --header 'x-api-key: YOUR_API_KEY'

Walking every page

Follow pagination.next_page until it is null. That works on every paginated endpoint and does not require you to compute anything.

Node.js

async function listAllCards(apiKey) {
  const cards = [];
  let page = 1;

  for (;;) {
    const response = await fetch(
      `https://api.themooncard.com/v1/api-gateway/card?page=${page}&limit=49`,
      { headers: { "x-api-key": apiKey, accept: "application/json" } },
    );
    if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);

    const body = await response.json();
    cards.push(...body.cards);

    if (body.pagination.next_page === null) return cards;
    page = body.pagination.next_page;
  }
}

Two things to keep in mind while you loop:

  • Lists are ordered newest first, and the underlying data can change between requests. An item created while you are paging can shift later items onto the next page. If exactness matters, reconcile by id rather than assuming each page is disjoint.
  • Space the requests out. A long paging loop can reach the request limit — see Rate limits for the ceiling, the X-RateLimit-* headers and the Retry-After on a 429.

A page past the end

Requesting a page beyond last_page returns 200 with an empty list. total and last_page still describe the real data and next_page is null. from and to describe the window that was asked for rather than the rows returned, so on an overflow page from can exceed to — read the list itself, and stop when next_page is null.

JSON

{
  "cards": [],
  "pagination": {
    "total": 12,
    "last_page": 2,
    "prev_page": 1,
    "next_page": null,
    "per_page": 10,
    "current_page": 9,
    "from": 81,
    "to": 12
  }
}

Filtering a list by an id

Filter a list by an identifier that does not exist — or that belongs to another organization — and you get 200 with an empty list and total: 0.

RequestUnknown or unowned id gives
GET /card?external_id=… (V1), GET /cards?external_id=… (V2)200, empty cards
GET /card?card_holder_id=… (V1), GET /cards?card_holder_id=… (V2)200, empty cards
GET /card/{card_id}/transactions (V1), GET /cards/{card_id}/transactions (V2, with an API key)200, empty transactions

total: 0 therefore means either that the id is real and has nothing attached to it, or that the id is not one of yours. To confirm an id exists, fetch the resource directly — GET /card/{card_id}, GET /cardholders/{id} — which returns 404 for an id outside your organization.

Where the id is a path segment, the parent resource is resolved first:

  • GET /v2/api-gateway/accounts/{id}/cards — 404 when the account is unknown or not yours; 200 with an empty list and total: 0 when the account is real and funds no card.
  • GET /v2/api-gateway/cardholders/{id}/cards — 404 when the cardholder is unknown or not yours. When it is yours, pagination.total counts every card ever linked to it, including deleted ones the page filters out, so total can exceed the number of items you can page through. Count the items you receive when you need the number of live cards.
  • GET /controls/velocity?card_id=… — 404 with code: "CARD_NOT_FOUND" when the card is unknown or not yours.
  • GET /v2/api-gateway/accounts/{id}/transactions — 404 when the account is unknown or not yours. While the activity feed is momentarily unavailable it returns 200 with an empty list; retry to confirm a feed you expect to hold entries.

An id that is not a well-formed UUID fails validation before the lookup and comes back as a 400. See Errors for that body and for the difference between validation 400s and { message, code } errors.

Last updated August 17, 2026