Interrogation : filtres, tri et pagination
Ce guide montre comment filtrer une liste de fiches sur n'importe quel champ avec l'un des 11 opérateurs pris en charge, combiner des conditions que la simple chaîne de requête ne peut pas exprimer — ET/OU mixtes — via POST /{module}/search, trier et paginer les résultats, et éviter la poignée d'erreurs qui produisent un 400. Il vous faut seulement un jeton d'API et votre URL de base, comme indiqué dans le guide de démarrage ; pour les noms de champs, les types et les formats de valeurs, consultez Champs, formats et mise en forme des réponses — ce guide traite uniquement de la restriction, du tri et de la pagination d'une liste, pas de l'apparence de la valeur d'un champ.
1. Filtrer sur un champ
filter[field]=value est une simple correspondance d'égalité :
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'filter[lastname]=Dupont' | jq '{total: .meta.total, first: .data[0].attributes.lastname}'{
"total": 1,
"first": "Dupont"
}filter[lastname][eq]=Dupont renvoie un résultat identique — eq n'est que l'écriture explicite de ce que filter[field]=value seul signifie déjà.
2. Le tableau des opérateurs
Chaque champ n'accepte qu'un seul de ces 11 opérateurs à la fois. filter[field][op]=value est la forme générale ; empty/not_empty ne prennent pas de valeur réelle (tout ce qui suit le =, y compris rien du tout, est ignoré — seule la présence/absence de l'opérateur compte), et in prend une liste séparée par des virgules dans la chaîne de requête (un véritable tableau JSON ne fonctionne que dans le corps de la requête search — voir l'étape 4).
| Opérateur | Ce qui correspond | Exemple |
|---|---|---|
eq | Exactement égal à la valeur. Identique à un simple filter[field]=value. | filter[lastname][eq]=Dupont |
neq | Tout sauf la valeur. | filter[lastname][neq]=Dupont |
contains | La valeur apparaît n'importe où dans le champ. | filter[lastname][contains]=upon |
starts_with | Le champ commence par la valeur. | filter[lastname][starts_with]=Dup |
gte | Supérieur ou égal à la valeur. | filter[modifiedtime][gte]=2026-01-01T00:00:00Z |
lte | Inférieur ou égal à la valeur. | filter[modifiedtime][lte]=2026-12-31T23:59:59Z |
gt | Strictement supérieur à la valeur. | filter[modifiedtime][gt]=2026-01-01T00:00:00Z |
lt | Strictement inférieur à la valeur. | filter[modifiedtime][lt]=2026-12-31T23:59:59Z |
empty | Le champ n'a aucune valeur. Ne prend pas de valeur propre. | filter[email][empty] |
not_empty | Le champ a une valeur, quelle qu'elle soit. Ne prend pas de valeur propre. | filter[email][not_empty] |
in | Le champ est égal à l'une des valeurs d'une liste séparée par des virgules. | filter[lastname][in]=Dupont,Martin |
3. Plusieurs champs à la fois
Plusieurs paramètres filter[field]=value différents dans la même requête sont toujours combinés avec ET — chaque condition doit correspondre :
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'filter[lastname]=Dupont' --data-urlencode 'filter[firstname]=Jean' \
| jq '{total: .meta.total}'{ "total": 1 }Remplacez par un firstname qui n'appartient pas au même contact et le ET entre en jeu comme prévu — 0 résultat, même si lastname=Dupont seul correspond toujours :
{ "total": 0 }Il n'y a aucun moyen d'obtenir un OU entre plusieurs champs depuis la chaîne de requête. Si vous avez besoin de « lastname est Dupont OU fait partie de cette autre liste », il vous faut l'endpoint de recherche — voir la section suivante.
4. Quand la forme simple ne suffit plus
Mettre deux opérateurs sur le même champ est l'erreur la plus courante de cette page — et elle est rejetée, pas fusionnée silencieusement :
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'filter[modifiedtime][gte]=2026-01-01T00:00:00Z' \
--data-urlencode 'filter[modifiedtime][lte]=2026-06-30T23:59:59Z' \
-w '\n%{http_code}\n'{"error":{"code":"INVALID_FILTER","message":"Malformed filter for 'modifiedtime': expected exactly one operator.","request_id":"e12d2fd4-7468-4cb1-9d9a-9e1e021972bb"}}
400Un champ ne peut porter qu'un seul opérateur dans la forme simple en chaîne de requête — aucune syntaxe de chaîne de requête ne permet d'exprimer un « entre ». Pour une plage sur un champ, ou pour une logique OU entre plusieurs champs, utilisez plutôt POST /{module}/search. Son filtre a deux niveaux : filter.glue relie les groups de premier niveau, et le glue propre à chaque groupe relie les conditions à l'intérieur de ce groupe :
curl -sS -X POST "https://app.initiative-crm.com/api/v1/contacts/search" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"filter": {
"glue": "AND",
"groups": [
{ "glue": "AND", "conditions": [
{ "field": "modifiedtime", "op": "gte", "value": "2026-01-01T00:00:00Z" },
{ "field": "modifiedtime", "op": "lte", "value": "2026-12-31T23:59:59Z" } ] },
{ "glue": "OR", "conditions": [
{ "field": "lastname", "op": "contains", "value": "Dupont" },
{ "field": "lastname", "op": "in", "value": ["Martin", "Bernard"] } ] }
]
},
"sort": "modifiedtime", "order": "desc", "page": 1, "per_page": 5,
"fields": ["firstname", "lastname", "email"]
}' | jq '{total: .meta.total, page: .meta.page, keys: (.data[0].attributes | keys)}'{
"total": 1,
"page": 1,
"keys": ["createdtime", "email", "firstname", "lastname", "modifiedtime"]
}Lisez ceci comme : tout ce qui correspond au premier groupe (modifiedtime entre les deux bornes) ET tout ce qui correspond au second groupe (lastname contient « Dupont » OU lastname est l'un de « Martin »/« Bernard »). Deux choses à noter : in prend ici un véritable tableau JSON (["Martin", "Bernard"]), et non la chaîne séparée par des virgules utilisée dans la forme en chaîne de requête ; et fields renvoie toujours createdtime/modifiedtime en plus des trois champs demandés — le même comportement « toujours inclus » documenté dans Champs, formats et mise en forme des réponses. Notez aussi que chaque groupe d'une liste reliée par ET doit être satisfait — restreindre le groupe modifiedtime à une fenêtre qui se termine avant la dernière modification d'une fiche renvoie légitimement 0, même si cette fiche correspond au groupe OU à elle seule.
5. Tri
sort=<field>&order=asc|desc. Si vous omettez les deux, vous obtenez le comportement par défaut : modifiedtime décroissant (les modifications les plus récentes en premier).
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'sort=lastname' --data-urlencode 'order=asc' \
--data-urlencode 'per_page=5' --data-urlencode 'fields=lastname' \
--data-urlencode 'filter[lastname][in]=Bernard,Dupont,Girard,Martin,Petit' \
| jq '[.data[].attributes.lastname]'["Bernard", "Dupont", "Girard", "Martin", "Petit"]curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'sort=lastname' --data-urlencode 'order=desc' \
--data-urlencode 'per_page=5' --data-urlencode 'fields=lastname' \
--data-urlencode 'filter[lastname][in]=Bernard,Dupont,Girard,Martin,Petit' \
| jq '[.data[].attributes.lastname]'["Petit", "Martin", "Girard", "Dupont", "Bernard"]Un champ de tri inconnu renvoie un 400, de la même forme qu'un champ de filtre inconnu :
{"code":"INVALID_FILTER","message":"Unknown sort field 'not_a_field'.","request_id":"38595338-0b68-4d0f-88d5-cc697582a61e"}6. Pagination
page (indexé à partir de 1) et per_page contrôlent la fenêtre ; le meta de la réponse porte toujours total, page, per_page et pages :
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'per_page=1' --data-urlencode 'page=1' | jq '.meta'{ "total": 222, "page": 1, "per_page": 1, "pages": 222, "request_id": "561b7019-ad41-4c09-97ec-d8c2ad5e81db" }curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'per_page=1' --data-urlencode 'page=2' | jq '.meta'{ "total": 222, "page": 2, "per_page": 1, "pages": 222, "request_id": "f5c5f66e-c510-4600-83a0-75ab7c1dcb9b" }per_page est plafonné à 100, silencieusement — demander plus ne provoque pas d'erreur, la valeur est simplement écrêtée :
curl -sS -G -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/contacts" \
--data-urlencode 'per_page=500' | jq '.meta.per_page'100
La valeur par défaut (en l'absence de per_page) est 20.
Avertissement : par défaut, les fiches sont triées par modifiedtime décroissant. Si vous parcourez toutes les pages d'une grande liste et que quelque chose est modifié entre deux requêtes de page, le modifiedtime de cette fiche la déplace — elle peut basculer dans une page que vous avez déjà récupérée, ou sortir d'une page que vous n'avez pas encore atteinte ; un parcours de pages long avec le tri par défaut peut donc silencieusement sauter ou dupliquer des lignes. Pour un parcours stable, triez plutôt par id (sort=id&order=asc), qui ne change pas lorsque des fiches sont modifiées. Si ce dont vous avez réellement besoin est « tout ce qui a changé depuis la dernière fois », consultez Synchroniser un autre système pour le schéma sûr de synchronisation, plutôt qu'un simple parcours de pages.
7. Moins de champs
fields=a,b,c restreint ce qui revient sur ce même endpoint de liste/recherche — consultez Champs, formats et mise en forme des réponses pour les règles complètes (cela n'affecte pas un GET d'une seule fiche, et un nom de champ inconnu ici renvoie un 400).
Erreurs courantes
- Deux opérateurs sur un même champ.
filter[modifiedtime][gte]=…&filter[modifiedtime][lte]=…est rejeté :400 INVALID_FILTER,"Malformed filter for 'modifiedtime': expected exactly one operator."Il n'existe pas de syntaxe de plage en chaîne de requête — utilisezPOST /{module}/searchpour une condition de type « entre » sur un champ. - Un champ de filtre inconnu.
filter[not_a_field]=x→400 INVALID_FILTER,"Unknown filter field 'not_a_field'." - Un champ de tri inconnu.
sort=not_a_field→400 INVALID_FILTER,"Unknown sort field 'not_a_field'." orderautre queasc/desc.order=sideways→400 INVALID_FILTER,"order must be 'asc' or 'desc', got 'sideways'."- S'attendre à un OU depuis la chaîne de requête. Chaque
filter[field]=valueque vous ajoutez est relié aux autres par ET — il n'y a aucun moyen d'exprimer un OU sans passer parPOST /{module}/search(étape 4). - Envoyer
insous forme de tableau dans la chaîne de requête.filter[lastname][in][]=Dupont&filter[lastname][in][]=Martinéchoue :400 INVALID_FILTER,"Malformed filter for 'lastname': operator value must be scalar."Dans la chaîne de requête,inest toujours une chaîne séparée par des virgules (filter[lastname][in]=Dupont,Martin) ; la forme en véritable tableau n'existe que dans le corps de la requêtesearch. (L'inverse — envoyerinsous forme de chaîne séparée par des virgules à l'intérieur du corps de recherche au lieu d'un tableau — n'est en fait pas rejeté : l'endpoint accepte les deux formes à cet endroit. La forme en tableau est celle qui est documentée, préférez-la donc, mais ne soyez pas surpris si une chaîne CSV fonctionne aussi dans un corps que vous n'avez pas écrit vous-même.)
Updated about 1 month ago
