SDK e client
Client TypeScript e Python pronti all'uso, collezione Postman, specifica OpenAPI e generazione automatica dei client.
Blu Pantheon non impone l'uso di una libreria specifica: le API sono REST standard e funzionano con qualsiasi client HTTP. In questa pagina trovi implementazioni di riferimento complete, la collezione Postman e la specifica OpenAPI da cui generare client per qualsiasi linguaggio.
Prima di iniziare
- Una chiave API attiva creata dalla Console
- Node.js 20 o superiore per il client TypeScript
- Python 3.11 o superiore per il client Python
Variabili di ambiente
Tutti gli esempi leggono la configurazione dall'ambiente. Non inserire mai valori letterali nel codice.
| Variabile | Obbligatoria | Default | Descrizione | Esempio |
|---|---|---|---|---|
BP_API_KEY | Sì | — | Chiave API usata per autenticare le chiamate. Conservala nel secret manager, mai nel repository. | bp_live_xxxxxxxxxxxxxxxx |
BP_BASE_URL | No | — | Base URL del servizio. Usa l’host sandbox durante lo sviluppo. | https://api.blupantheon.com |
BP_TENANT_SLUG | No | — | Slug del tenant. Necessario solo se la credenziale ha accesso a più tenant. | acme |
BP_WEBHOOK_SECRET | No | — | Segreto condiviso per verificare la firma dei webhook in ingresso. | whsec_9c4f2a1e8b7d0345 |
BP_TIMEOUT_MS | No | — | Timeout delle richieste in millisecondi. Valore consigliato tra 10000 e 30000. | 15000 |
BP_MAX_RETRIES | No | — | Numero massimo di ritentativi sugli errori temporanei 429 e 5xx. | 4 |
Client TypeScript
Client completo basato su fetch, senza dipendenze esterne: autenticazione, timeout, ritentativi con
backoff, errori tipizzati e helper di paginazione.
export interface ApiErrorDetail {
field?: string;
code?: string;
message?: string;
}
export interface ApiErrorBody {
code: string;
title: string;
message: string;
traceId: string;
details?: ApiErrorDetail[];
}
export class BluPantheonError extends Error {
readonly code: string;
readonly status: number;
readonly traceId: string;
readonly details: ApiErrorDetail[];
constructor(status: number, body: ApiErrorBody) {
super(`${body.code}: ${body.message}`);
this.name = 'BluPantheonError';
this.code = body.code;
this.status = status;
this.traceId = body.traceId;
this.details = body.details ?? [];
}
get isRetryable(): boolean {
return [429, 502, 503, 504].includes(this.status);
}
}
export interface Page<T> {
data: T[];
pagination: { nextCursor: string | null; hasMore: boolean; total?: number };
}
export interface ClientOptions {
apiKey: string;
baseUrl?: string;
timeoutMs?: number;
maxRetries?: number;
}
type Query = Record<string, string | number | boolean | undefined>;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export class BluPantheonClient {
private readonly apiKey: string;
private readonly baseUrl: string;
private readonly timeoutMs: number;
private readonly maxRetries: number;
constructor(options: ClientOptions) {
this.apiKey = options.apiKey;
this.baseUrl = (options.baseUrl ?? 'https://api.blupantheon.com').replace(/\/$/, '');
this.timeoutMs = options.timeoutMs ?? 15_000;
this.maxRetries = options.maxRetries ?? 4;
}
async request<T>(
method: string,
path: string,
init: { query?: Query; body?: unknown; idempotencyKey?: string } = {},
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
for (const [key, value] of Object.entries(init.query ?? {})) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
const headers: Record<string, string> = {
Authorization: `Bearer ${this.apiKey}`,
Accept: 'application/json',
};
if (init.body !== undefined) headers['Content-Type'] = 'application/json';
if (init.idempotencyKey) headers['Idempotency-Key'] = init.idempotencyKey;
let lastError: unknown;
for (let attempt = 1; attempt <= this.maxRetries + 1; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
body: init.body === undefined ? undefined : JSON.stringify(init.body),
signal: controller.signal,
});
if (response.status === 204) return undefined as T;
const payload = await response.json();
if (response.ok) return payload as T;
const error = new BluPantheonError(response.status, payload as ApiErrorBody);
if (!error.isRetryable || attempt > this.maxRetries) throw error;
await sleep(this.backoffMs(attempt, response.headers.get('Retry-After')));
lastError = error;
} catch (error) {
if (error instanceof BluPantheonError) {
if (!error.isRetryable || attempt > this.maxRetries) throw error;
lastError = error;
} else {
if (attempt > this.maxRetries) throw error;
lastError = error;
await sleep(this.backoffMs(attempt, null));
}
} finally {
clearTimeout(timer);
}
}
throw lastError ?? new Error('Numero massimo di tentativi superato');
}
// Backoff esponenziale con jitter, rispettando Retry-After quando presente.
private backoffMs(attempt: number, retryAfter: string | null): number {
const serverDelay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 0;
const backoff = Math.min(500 * 2 ** (attempt - 1), 30_000);
return Math.max(serverDelay, Math.random() * backoff);
}
async *paginate<T>(path: string, query: Query = {}): AsyncGenerator<T, void, undefined> {
let cursor: string | null = null;
do {
const page = await this.request<Page<T>>('GET', path, {
query: { limit: 200, ...query, cursor: cursor ?? undefined },
});
for (const item of page.data) yield item;
cursor = page.pagination.nextCursor;
} while (cursor);
}
get<T>(path: string, query?: Query): Promise<T> {
return this.request<T>('GET', path, { query });
}
post<T>(path: string, body: unknown, idempotencyKey?: string): Promise<T> {
return this.request<T>('POST', path, { body, idempotencyKey });
}
patch<T>(path: string, body: unknown, idempotencyKey?: string): Promise<T> {
return this.request<T>('PATCH', path, { body, idempotencyKey });
}
delete<T>(path: string, idempotencyKey?: string): Promise<T> {
return this.request<T>('DELETE', path, { idempotencyKey });
}
}Uso:
import { randomUUID } from 'node:crypto';
import { BluPantheonClient, BluPantheonError } from './blupantheon.js';
const bp = new BluPantheonClient({
apiKey: process.env.BP_API_KEY!,
baseUrl: process.env.BP_BASE_URL,
});
try {
const me = await bp.get<{ email: string }>('/identity/v1/me');
console.log('Autenticato come', me.email);
const cliente = await bp.post(
'/console/v1/clients',
{ email: 'mario.rossi@acme.it', firstName: 'Mario', lastName: 'Rossi' },
randomUUID(),
);
console.log('Cliente creato', cliente);
for await (const sessione of bp.paginate('/console/v1/sessions', {
projectId: 'bp_proj_7f3a9c21',
status: 'open',
})) {
console.log(sessione);
}
} catch (error) {
if (error instanceof BluPantheonError) {
console.error(`[${error.code}] ${error.message} traceId=${error.traceId}`);
for (const detail of error.details) console.error(` - ${detail.field}: ${detail.message}`);
} else {
throw error;
}
}Client Python
Basato su requests.Session con HTTPAdapter e Retry, che gestisce nativamente il backoff e
l'header Retry-After.
import os
import uuid
from typing import Any, Iterator
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class BluPantheonError(Exception):
def __init__(self, status: int, body: dict[str, Any]) -> None:
super().__init__(f"{body.get('code')}: {body.get('message')}")
self.status = status
self.code = body.get("code", "UNKNOWN")
self.trace_id = body.get("traceId", "")
self.details = body.get("details", [])
class BluPantheonClient:
def __init__(
self,
api_key: str | None = None,
base_url: str | None = None,
timeout: float = 15.0,
max_retries: int = 4,
) -> None:
self.api_key = api_key or os.environ["BP_API_KEY"]
self.base_url = (base_url or os.getenv("BP_BASE_URL", "https://api.blupantheon.com")).rstrip("/")
self.timeout = timeout
retry = Retry(
total=max_retries,
backoff_factor=0.5,
status_forcelist=[429, 502, 503, 504],
allowed_methods=["GET", "POST", "PATCH", "PUT", "DELETE"],
respect_retry_after_header=True,
raise_on_status=False,
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
)
adapter = HTTPAdapter(max_retries=retry, pool_maxsize=16)
self.session.mount("https://", adapter)
def request(
self,
method: str,
path: str,
params: dict[str, Any] | None = None,
json: Any = None,
idempotency_key: str | None = None,
) -> Any:
headers: dict[str, str] = {}
if json is not None:
headers["Content-Type"] = "application/json"
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
response = self.session.request(
method,
f"{self.base_url}{path}",
params=params,
json=json,
headers=headers,
timeout=self.timeout,
)
if response.status_code == 204:
return None
payload = response.json()
if not response.ok:
raise BluPantheonError(response.status_code, payload)
return payload
def get(self, path: str, **params: Any) -> Any:
return self.request("GET", path, params=params)
def post(self, path: str, json: Any, idempotency_key: str | None = None) -> Any:
return self.request("POST", path, json=json, idempotency_key=idempotency_key or str(uuid.uuid4()))
def patch(self, path: str, json: Any, idempotency_key: str | None = None) -> Any:
return self.request("PATCH", path, json=json, idempotency_key=idempotency_key or str(uuid.uuid4()))
def paginate(self, path: str, **filters: Any) -> Iterator[dict[str, Any]]:
cursor: str | None = None
while True:
params: dict[str, Any] = {"limit": 200, **filters}
if cursor:
params["cursor"] = cursor
payload = self.request("GET", path, params=params)
yield from payload["data"]
pagination = payload["pagination"]
if not pagination.get("hasMore"):
return
cursor = pagination["nextCursor"]Uso:
from blupantheon import BluPantheonClient, BluPantheonError
bp = BluPantheonClient()
try:
me = bp.get("/identity/v1/me")
print("Autenticato come", me["email"])
cliente = bp.post(
"/console/v1/clients",
{"email": "mario.rossi@acme.it", "firstName": "Mario", "lastName": "Rossi"},
)
print("Cliente creato", cliente["id"])
for sessione in bp.paginate("/console/v1/sessions", projectId="bp_proj_7f3a9c21", status="open"):
print(sessione["id"], sessione["channel"])
except BluPantheonError as error:
print(f"[{error.code}] {error} traceId={error.trace_id}")
for detail in error.details:
print(" -", detail.get("field"), detail.get("message"))Collezione Postman
La collezione ufficiale contiene tutti gli endpoint documentati, già organizzati per servizio, con esempi di richiesta precompilati.
Scarica i file
Dalla Console scarichi due file: la collezione e l'ambiente di esempio.
URL collezione
https://api.blupantheon.com/openapi/postman/collection.jsonSempre allineata alla versione corrente delle API.
Importa in Postman
In Postman scegli Import, poi Link e incolla l'URL della collezione, oppure trascina il file scaricato nella finestra.
Configura le variabili di ambiente
Crea un ambiente per ciascun contesto (sandbox, staging, produzione) e valorizza queste variabili:
baseUrl = https://api.sandbox.blupantheon.com
apiKey = bp_test_xxxxxxxxxxxxxxxx
tenantSlug = acme
projectId = bp_proj_7f3a9c21Nelle richieste della collezione i riferimenti sono già impostati come {{baseUrl}} e {{apiKey}}:
l'header di autorizzazione è definito a livello di collezione come Bearer {{apiKey}}, quindi non devi
ripeterlo in ogni richiesta.
Proteggi il segreto
Imposta la variabile apiKey come secret nel tipo della variabile e valorizzala solo nel campo
Current value, mai in Initial value: quest'ultimo viene sincronizzato con il workspace condiviso.
Non esportare ambienti con credenziali
Un file di ambiente Postman esportato contiene i valori in chiaro. Non allegarlo a ticket, non committarlo e non condividerlo in chat: usa sempre chiavi di sandbox per le demo.
OpenAPI
La specifica OpenAPI 3.1 è pubblicata per ogni servizio e aggiornata a ogni rilascio.
| Servizio | URL della specifica |
|---|---|
| Identity | https://api.blupantheon.com/identity/v1/openapi.json |
| Console | https://api.blupantheon.com/console/v1/openapi.json |
| Billing | https://api.blupantheon.com/billing/v1/openapi.json |
| Booking | https://api.blupantheon.com/booking/v1/openapi.json |
| Specifica unificata | https://api.blupantheon.com/openapi.json |
Le specifiche sono pubbliche e non richiedono autenticazione.
Generare un client
npm install --global @openapitools/openapi-generator-cliopenapi-generator-cli generate \
-i https://api.blupantheon.com/console/v1/openapi.json \
-g typescript-fetch \
-o ./src/generated/blupantheon \
--additional-properties=supportsES6=true,withInterfaces=trueRigenera a ogni aggiornamento
Fissa la generazione del client in una fase della pipeline di build e committa il codice generato solo se la tua policy lo richiede. In alternativa scarica la specifica in un file versionato e rigenera in modo riproducibile, così una modifica lato piattaforma non altera silenziosamente il tuo build.
Validazione locale
Puoi validare la specifica prima di generare, utile in pipeline:
openapi-generator-cli validate \
-i https://api.blupantheon.com/openapi.jsonClient comunitari
I progetti seguenti non sono mantenuti da Blu Pantheon e non sono coperti dal supporto. Valutane attentamente manutenzione e sicurezza prima dell'adozione in produzione.
| Linguaggio | Progetto | Stato |
|---|---|---|
| PHP | blupantheon-php | Comunitario, copre Identity e Console |
| Go | go-blupantheon | Comunitario, copre Console |
| Java | blupantheon-java | Generato da OpenAPI, aggiornamento manuale |
Verifica sempre i client di terze parti
Un client non ufficiale può gestire in modo scorretto ritentativi, timeout e memorizzazione delle
credenziali. Prima dell'uso in produzione verifica che non registri le chiavi nei log e che rispetti
l'header Retry-After.