Skip to content

Concept map· 6 connected terms

Cursor-Based Pagination

Also called: Cursor pagination · Graph API paging · Paging cursors

In one line
Cursor-based pagination walks a long Graph API list page by page with opaque before and after cursors and paging.next links instead of page numbers.

Reviewed

What is cursor-based pagination?

Cursor-based pagination is how the Graph API returns lists too long for one response: a WhatsApp Business Account's message templates, its phone numbers, or the WABAs a business owns. Instead of page numbers, each response carries a data array and a paging object. paging.cursors.before and paging.cursors.after are opaque strings that mark the first and last item of the current page, and paging.next and paging.previous are ready-made URLs for the neighbouring pages.

To walk a whole list you request the edge with a limit (the most items you want per page), process data, then request paging.next, or repeat your own request with after={cursor}, until a response comes back with no next link. Because a cursor marks a position in the result set rather than a numeric offset, items added or removed while you page do not shift every later page; Meta calls cursor paging the most efficient method and recommends it wherever an edge supports it.

The Graph API also has time-based paging (since and until) and offset paging on some edges, but the WhatsApp management edges you page most often, such as /{WABA_ID}/message_templates and /{WABA_ID}/phone_numbers, return cursors. Pagination is not field expansion: fields=name,status,category shapes each item, while cursors choose which slice of the list comes back. The two combine, and a nested edge requested through field expansion carries its own paging block.

Key takeaways

  • Cursor-based pagination returns a list in pages, each with opaque before and after cursors marking where that page starts and ends.
  • Follow paging.next until it is absent: that, not a short or empty page, is the signal that you have reached the end.
  • limit sets the most items per page; a page can still come back with fewer while more pages follow.
  • Cursors are short-lived bookmarks: use them straight away and store item IDs, never cursors.
  • Field expansion decides which fields each item has; pagination decides which items you get.

Follow the cursors to the end of the list

5 / 5
import requests
url = "https://graph.facebook.com/v26.0/{WABA_ID}/message_templates"
params = {"fields": "name,status,category", "limit": 100}
headers = {"Authorization": f"Bearer {TOKEN}"}
templates, pages = [], 0
while url and pages < 200:
body = requests.get(url, params=params, headers=headers, timeout=30).json()
if "error" in body:
raise RuntimeError(body["error"]["message"])
templates.extend(body.get("data", []))
url = body.get("paging", {}).get("next")
params = None # next already carries fields, limit and after
pages += 1

Step 5 of 5 · line 14

Drop your own params: next already contains them plus the after cursor, and adding them twice can break the request.

Jump to
A Python loop that pages through a WABA's message templates, then the page it reads. Step through to see where the cursor comes from and the one condition that ends the walk.

sync_templates.py, step 5 of 5, line 14: Drop your own params: next already contains them plus the after cursor, and adding them twice can break the request.

Cursor-based pagination in code

const BASE = "https://graph.facebook.com/v26.0";
async function listTemplates(wabaId, token) {
const all = [];
let url = BASE + "/" + wabaId +
"/message_templates?fields=name,status,category,language&limit=100";
for (let page = 0; url && page < 200; page++) { // hard cap on pages
const res = await fetch(url, { headers: { Authorization: "Bearer " + token } });
const body = await res.json();
if (!res.ok) throw new Error(body.error?.code + ": " + body.error?.message);
all.push(...body.data);
url = body.paging?.next; // undefined on the last page
}
return all;
}

Follow paging.next until it disappears; never stop because a page looks short.

Key facts at a glance

Cursor-based pagination: key facts and limits

Page shape
data array + paging object
Cursors
paging.cursors.before / after (opaque strings)
Navigation
paging.next / paging.previous (full URLs)
Page size
limit= is a maximum, not a guarantee
End of the list
No paging.next in the response
Other Graph paging
Time-based (since/until) and offset on some edges

How does cursor-based pagination work?

  1. Request the edge with a limit

    Call, for example, GET /v26.0/{WABA_ID}/message_templates?fields=name,status,category,language&limit=100. Keep limit moderate: very large pages are slower and more likely to time out.

  2. Process every item in data

    Handle the whole data array, and don't assume its length equals limit. A page may hold fewer items even when more pages follow.

  3. Read the paging object

    paging.cursors.after marks the end of this page, and paging.next is the full URL of the next one with your fields, filters and limit already in it.

  4. Fetch the next page

    Request paging.next as it is, or reissue your own request with after={cursor}. Use before or paging.previous to move backwards.

  5. Stop when next is missing

    The last page has no next link. Stop there, not on a short or empty page, and keep a hard page cap so a bug can never loop forever.

Common wrong turns

Common mistakes with cursor-based pagination

  • Reading only the first page

    The commonest bug: a sync that never looks at paging. Lists under one page work in testing, then silently truncate in production. Always loop on next.

  • Stopping on a short page

    Fewer items than limit does not mean the end; the Graph API can return a partial page with more to come. The only stop signal is a missing paging.next.

  • Storing cursors as if they were IDs

    Cursors are position markers that can become invalid as the list changes. Persist template or phone number IDs, and restart the walk instead of resuming from yesterday's cursor.

  • Rebuilding the next URL and losing parameters

    If you build page two yourself, carry over the same fields, filters and limit, or later pages come back in a different shape. Requesting paging.next unchanged avoids the problem.

  • Looping without a guard

    A bug that reuses the same cursor loops forever and burns your rate limit. Cap the page count and stop if a cursor repeats.

Example: Mirroring a template library in Hyderabad

The ops team at a Hyderabad consumer-lending company keeps a nightly copy of its WhatsApp templates so support can see which reminders are approved, paused or rejected. The job requests message_templates with limit=100 and the four fields it needs, follows paging.next five times, and stops when the sixth response has no next: 512 templates in six calls. The previous version read only the first response, so for months the dashboard showed 100 templates and missed a paused collections reminder that was failing on every send.

Why does cursor-based pagination matter?

Template libraries grow into the hundreds (verified portfolios can hold up to 6,000 templates per WABA), and businesses with many numbers or client WABAs outgrow a single page quickly. Code that reads only the first response fails silently: the sync job shows the first page of templates when there are 400, and the dashboard never sees the rejected ones on page three. Following cursors correctly is the difference between a complete mirror of your account and a quietly partial one.

Good practice

Best practices for cursor-based pagination

  • Request only the fields you need with fields=: smaller items make every page faster.
  • Use server-side filters where the edge's reference documents them instead of paging through everything and filtering locally.
  • Log the page count and item count of every sync, so a sudden drop stands out.
  • On large accounts, back off on rate limiting errors between pages rather than hammering the next URL.

Cursor-Based Pagination FAQs

How do I get the next page of results in the Graph API?+
Request the URL in `paging.next`. It already contains your fields, limit and the `after` cursor; keep going until a response arrives without a `next` link.
What is the difference between cursor and offset pagination?+
Offset pagination skips a number of items, while cursor pagination resumes from a marker. Cursors stay correct when items are added or removed mid-walk, which is why the Graph API prefers them.
Why did a Graph API page return fewer items than my limit?+
Because `limit` is a ceiling, not a promise. A page can hold fewer items while more pages exist, so keep following `paging.next` rather than stopping early.
Can I save a cursor and resume pagination tomorrow?+
Don't rely on it. Cursors are short-lived position markers and can become invalid when the list changes; save item IDs and start a fresh walk instead.

See InfiQ pricing

Try it on the official WhatsApp Business API

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.