Guida - 15 min

Connettiti alla Data API

La Data API rende disponibili le business entity come endpoint REST versionati sotto /api/data/v{version}/{route}, per esempio /api/data/v1/customers. Un admin decide quali entity sono esposte, quali campi offre ciascuna versione e quali ruoli possono elencare, leggere, creare, aggiornare o eliminare. Un endpoint può anche chiamare uno script module: riceve dei parametri e risponde con ciò che il modulo restituisce. Questa guida è per chi scrive l'integrazione: come connettersi, come servizio con una chiave API o come utente che accede, e come chiamare gli endpoint.

Prima di iniziare

Chiedi all'admin del workspace quattro cose:

  1. L'indirizzo dell'API. È lo stesso host con cui parla l'app. Nella schermata API Endpoints, il pannello Try lo mostra nella sua riga curl.
  2. L'endpoint e la versione, ad es. customers versione 1. Risponde solo una versione con stato Active o Deprecated; una Draft no.
  3. Un ruolo autorizzato su quell'endpoint per le operazioni che ti servono, impostato nella griglia Who can call it dell'endpoint. Admin e owner possono sempre chiamare tutto.
  4. Un accesso con quel ruolo: una chiave API per un servizio, oppure un account utente per una persona o per uno script veloce.

Per tutto ciò che gira senza supervisione, come una sincronizzazione notturna, un webshop o uno strumento di reportistica, usa una chiave API. Non richiede password, accesso né refresh, e apre la Data API e nient'altro.

Connetti un servizio con una chiave API

Un admin crea un API client nella schermata API Clients: un nome, i ruoli che ha e, facoltativamente, una data di scadenza. Admin e owner non si possono assegnare a un client, quindi raggiunge solo ciò che è concesso ai suoi ruoli su ciascun endpoint. La chiave viene mostrata una sola volta, alla creazione del client; ne viene memorizzata solo un'impronta, quindi nessuno può recuperarla in seguito, nemmeno un admin.

Invia la chiave come bearer token a ogni chiamata:

API=https://api.example.com
KEY=ask_api_acme.q7Vt0r2Lk_dP9sXw3eNfH1mA6yZc4uBgJ8oTiR5vWnE

curl "$API/api/data/v1/customers" -H "Authorization: Bearer $KEY"
  • La chiave funziona solo sotto /api/data. In qualsiasi altro punto del prodotto viene rifiutata, quindi una chiave trapelata non può raggiungere gli altri dati dell'app né le sue impostazioni.
  • Resta valida finché non scade, finché il client non viene disattivato o eliminato, o finché un admin non la ruota, sostituendola con una nuova. Ognuno di questi eventi ha effetto dalla chiamata successiva, che risponde quindi 401.
  • Le modifiche fatte dal servizio riportano il nome del client come autore, e i business event lo vedono come utente.
  • La chiave indica il proprio workspace (ask_api_{workspace}.{secret}), quindi non serve scegliere un workspace. Conservala in un secret store, mai nel codice.

Prosegui con Chiama un endpoint.

Connettiti come utente: accedi

Usa un account dedicato, non quello di una persona. Il token di un utente autenticato può fare tutto ciò che quell'utente può fare nell'app, non solo chiamare la Data API, quindi assegna all'account il ruolo autorizzato e nient'altro, e conserva la sua password in un secret store. Per un servizio, una chiave API è la scelta migliore.

Invia con una POST l'email e la password dell'account a /api/v1/auth/login:

API=https://api.example.com

curl -X POST "$API/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "webshop-sync@example.com", "password": "..."}'

La risposta contiene i token:

{
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "refreshToken": "q8Zr0dW3...",
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "expiresIn": 900,
  "redirectHost": null
}
  • token è l'access token. Invialo a ogni chiamata. Scade dopo expiresIn secondi, 15 minuti di default.
  • refreshToken ti dà una nuova coppia di token senza bisogno della password (vedi Mantieni viva una sessione utente). Dura 30 giorni di default e si può usare una sola volta.
  • idToken e redirectHost servono all'app nel browser; ignorali.

Se l'accesso viene rifiutato, la risposta è 401 con un code: invalid_credentials, disabled (l'account è disattivato) o no_tenant (l'account non appartiene a nessun workspace attivo). Dopo troppi tentativi falliti la risposta è 429 con locked_out. L'accesso è inoltre limitato a 20 tentativi al minuto per indirizzo IP, quindi accedi una volta e tieni la sessione, invece di accedere a ogni chiamata.

Chiama un endpoint

Metti la chiave API o l'access token nell'header Authorization; qui sotto, $TOKEN vale per entrambi. Una chiamata di elenco risponde con una pagina di elementi e il totale:

curl "$API/api/data/v1/customers?pageSize=2&sort=name" \
  -H "Authorization: Bearer $TOKEN"
{
  "items": [
    { "id": 1, "name": "Acme", "email": "info@acme.test", "creditLimit": 500 },
    { "id": 2, "name": "Globex", "email": "hello@globex.test", "creditLimit": 50 }
  ],
  "page": 1,
  "pageSize": 2,
  "totalCount": 3
}

I nomi dei campi sono quelli che l'admin ha dato a questa versione, e non devono per forza coincidere con le colonne del database. Leggere un singolo elemento, crearlo, modificarlo ed eliminarlo funziona così:

# One item
curl "$API/api/data/v1/customers/1" -H "Authorization: Bearer $TOKEN"

# Create: answers 201 with the stored item and a Location header
curl -X POST "$API/api/data/v1/customers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Hooli", "email": "hi@hooli.test"}'

# Change some fields
curl -X PATCH "$API/api/data/v1/customers/4" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "sales@hooli.test"}'

# Delete: answers 204
curl -X DELETE "$API/api/data/v1/customers/4" -H "Authorization: Bearer $TOKEN"
  • PATCH modifica solo i campi presenti nel body. PUT fa lo stesso, ma rifiuta un body in cui manca un campo obbligatorio.
  • L'id arriva sempre dall'URL. Un id nel body di una creazione viene ignorato, quindi una creazione non sovrascrive mai un elemento esistente.
  • I campi di sola lettura presenti nel body vengono ignorati, quindi puoi rimandare un elemento esattamente come l'hai ricevuto. Un campo che la versione non conosce viene rifiutato con 400, così gli errori di battitura saltano fuori subito.
  • Le scritture si comportano esattamente come i salvataggi nell'app: i business event scattano, la validazione viene eseguita e le schermate aperte si aggiornano.

Chiama uno script module

Alcuni endpoint eseguono uno script module invece di servire le righe di una entity, per un lavoro che va oltre una lettura o una scrittura: calcolare il prezzo di un ordine, prenotare uno slot, produrre un report. Ogni versione di un endpoint del genere risponde a un solo metodo, e il documento OpenAPI dice quale: POST riceve i parametri come oggetto JSON nel body, GET li riceve nella query string.

# A version called with POST: the parameters as a JSON object
curl -X POST "$API/api/data/v1/quote" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 3, "express": true}'

# A version called with GET: the parameters in the query string
curl "$API/api/data/v1/planner-week?weekStart=2026-09-21" \
  -H "Authorization: Bearer $TOKEN"

La risposta è ciò che il modulo restituisce, in JSON:

{
  "quantity": 3,
  "total": 172.5,
  "deliveryDate": "2026-09-28"
}
  • I nomi dei parametri sono quelli che l'admin ha dato alla versione. Ogni valore viene controllato rispetto al suo tipo prima che il modulo venga eseguito: numero intero, numero, true o false, una data con o senza ora (2026-09-25, 2026-09-25T14:30:00Z), o testo. Un valore del tipo sbagliato, un parametro che la versione non conosce e un parametro obbligatorio mancante danno un 400; un null conta come omesso.
  • Un modulo che non restituisce nulla risponde 204 No Content. Uno che fallisce risponde 500, con il messaggio del modulo stesso in detail.
  • La chiamata richiede un ruolo a cui è stata concessa l'operazione Call sull'endpoint. Un modulo che l'admin ha segnato come admin only risponde solo ad admin e owner, quindi una chiave API non lo raggiunge mai.
  • Il modulo viene eseguito davvero, esattamente come quando lo esegue l'app: ciò che scrive fa scattare i business event, e le mail che invia partono davvero.
  • Una chiamata con l'altro metodo, o su un path di elemento come /quote/1, dà un 405 il cui header Allow indica il metodo da usare.

Mantieni viva una sessione utente

Questo vale solo per un utente autenticato; a una chiave API non serve nulla di tutto ciò. Prima che l'access token scada, o quando una chiamata risponde 401, scambia il refresh token con una nuova coppia:

curl -X POST "$API/api/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "q8Zr0dW3..."}'

La risposta ha la stessa forma di quella dell'accesso, con un nuovo refresh token. Salvalo e butta via quello vecchio: un refresh token si usa una sola volta. Ripresentare un refresh token già usato, trascorso un minuto, viene considerato un furto e chiude l'intera sessione, dopodiché devi accedere di nuovo. Quindi dai a ogni processo o worker una sessione propria, e non condividere mai un refresh token tra di loro.

Un refresh risponde 401 quando la sessione è terminata: è scaduta, l'account è stato disattivato o rimosso dal workspace, oppure è stato fatto il logout. In quel caso accedi di nuovo. Le modifiche ai ruoli arrivano nel token al refresh successivo, quindi un ruolo revocato smette di funzionare entro 15 minuti. Per chiudere tu stesso una sessione:

curl -X POST "$API/api/v1/auth/logout" \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "q8Zr0dW3..."}'

Un account in più di un workspace

L'accesso sceglie un solo workspace per la sessione: quello a cui appartiene l'indirizzo che chiami, se l'account ne è membro, altrimenti l'ultimo che ha usato. Per questo un account dedicato dovrebbe appartenere a un solo workspace. Se ne ha più di uno, elencali e passa a quello che ti serve usando il suo tenantId:

# The workspaces this account belongs to
curl "$API/api/v1/auth/tenants" -H "Authorization: Bearer $TOKEN"

# A new token pair for one of them
curl -X POST "$API/api/v1/auth/switch-tenant" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tenantId": "3f1c2a9e-..."}'

Il cambio restituisce una nuova coppia di token per quel workspace; da lì prosegui come dopo un accesso.

Filtri, ordinamento e paginazione

# Customers whose name contains "acme", highest credit limit first
curl -G "$API/api/data/v1/customers" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "filter[name][like]=acme" \
  --data-urlencode "filter[creditLimit][gte]=100" \
  --data-urlencode "sort=-creditLimit" \
  --data-urlencode "page=2" \
  --data-urlencode "pageSize=50"
ParametroSignificato
pageNumero di pagina, a partire da 1.
pageSizeElementi per pagina, 50 di default. Ogni versione imposta un massimo (100, se l'admin non l'ha cambiato); chiederne di più dà 400.
sortUn solo campo, preceduto da - per l'ordine decrescente. Senza, prima i più recenti.
searchTesto cercato in tutti i campi che la versione restituisce.
filter[field]Uguale al valore.
filter[field][op]Confronto con un operatore, vedi sotto. Con più filtri, devono corrispondere tutti.
OperatoreCorrisponde a
eq, neUguale, diverso.
gt, gte, lt, lteMaggiore o minore di. Numeri, date e testo.
like, startswith, endswithTesto che contiene il valore, che inizia o che finisce con esso, senza distinzione tra maiuscole e minuscole. Solo campi di testo.
inUno qualsiasi dei valori di un elenco separato da virgole: filter[id][in]=1,2,3.
nulltrue per i valori vuoti, false per quelli valorizzati.

I valori vengono verificati rispetto al tipo del campo, quindi filter[creditLimit]=lots dà un 400 che indica il campo. Si possono filtrare, ordinare o cercare solo i campi che la versione restituisce, e un parametro sconosciuto viene rifiutato invece che ignorato, così un errore di battitura non restituisce mai tutto senza dire niente. I parametri che iniziano con _ vengono ignorati, per il cache busting.

Versioni

La versione fa parte del path, e più versioni dello stesso endpoint possono essere in servizio contemporaneamente. Una versione pubblicata non cambia mai in un modo che rompa la tua integrazione: i campi non vengono mai rimossi o rinominati, e nulla diventa obbligatorio. Allo stesso modo, una versione che chiama uno script module mantiene il suo metodo e i suoi parametri, e ne acquisisce solo di facoltativi. Modifiche di questo tipo arrivano come nuova versione.

  • Le versioni Active e Deprecated rispondono.
  • Una versione che non esiste, o che è ancora Draft, risponde 404 ed elenca quelle che esistono.
  • Una versione Retired risponde 410 Gone e indica quella a cui passare.

Ogni risposta indica quali versioni esistono. Una versione deprecata lo segnala, e dice anche quando verrà dismessa:

HTTP/1.1 200 OK
api-supported-versions: 1, 2
api-deprecated-versions: 1
Deprecation: @1788220800
Sunset: Thu, 31 Dec 2026 00:00:00 GMT
Link: <https://api.example.com/api/data/v2/customers>; rel="successor-version"

Deprecation è il momento in cui è stata deprecata (un timestamp Unix), Sunset la data in cui smette di rispondere, se l'admin ne ha impostata una, e Link punta alla versione successiva. Scrivere un warning nei log ogni volta che compare Deprecation è il modo più semplice per accorgersene in tempo.

Errori

Un rifiuto della Data API è un problem document standard (RFC 7807), il cui detail spiega cosa non andava:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{
  "title": "Forbidden",
  "status": 403,
  "detail": "None of your roles may Delete on 'customers'."
}

Un'eccezione: quando la validazione di un business event rifiuta una scrittura, il body del 422 è { "message": "...", "errorColumns": [...] }, che indica i campi in errore con i nomi delle colonne del database.

StatoSignificato
400La richiesta non è valida: un campo o un parametro sconosciuto, un valore del tipo sbagliato, un campo obbligatorio mancante.
401Nessun token né chiave, oppure quello inviato non è più valido. Un utente fa il refresh e riprova; una chiave API invece è scaduta, è stata ruotata, oppure il suo client è stato disattivato o eliminato.
403Nessuno dei ruoli dell'account può eseguire questa operazione su questo endpoint.
404Endpoint, versione o elemento inesistente.
405Questa versione non offre l'operazione, ad es. l'eliminazione, oppure un endpoint di uno script module è stato chiamato con l'altro metodo.
409I dati sono in conflitto: un valore univoco esiste già, oppure altri record fanno ancora riferimento a questo.
410La versione è stata ritirata.
422Una regola di business o un vincolo della tabella ha rifiutato i valori.
429Troppe richieste. Aspetta un attimo e riprova.
500Lo script module dietro un endpoint è fallito; detail riporta il suo messaggio.

Descrizione OpenAPI

GET /api/data/openapi.json, chiamato con il tuo token, restituisce un documento OpenAPI 3 con esattamente le versioni e le operazioni che il tuo account può usare, con ogni campo e il suo tipo. Importalo in Postman o Insomnia, oppure usalo per generare un client tipizzato. Scaricalo di nuovo dopo che l'admin ha pubblicato una nuova versione.

Esempi in Python

Con una chiave API basta l'header:

import os
import requests

API = os.environ["ARCHESTACK_API"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['ARCHESTACK_API_KEY']}"

r = session.get(f"{API}/api/data/v1/customers", params={"pageSize": 100}, timeout=30)
r.raise_for_status()
for customer in r.json()["items"]:
    print(customer["id"], customer["name"])

Con un utente autenticato, invece, la sessione va gestita. Questo esempio accede, fa il refresh un minuto prima che il token scada, si riprende da un 401, legge tutte le pagine e crea un elemento. Metti l'indirizzo e le credenziali in variabili d'ambiente.

import os
import time
import requests

API = os.environ["ARCHESTACK_API"]            # e.g. https://api.example.com
EMAIL = os.environ["ARCHESTACK_EMAIL"]
PASSWORD = os.environ["ARCHESTACK_PASSWORD"]


class DataApi:
    """One signed-in session. Give every process or worker its own."""

    def __init__(self):
        self.token = None
        self.refresh_token = None
        self.expires_at = 0.0

    def _keep(self, body):
        self.token = body["token"]
        self.refresh_token = body["refreshToken"]  # always replace it: each one works once
        self.expires_at = time.time() + body["expiresIn"] - 60

    def sign_in(self):
        r = requests.post(f"{API}/api/v1/auth/login",
                          json={"email": EMAIL, "password": PASSWORD}, timeout=30)
        r.raise_for_status()
        self._keep(r.json())

    def refresh(self):
        r = requests.post(f"{API}/api/v1/auth/refresh",
                          json={"refreshToken": self.refresh_token}, timeout=30)
        if r.status_code == 401:  # the session ended: start a new one
            return self.sign_in()
        r.raise_for_status()
        self._keep(r.json())

    def call(self, method, path, **kwargs):
        if self.token is None:
            self.sign_in()
        elif time.time() >= self.expires_at:
            self.refresh()
        r = self._send(method, path, **kwargs)
        if r.status_code == 401:  # expired early or revoked: refresh once and retry
            self.refresh()
            r = self._send(method, path, **kwargs)
        return r

    def _send(self, method, path, **kwargs):
        headers = {"Authorization": f"Bearer {self.token}"}
        return requests.request(method, f"{API}/api/data{path}",
                                headers=headers, timeout=30, **kwargs)


api = DataApi()

# Every page of customers
page = 1
while True:
    r = api.call("GET", "/v1/customers", params={"page": page, "pageSize": 100})
    r.raise_for_status()
    body = r.json()
    for customer in body["items"]:
        print(customer["id"], customer["name"])
    if page * body["pageSize"] >= body["totalCount"]:
        break
    page += 1

# Create one
r = api.call("POST", "/v1/customers", json={"name": "Hooli", "email": "hi@hooli.test"})
if r.status_code == 201:
    print("created", r.json()["id"])
else:
    print("refused:", r.json().get("detail"))
Ambienti trial: lì possono chiamare l'API solo l'account del proprietario del trial e gli API client che il proprietario crea.

Dove andare dopo

  • Business Entities - da dove legge un endpoint, e perché una entity in join con una tabella one-to-many restituisce un elemento per ogni riga collegata.
  • User Management - crea l'account dell'integrazione e il suo ruolo.