Blu Pantheon
Справочник API

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.

VariabileObbligatoriaDefaultDescrizioneEsempio
BP_API_KEYSì—Chiave API usata per autenticare le chiamate. Conservala nel secret manager, mai nel repository.bp_live_xxxxxxxxxxxxxxxx
BP_BASE_URLNo—Base URL del servizio. Usa l’host sandbox durante lo sviluppo.https://api.blupantheon.com
BP_TENANT_SLUGNo—Slug del tenant. Necessario solo se la credenziale ha accesso a più tenant.acme
BP_WEBHOOK_SECRETNo—Segreto condiviso per verificare la firma dei webhook in ingresso.whsec_9c4f2a1e8b7d0345
BP_TIMEOUT_MSNo—Timeout delle richieste in millisecondi. Valore consigliato tra 10000 e 30000.15000
BP_MAX_RETRIESNo—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.

blupantheon.ts
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:

esempio.ts
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.

blupantheon.py
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:

esempio.py
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.

ConsoleAmministrazioneAPIScarica collezione Postman

URL collezione

https://api.blupantheon.com/openapi/postman/collection.json

Sempre 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:

Variabili dell'ambiente Postman
baseUrl  = https://api.sandbox.blupantheon.com
apiKey   = bp_test_xxxxxxxxxxxxxxxx
tenantSlug = acme
projectId  = bp_proj_7f3a9c21

Nelle 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.

ServizioURL della specifica
Identityhttps://api.blupantheon.com/identity/v1/openapi.json
Consolehttps://api.blupantheon.com/console/v1/openapi.json
Billinghttps://api.blupantheon.com/billing/v1/openapi.json
Bookinghttps://api.blupantheon.com/booking/v1/openapi.json
Specifica unificatahttps://api.blupantheon.com/openapi.json

Le specifiche sono pubbliche e non richiedono autenticazione.

Generare un client

Installazione
npm install --global @openapitools/openapi-generator-cli
openapi-generator-cli generate \
  -i https://api.blupantheon.com/console/v1/openapi.json \
  -g typescript-fetch \
  -o ./src/generated/blupantheon \
  --additional-properties=supportsES6=true,withInterfaces=true

Rigenera 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.json

Client 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.

LinguaggioProgettoStato
PHPblupantheon-phpComunitario, copre Identity e Console
Gogo-blupantheonComunitario, copre Console
Javablupantheon-javaGenerato 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.

Collegamenti

На этой странице