Paginazione
Paginazione a cursore delle API Blu Pantheon: parametri, struttura della risposta, filtri, ordinamento e cicli completi.
Tutti gli endpoint che restituiscono collezioni usano una paginazione a cursore. Il client non richiede un numero di pagina ma un puntatore opaco alla posizione successiva, restituito dalla chiamata precedente. Questo garantisce risultati coerenti anche quando la collezione cambia durante la lettura.
Parametri di query
Parametri di paginazione
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
limit | number | No | 50 | Numero di elementi per pagina. Valori ammessi da 1 a 200. Un valore fuori intervallo produce VALIDATION_ERROR. |
cursor | string | No | — | Puntatore opaco alla pagina successiva, copiato da pagination.nextCursor. Omettilo per la prima pagina. |
sort | string | No | createdAt | Campo di ordinamento. I valori ammessi variano per risorsa e sono indicati nel riferimento di ogni endpoint. |
order | string | No | desc | Direzione dell’ordinamento: asc oppure desc. |
Il cursore incorpora l'ordinamento
Un cursore è valido solo per la combinazione di sort, order e filtri con cui è stato generato.
Se cambi uno di questi parametri devi ripartire dalla prima pagina, altrimenti la risposta è 400
con codice INVALID_CURSOR.
Struttura della risposta
Ogni risposta paginata ha la stessa forma: un array data e un oggetto pagination.
{
"data": [
{
"id": "sess_9f2c8d41",
"projectId": "bp_proj_7f3a9c21",
"clientId": "cli_5e1a7b30",
"channel": "web",
"status": "open",
"messageCount": 14,
"startedAt": "2026-09-04T09:12:44.006Z",
"lastActivityAt": "2026-09-04T09:41:02.884Z",
"createdAt": "2026-09-04T09:12:44.006Z"
},
{
"id": "sess_3b7e0c58",
"projectId": "bp_proj_7f3a9c21",
"clientId": "cli_9d4f2a17",
"channel": "whatsapp",
"status": "closed",
"messageCount": 6,
"startedAt": "2026-09-04T08:55:10.412Z",
"lastActivityAt": "2026-09-04T09:03:37.221Z",
"createdAt": "2026-09-04T08:55:10.412Z"
}
],
"pagination": {
"nextCursor": "eyJpZCI6InNlc3NfM2I3ZTBjNTgiLCJ0cyI6MTc4ODUxNTUzNX0",
"hasMore": true,
"total": 1234
}
}Oggetto pagination
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
nextCursor | string | null | Sì | — | Cursore da passare alla chiamata successiva. Vale null quando non ci sono altre pagine. |
hasMore | boolean | Sì | — | True se esistono altri elementi oltre quelli restituiti. È la condizione di uscita del ciclo. |
total | number | No | — | Numero totale di elementi che soddisfano i filtri. Può essere assente o approssimato su collezioni molto grandi. |
Usa hasMore, non total
Il campo total è una stima calcolata al momento della prima chiamata e può essere omesso quando il
conteggio esatto sarebbe troppo costoso. La condizione corretta per continuare il ciclo è
hasMore uguale a true, oppure nextCursor diverso da null.
Ciclo di paginazione completo
const BASE_URL = 'https://api.blupantheon.com';
const API_KEY = process.env.BP_API_KEY!;
interface Page<T> {
data: T[];
pagination: { nextCursor: string | null; hasMore: boolean; total?: number };
}
export async function* paginate<T>(
path: string,
query: Record<string, string | number> = {},
): AsyncGenerator<T, void, undefined> {
let cursor: string | null = null;
do {
const params = new URLSearchParams({
limit: '200',
...Object.fromEntries(Object.entries(query).map(([k, v]) => [k, String(v)])),
});
if (cursor) params.set('cursor', cursor);
const res = await fetch(`${BASE_URL}${path}?${params}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${err.code}: ${err.message} (traceId ${err.traceId})`);
}
const page = (await res.json()) as Page<T>;
for (const item of page.data) yield item;
cursor = page.pagination.nextCursor;
} while (cursor);
}
// Uso: scorre tutte le sessioni aperte del progetto senza caricarle tutte in memoria.
for await (const session of paginate('/console/v1/sessions', {
projectId: 'bp_proj_7f3a9c21',
status: 'open',
})) {
console.log(session);
}Filtri comuni
I filtri si combinano liberamente con la paginazione e si applicano prima dell'ordinamento. I filtri non riconosciuti vengono ignorati silenziosamente: verifica sempre di aver scritto correttamente il nome.
Filtri disponibili sulla maggior parte delle collezioni
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
createdAfter | string | No | — | Data ISO 8601. Include solo gli elementi creati dopo l’istante indicato, estremo escluso. |
createdBefore | string | No | — | Data ISO 8601. Include solo gli elementi creati prima dell’istante indicato, estremo escluso. |
updatedAfter | string | No | — | Data ISO 8601. Utile per le sincronizzazioni incrementali basate sull’ultima modifica. |
status | string | No | — | Filtra per stato della risorsa. Accetta più valori separati da virgola, ad esempio open,pending. |
projectId | string | No | — | Limita i risultati a un singolo progetto, ad esempio bp_proj_7f3a9c21. |
clientId | string | No | — | Limita i risultati alle risorse associate a uno specifico cliente. |
channel | string | No | — | Canale di origine: web, whatsapp, email, telegram, api. |
q | string | No | — | Ricerca testuale libera sui campi indicizzati della risorsa. Minimo 2 caratteri. |
Esempio di combinazione tipica per una sincronizzazione incrementale notturna:
curl -sS -G "https://api.blupantheon.com/console/v1/clients" \
-H "Authorization: Bearer bp_live_xxxxxxxxxxxxxxxx" \
--data-urlencode "updatedAfter=2026-09-03T00:00:00Z" \
--data-urlencode "status=active" \
--data-urlencode "sort=updatedAt" \
--data-urlencode "order=asc" \
--data-urlencode "limit=200"Sincronizzazioni incrementali
Per le sincronizzazioni usa updatedAfter con sort=updatedAt e order=asc: leggi in avanti nel
tempo e memorizza l'updatedAt dell'ultimo elemento elaborato come punto di ripartenza. Sottrai un
margine di 60 secondi al valore memorizzato per non perdere record scritti a cavallo della finestra.
Ordinamento
| Parametro | Valori | Comportamento |
|---|---|---|
sort | createdAt, updatedAt, name, lastActivityAt | Il set di valori ammessi dipende dalla risorsa |
order | asc, desc | Predefinito desc |
L'ordinamento è sempre totale e deterministico: a parità di valore del campo scelto viene applicato un criterio secondario sull'identificativo della risorsa. Questo garantisce che nessun elemento venga saltato o restituito due volte durante la paginazione.
Un valore di sort non supportato produce 400 con codice VALIDATION_ERROR e l'elenco dei campi
ammessi in details.
Limiti massimi
| Vincolo | Valore | Comportamento oltre il limite |
|---|---|---|
limit massimo | 200 | 400 VALIDATION_ERROR |
limit minimo | 1 | 400 VALIDATION_ERROR |
| Validità del cursore | 15 minuti | 400 INVALID_CURSOR |
| Profondità di scorrimento | Nessun limite | — |
Lunghezza minima di q | 2 caratteri | 400 VALIDATION_ERROR |
Stabilità del cursore
Il cursore è opaco e temporaneo
Il valore di nextCursor è una stringa codificata il cui formato interno può cambiare senza preavviso.
Non decodificarlo, non modificarlo, non costruirlo manualmente e non salvarlo in modo persistente per
riprendere una lettura il giorno successivo: dopo 15 minuti scade e restituisce INVALID_CURSOR.
Per riprendere una lettura interrotta usa un filtro temporale (createdAfter o updatedAfter).
Durante una paginazione la collezione sottostante può cambiare. Il comportamento è il seguente:
- Gli elementi creati dopo l'inizio del ciclo non compaiono se l'ordinamento è discendente su
createdAt; compaiono in coda se l'ordinamento è ascendente. - Gli elementi eliminati durante il ciclo semplicemente non vengono restituiti: la pagina può contenere meno elementi di
limitpur avendohasMorea true. - Gli elementi modificati durante il ciclo possono essere restituiti due volte se ordini per
updatedAtin ordine ascendente. Deduplica sempre in base all'idquando elabori grandi volumi.
Una pagina corta non significa fine dei risultati
Non interrompere il ciclo perché una pagina contiene meno elementi di limit. L'unica condizione di
uscita corretta è hasMore uguale a false oppure nextCursor uguale a null.
Errori frequenti
Il cursore è scaduto, malformato oppure appartiene a una combinazione di filtri diversa. Riparti dalla prima pagina mantenendo gli stessi parametri per l'intero ciclo.
Accade quando si riusa lo stesso cursore a ogni iterazione. Assicurati di aggiornare la variabile con
pagination.nextCursor della risposta corrente, non con il valore iniziale.
Con limit=200 e un ciclo stretto puoi saturare la quota. Applica il backoff descritto in
Rate limiting e limita la concorrenza a poche richieste parallele.