# Pagination

> Organizations' contacts page per organization inside a batch, with a limit of 1 to 50 and an opaque cursor per item; follow each result's nextCursor until it is null.

The one endpoint that can return many records for an item — organizations' contacts, `POST /organizations/contacts/match` — pages each organization's contacts with a cursor. The cursor lives on the item, not on the request: one batch can read the first page of some organizations and the next page of others. Everything else answers a batch of at most 25 items in one response.

## Request

| Field | Meaning |
|---|---|
| `limit` | Top level. Contacts per organization, 1 to 50. Default 20. An item with a `cursor` ignores it: the first page fixed the page size. |
| `items[].cursor` | Leave it out for an organization's first page. For its next page, send the `nextCursor` of that organization's previous result. The item then needs no identifier. |

```bash
curl https://api.funnelfeedr.com/external/v1/organizations/contacts/match \
  -H "Authorization: Bearer $FUNNELFEEDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "ref": "crm-1001", "domain": "nordviklogistik.se" },
      { "ref": "crm-1002", "orgNumber": "5590123456", "countryCode": "SE" }
    ],
    "limit": 50
  }'
```

## Response

Each `matched` result holds one page in `contacts`, next to `totalCount`, `filteredByTargetRoles` and `nextCursor`:

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "contacts": [
        {
          "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
          "name": "Anna Svensson-Berg",
          "jobTitle": "Head of Procurement",
          "isExecutive": false,
          "isDecisionMaker": true,
          "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
          "organizationName": "Nordvik Logistik AB",
          "email": "XXXXX@nordviklogistik.se",
          "emailIsMasked": true,
          "phone": "+46701XXXXXX",
          "phoneIsMasked": true,
          "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
        }
      ],
      "totalCount": 63,
      "filteredByTargetRoles": true,
      "nextCursor": "eyJzIjoib3JnYW5pemF0aW9uLWNvbnRhY3RzOjZmOWQyZjFlNmYzNzRhMmY5YTNlMWI1YzBmNGQ3ZTIxIiwicCI6MiwibiI6NTB9"
    }
  ]
}
```

`totalCount` counts every contact the endpoint returns for the organization, across pages. `nextCursor` is on every `matched` result, and `null` on the organization's last page. Send the cursors that are not `null` back as items, and repeat until none are left:

```python
import os, requests

url = "https://api.funnelfeedr.com/external/v1/organizations/contacts/match"
headers = {"Authorization": f"Bearer {os.environ['FUNNELFEEDR_API_KEY']}"}
items = [
    {"ref": "crm-1001", "domain": "nordviklogistik.se"},
    {"ref": "crm-1002", "orgNumber": "5590123456", "countryCode": "SE"},
]

while items:
    body = requests.post(url, headers=headers, json={"items": items, "limit": 50}).json()
    items = []
    for result in body["results"]:
        if result["status"] != "matched":
            continue
        for contact in result["contacts"]:
            print(result["ref"], contact["name"], contact.get("jobTitle"))
        if result["nextCursor"]:
            items.append({"ref": result["ref"], "cursor": result["nextCursor"]})
```

Each round is one request against the `search` [rate limit](https://funnelfeedr.com/developers/rate-limits.md), however many organizations it pages. Keep a round to 25 items: add first pages of new organizations to the same request while there is room.

## Cursors are opaque

A cursor is a token, not a page number. Don't decode, build or change it; pass it back exactly as you got it. It names its organization, so you can send it alone; if you send identifiers beside it, they must name the same organization. A cursor that has been altered, or whose item names another organization, makes that item `invalid` with the error `invalid_cursor` — the rest of the batch still runs. Cursors do not expire, but the contacts behind them can change between pages: a contact added, or one that no longer matches your target roles, can shift the rest of the list by a row.
