---
title: "Pagination et filtres"
description: "Comprendre la pagination par curseur de Billabex et le paramètre filters, et ce que votre intégration doit implémenter."
canonical: https://developer.billabex.com/fr/guides/pagination/
lang: fr
alternate: https://developer.billabex.com/en/guides/pagination/
last-updated: 2026-10-25
---

# Pagination et filtres

> Comprendre la pagination par curseur de Billabex et le paramètre filters, et ce que votre intégration doit implémenter.

Source: https://developer.billabex.com/fr/guides/pagination/
Langue: Français (fr)
Version anglaise: https://developer.billabex.com/en/guides/pagination/

Les endpoints de liste de l'API publique Billabex utilisent une **pagination par curseur**.
Ce guide explique le modèle, pourquoi il est conçu ainsi, et ce que votre client doit implémenter
pour paginer sans risque et efficacement en production.

## Vue d'ensemble

La pagination repose sur deux paramètres de requête :

- `first` : le nombre d'éléments à renvoyer
- `after` : le curseur opaque renvoyé par la page précédente

Chaque réponse paginée contient :

- `nodes` : les éléments de la page courante
- `pageInfo.endCursor` : le curseur pour demander la page suivante

Il n'y a ni numéro de page, ni décalage, ni total.

### Référence client dans les listes

`GET /accounts` renvoie la référence propre à l'organisation dans `reference`.
`GET /communications` la renvoie dans `accountReference`, à côté de `accountName`.
En MCP, `list-accounts` expose `reference`, et `list-invoices` et `list-communications`
exposent `accountReference`. Ces champs valent `null` sans référence disponible et permettent
d'identifier le client sans appeler le détail du compte. Ils sont distincts de `source.sourceId`,
l'identifiant du client dans le connecteur.

## Pourquoi un curseur ?

La pagination par curseur vise la **cohérence et le passage à l'échelle**. Elle évite les problèmes
classiques de la pagination par décalage quand les données changent entre deux requêtes.

Ses avantages :

- une pagination stable même quand des éléments sont créés ou supprimés ;
- pas de parcours coûteux par décalage à grande échelle ;
- des performances prévisibles sur de gros volumes.

## Choix de conception et conséquences

| Choix de conception                        | Pourquoi                                           | Ce que vous devez faire                                     |
| ------------------------------------------ | -------------------------------------------------- | ----------------------------------------------------------- |
| Pagination par curseur (`first` + `after`) | Des résultats cohérents quand les données changent | Enchaînez toujours les requêtes avec le dernier `endCursor` |
| Ni décalage ni numéro de page              | Évite les parcours coûteux en base                 | Ne construisez pas d'interface autour d'un index de page    |
| Ni `totalCount` ni `hasNextPage`           | Des réponses plus petites et plus rapides          | Arrêtez la pagination quand `nodes.length === 0`            |
| Ordre défini par l'endpoint                | L'ordre dépend du stockage et des index            | Ne présumez pas d'un tri global sans documentation          |

## Paramètres de requête

Paramètres de pagination :

| Paramètre | Type   | Obligatoire | Remarques                                                            |
| --------- | ------ | ----------- | -------------------------------------------------------------------- |
| `first`   | entier | non         | Doit valoir au moins `1` et au plus `100`                            |
| `after`   | chaîne | non         | Curseur opaque issu de `pageInfo.endCursor`                          |
| `filters` | chaîne | non         | Conditions de filtrage, voir [Filtrer une liste](#filtrer-une-liste) |

Toutes les listes plafonnent `first` à `100`, et une valeur supérieure renvoie `400`. Une page lit
une ligne par élément dans la projection : une taille de page non bornée transformerait donc un
appel unique en des milliers de lectures. Omettre `first` sur un appel qui porte un filtre ou un tri
renvoie `20` éléments. Un appel sans l'un ni l'autre conserve le comportement qu'il a toujours eu.

Les trois listes d'emails font exception au comportement historique : `GET /emails`,
`GET /incoming-emails` et `GET /outgoing-emails` renvoient toujours au plus `20` éléments
si `first` est omis, même sans filtre ni tri. Pour lire tout l'historique, enchaînez les
pages avec `after=pageInfo.endCursor` jusqu'à une réponse dont `nodes` est vide.

D'autres filtres obligatoires dépendent de l'endpoint :

- la plupart des listes exigent `organizationId` ;
- les endpoints rattachés à un compte portent `accountId` dans le chemin ;
- `GET /organizations` n'exige **pas** `organizationId`.

Reportez-vous toujours à la [référence de l'API](/fr/reference-api/) pour les exigences propres à chaque endpoint.

## Filtrer une liste

Les listes filtrables prennent un unique paramètre `filters`. Les conditions sont séparées par `;`,
chacune s'écrivant `champ:opérateur` ou `champ:opérateur:valeur,valeur` :

```
filters=status:isAnyOf:Overdue,Issued;accountName:contains:acme
```

Les règles du format :

- Les valeurs sont **encodées en pourcent**. C'est ce qui empêche une valeur contenant `;`, `,`,
  `:`, une espace ou un accent d'être lue comme un séparateur.
- Les conditions se combinent en `ET` ; plusieurs valeurs dans une même condition se combinent en `OU`.
- Un champ ne porte **qu'une seule condition**. Un champ répété renvoie `400`.
- Les champs et les opérateurs proviennent d'une liste blanche par ressource. Un champ inconnu, ou un
  opérateur que le champ n'accepte pas, renvoie `400` avec le code `LIST_FILTER_INVALID`. Rien n'est
  ignoré en silence : une faute de frappe ne peut donc jamais élargir une liste.

La webapp Billabex utilise la même chaîne dans son URL : un lien copié depuis une liste est donc un
appel d'API valide, et l'inverse est vrai aussi.

### Opérateurs

| Opérateur                                    | Valeurs   | S'applique à                          |
| -------------------------------------------- | --------- | ------------------------------------- |
| `contains`, `startsWith`, `equals`           | une       | texte                                 |
| `is`, `isNot`                                | une       | énumération, texte                    |
| `isAnyOf`                                    | plusieurs | énumération, texte                    |
| `isTrue`, `isFalse`                          | aucune    | booléen                               |
| `on`, `before`, `after`                      | une       | date                                  |
| `between`                                    | deux      | date, nombre                          |
| `greaterThan`, `lessThan`                    | une       | nombre                                |
| `isEmpty`, `isNotEmpty`                      | aucune    | tout champ nullable                   |
| `containsAny`, `containsAll`, `containsNone` | plusieurs | champs de liste, par exemple les tags |

Quelques points de sémantique à connaître :

- La comparaison de texte ignore la casse et les accents. `%` et `_` sont des caractères
  littéraux, jamais des jokers.
- Les dates s'écrivent `AAAA-MM-JJ` et se comparent par jour entier. `on` couvre la journée
  complète, `before` est strictement avant, `after` strictement après, et `between` inclut les
  deux bornes.
- Sur un champ texte, une chaîne vide compte comme vide pour `isEmpty`.
- `isTrue` et `isFalse` ne correspondent jamais à une valeur nulle.
- `isAnyOf` ne s'applique à un champ texte que là où la liste blanche de ce champ l'ouvre. Sur les
  factures et les avoirs, `number` et `sourceId` l'acceptent, donc une seule condition nomme
  plusieurs documents ; `accountName` ne l'accepte pas.
- Un numéro de document se compare sur sa forme canonique : ponctuation, espaces et casse tombent
  des deux côtés, donc `FA-2026/1` et `fa 2026 1` sont le même numéro. Une valeur à laquelle il ne
  reste rien après cela, un `-` égaré dans une liste collée, est écartée plutôt qu'appliquée,
  puisque les valeurs d'une condition se combinent en `OU`. Une condition dont toutes les valeurs
  sont écartées ne correspond à rien.

### Exemples

Texte, sur un nom de compte :

```http
GET https://next.billabex.com/api/public/v1/accounts?organizationId=ORG_ID&filters=accountName%3Acontains%3Aacme
```

Énumération, sur plusieurs statuts de facture à la fois :

```http
GET https://next.billabex.com/api/public/v1/invoices?organizationId=ORG_ID&filters=status%3AisAnyOf%3AOverdue%2CIssued
```

Plusieurs numéros de facture à la fois, au lieu d'un appel par numéro :

```http
GET https://next.billabex.com/api/public/v1/invoices?organizationId=ORG_ID&filters=number%3AisAnyOf%3AFA-2026%2F1%2CFA-2026%2F2%2CFA-2026%2F3
```

Les valeurs se séparent par `,`, donc une `,` à l'intérieur d'une valeur s'encode `%2C`, tout comme
le `/` d'un numéro. 50 valeurs au maximum par condition.

Plage de dates, sur la date d'émission d'un avoir :

```http
GET https://next.billabex.com/api/public/v1/credit-notes?organizationId=ORG_ID&filters=issuedDate%3Abetween%3A2026-01-01%2C2026-03-31
```

Montant, avec une seconde condition combinée en `ET` :

```http
GET https://next.billabex.com/api/public/v1/invoices?organizationId=ORG_ID&filters=remainingBalance%3AgreaterThan%3A1000%3Bstatus%3Ais%3AOverdue
```

Construction du paramètre en JavaScript, où `URLSearchParams` fait l'encodage pour vous :

```javascript
const filters = [
  'status:isAnyOf:Overdue,Issued',
  'accountName:contains:acme',
].join(';');

const url = new URL(`${baseUrl}/api/public/v1/invoices`);
url.searchParams.set('organizationId', organizationId);
url.searchParams.set('filters', filters);
```

Une valeur qui contient elle-même un séparateur doit être encodée séparément avant d'être jointe,
avec `encodeURIComponent`.

### Limites

| Limite                  | Valeur |
| ----------------------- | ------ |
| Conditions par requête  | 20     |
| Valeurs par condition   | 50     |
| Caractères par valeur   | 200    |
| Caractères de `filters` | 8000   |

### Champs filtrables

Ils dépendent de la ressource. La liste à jour de chaque endpoint se trouve dans la
[référence de l'API](/fr/reference-api/), dans la description de son paramètre `filters`.

| Endpoint                | Champs                                                                                                                                                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/accounts`             | `accountName`, `tags`, `dunningPaused`, `collectionStatus`, `followUpStatus`, `daysOverdue`, `overdueInvoiceCount`, `overdueBalance`, `remainingBalance`, `hasPaymentSchedule`, `paymentMethod`, `collectiveProceeding`, `source` |
| `/invoices`             | `number`, `accountName`, `tags`, `status`, `dueDate`, `paymentSchedule`, `paymentMethod`, `totalAmount`, `remainingBalance`, `channel`, `sourceId`                                                                                |
| `/credit-notes`         | `number`, `accountName`, `tags`, `status`, `issuedDate`, `paymentMethod`, `totalAmount`, `remainingAmount`, `channel`, `sourceId`                                                                                                 |
| `/communications`       | `type`, `direction`, `status`, `deliveryStatus`, `accountName`, `contactName`, `subject`, `date`                                                                                                                                  |
| `/account-tasks/search` | `title`, `accountName`, `status`, `type`, `tags`, `balance`, `createdAt`, `updatedAt`                                                                                                                                             |

Pour `/communications`, le filtre `type` accepte `is`, `isNot` et `isAnyOf` avec `IncomingEmail`, `OutgoingEmail`, `OutgoingSms`, `OutgoingTrackedLetter` et `OutgoingRegisteredLetter`. Pour les deux types d’email : `type:isAnyOf:IncomingEmail,OutgoingEmail`.

Pour les comptes, `collectionStatus` accepte `Open`, `Settled`, `WrittenOff` ou `NoInvoice`. C'est
un filtre dérivé, qui n'est pas ajouté à chaque élément de la liste. Lisez la même valeur sur
`GET /accounts/{accountId}/dunning` quand vous avez besoin du statut d'un compte précis.

`followUpStatus` filtre sur l'état de suivi affiché dans la webapp : `Active` (encours ouvert,
relances en cours), `Paused` (relances suspendues, y compris sur un compte mis en pause alors
qu'il ne doit rien, en prévision de sa prochaine facture), `NothingToCollect` (`Settled` ou
`NoInvoice`, sans suspension) et `WrittenOff`. Il sert à combiner ces états dans une seule condition, par exemple
`followUpStatus:isAnyOf:Paused,WrittenOff`, ce que deux conditions sur `collectionStatus` et
`dunningPaused`, combinées en ET, ne permettent pas.

`channel` dit par où un document est entré dans Billabex, et ce n'est jamais la source de son
compte. `sourceId` est l'identifiant que le document porte dans l'outil dont il vient, unique par
organisation, et le moyen fiable de le retrouver après une réponse perdue. Voyez
[Sources de compte](/fr/guides/sources-de-compte/).

### Deux listes conçues pour le filtrage

Deux endpoints existent pour répondre à une question à l'échelle d'une organisation plutôt que
compte par compte. Les endpoints rattachés à un compte ou à un canal qu'ils complètent restent
inchangés.

`GET /communications` renvoie les communications entrantes et sortantes dans une même liste, déjà
jointes à leur compte et à leur contact. Il prend `organizationId`, éventuellement `accountId`,
`filters`, `sortBy` (`communicationAt`, `accountFullName`, `contactFullName`, `status`),
`sortOrder`, `first` et `after`. Il exige le scope `communications:read` ou `dunning:manage`.

```http
GET https://next.billabex.com/api/public/v1/communications?organizationId=ORG_ID&filters=direction%3Ais%3AIncoming%3Bdate%3Aafter%3A2026-07-31&sortBy=communicationAt&sortOrder=desc
```

Utilisez-le plutôt que de paginer séparément les listes entrantes et sortantes pour les fusionner
côté client.

Une communication survit au compte auquel elle appartenait. Quand ce compte a été supprimé, la ligne
porte encore son `accountId` et son `accountName`, et `accountDeletedAt` donne l'instant de la
suppression. Traitez un `accountDeletedAt` non nul comme « ne pas résoudre ce compte » :
`GET /accounts/{accountId}` répond `404`. Il vaut null sur tout compte encore présent. Les listes de
communications rattachées à un compte ou à un canal portent le même fait sous forme du booléen
`accountDeleted`, qui est tout ce que leurs agrégats enregistrent.

`GET /account-tasks/search` renvoie les tâches de toute une organisation, filtrées, triées et
paginées. Voyez [Tâches de compte](/fr/guides/taches-de-compte/#rechercher-des-tâches-de-compte).

## Curseurs et filtres

Un curseur est lié aux filtres et à l'ordre de tri de l'appel qui l'a produit. Il est renvoyé sous
la forme versionnée `v2:<empreinte>:<page>`, où l'empreinte représente cet ensemble exact de
conditions et ce tri.

- Renvoyer un curseur **tel quel**, avec les mêmes `filters`, `sortBy` et `sortOrder`, donne la
  page suivante.
- Le renvoyer avec **d'autres filtres ou un autre tri** donne `400` avec le code
  `LIST_CURSOR_INVALID`. Abandonnez `after` et relisez la liste depuis sa première page.
- Les curseurs émis avant l'existence du filtrage continuent de fonctionner sur les appels qui ne
  portent **ni filtre ni tri**. Rien ne les rattachait à un jeu de résultats à l'époque, ils ne
  peuvent donc pas être honorés sur un appel filtré.
- Un curseur pris sur **un autre endpoint** renvoie `400` avec le même code `LIST_CURSOR_INVALID`,
  plutôt que de répondre discrètement la première page. Seules les listes de comptes, de factures
  et d'avoirs acceptent encore les curseurs opaques qu'elles ont émis avant le filtrage, et
  seulement sur un appel sans filtre ni tri.

Réordonner les conditions, ou les valeurs d'une même condition, ne change pas l'empreinte : seul
leur contenu compte.

## Contrat de réponse

Exemple :

```json
{
  "nodes": [{ "id": "..." }, { "id": "..." }],
  "pageInfo": {
    "endCursor": "eyJpZCI6IjEyM2U0NTY3LWU4OWItMTJkMy1hNDU2LTQyNjYxNDE3NDAwMCJ9"
  }
}
```

Trois comportements à comprendre :

- `endCursor` est un jeton de continuation, **pas** un signal qu'il reste des données ;
- la dernière page non vide peut tout de même renvoyer un `endCursor` ;
- une page vide (`nodes.length === 0`) est la seule condition d'arrêt fiable.

## Le parcours de base

Première requête :

```http
GET https://next.billabex.com/api/public/v1/accounts?organizationId=YOUR_ORG_ID&first=50
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Requête suivante, avec le curseur précédent :

```http
GET https://next.billabex.com/api/public/v1/accounts?organizationId=YOUR_ORG_ID&first=50&after=PREVIOUS_END_CURSOR
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Répétez jusqu'à obtenir une page vide.

## Motif client recommandé

L'exemple ci-dessous montre une boucle de pagination sûre, prête pour la production.

```javascript
async function fetchAllAccounts({ baseUrl, accessToken, organizationId }) {
  const all = [];
  let after;

  while (true) {
    const url = new URL(`${baseUrl}/api/public/v1/accounts`);
    url.searchParams.set('organizationId', organizationId);
    url.searchParams.set('first', '50');
    if (after) url.searchParams.set('after', after);

    const response = await fetch(url.toString(), {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    // Facultatif : s'articuler avec la limitation de débit
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get('Retry-After') || '1');
      await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
      continue;
    }

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();
    all.push(...data.nodes);

    if (!Array.isArray(data.nodes) || data.nodes.length === 0) {
      break;
    }

    after = data.pageInfo?.endCursor;
    if (!after) {
      break;
    }
  }

  return all;
}
```

## Choisir une taille de page (`first`)

Il n'existe pas de valeur optimale universelle.

`first` influe directement sur :

- la taille de la réponse ;
- la latence ;
- la mémoire consommée côté client ;
- la consommation du quota de débit.

Valeurs de départ recommandées :

- `20` à `50` pour un parcours piloté par une interface ;
- `50` à `100` pour un traitement par lots côté serveur.

Ajustez selon la taille réelle des charges utiles et votre trafic.

## Interaction avec la limitation de débit

La pagination fait vite monter le volume de requêtes.

Bonnes pratiques :

- préférez peu de grandes pages à beaucoup de petites ;
- combinez la pagination avec une file de requêtes par jeton ;
- surveillez les en-têtes `X-RateLimit-Remaining` ;
- évitez de paginer en parallèle avec le même jeton d'accès.

Le [guide des limites de débit](/fr/guides/limites-de-debit/) entre dans le détail.

## Erreurs fréquentes

- Traiter un curseur comme un identifiant lisible ou stable
- Réutiliser un curseur après avoir changé les filtres ou le tri, ce qui renvoie désormais `400`
- Mélanger des curseurs de différents endpoints
- Croire qu'un `endCursor` absent signifie « plus de données »
- Demander de très grandes pages par défaut

## Endpoints paginés de l'API publique

Parmi les plus courants :

- `GET /organizations`
- `GET /accounts`
- `GET /invoices`
- `GET /credit-notes`
- `GET /account-tasks/search`
- `GET /communications`
- `GET /emails`
- `GET /incoming-emails`
- `GET /outgoing-emails`
- `GET /incoming-email-communications`
- `GET /outgoing-email-communications`
- `GET /customer-outstanding-balances`
- `GET /accounts/:accountId/incoming-email-communications`
- `GET /accounts/:accountId/outgoing-email-communications`

Consultez toujours la référence de l'API pour la liste qui fait foi.

## Pour aller plus loin

- **[Démarrage](/fr/guides/demarrage/)** : le parcours d'intégration de bout en bout
- **[Authentification OAuth](/fr/guides/oauth/)** : cycle de vie du jeton et scopes
- **[Limites de débit](/fr/guides/limites-de-debit/)** : quota par jeton et comportement de reprise
- **[Référence de l'API](/fr/reference-api/)** : schémas et exemples des endpoints

## Support

Une question sur la pagination ?
Écrivez-nous via le [formulaire de contact](https://www.billabex.com/fr/contactez-nous/).

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
