Anleitung · 15 Min

Mit der Data API verbinden

Die Data API stellt Business Entities als versionierte REST-Endpunkte unter /api/data/v{version}/{route} bereit, zum Beispiel /api/data/v1/customers. Ein Admin legt fest, welche Entities freigegeben sind, welche Felder jede Version kennt und welche Rollen auflisten, lesen, anlegen, ändern oder löschen dürfen. Ein Endpunkt kann auch ein Script Module aufrufen: Er nimmt Parameter entgegen und antwortet mit dem, was das Modul zurückgibt. Diese Anleitung richtet sich an alle, die die Integration schreiben: wie du dich verbindest, als Dienst mit einem API-Schlüssel oder als Benutzer mit Anmeldung, und wie du die Endpunkte aufrufst.

Bevor du anfängst

Frag den Admin des Workspaces nach vier Dingen:

  1. Die API-Adresse. Derselbe Host, mit dem auch die App selbst spricht. Im Screen API Endpoints zeigt das Try-Panel sie in seiner curl-Zeile.
  2. Den Endpunkt und die Version, z. B. customers Version 1. Nur eine Version mit dem Status Active oder Deprecated antwortet, ein Draft nicht.
  3. Eine auf diesem Endpunkt vergebene Rolle für die Operationen, die du brauchst, festgelegt im Who can call it-Grid des Endpunkts. Admin und Owner dürfen immer alles aufrufen.
  4. Einen Zugang mit dieser Rolle: einen API-Schlüssel für einen Dienst oder ein Benutzerkonto für eine Person oder ein schnelles Skript.

Läuft etwas unbeaufsichtigt, etwa eine nächtliche Synchronisation, ein Webshop oder ein Reporting-Tool, nimm einen API-Schlüssel. Er braucht kein Passwort, keine Anmeldung und keinen Refresh, und er öffnet die Data API und sonst nichts.

Einen Dienst mit einem API-Schlüssel verbinden

Ein Admin legt einen API client im Screen API Clients an: mit einem Namen, seinen Rollen und optional einem Ablaufdatum. Admin und Owner lassen sich einem Client nicht zuweisen, er kann auf jedem Endpunkt also nur das, was seinen Rollen dort gewährt ist. Der Schlüssel wird genau einmal angezeigt, beim Anlegen des Clients. Gespeichert wird nur ein Fingerabdruck davon, sodass niemand ihn später nachschlagen kann, nicht einmal ein Admin.

Schicke den Schlüssel bei jedem Aufruf als Bearer Token mit:

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

curl "$API/api/data/v1/customers" -H "Authorization: Bearer $KEY"
  • Der Schlüssel funktioniert nur unter /api/data. Überall sonst im Produkt wird er abgelehnt, ein geleakter Schlüssel kommt also weder an die übrigen Daten der App noch an ihre Einstellungen heran.
  • Er bleibt gültig, bis er abläuft, der Client deaktiviert oder gelöscht wird oder ein Admin ihn rotiert, also durch einen neuen ersetzt. Das wirkt jeweils ab dem nächsten Aufruf, der dann mit 401 antwortet.
  • Änderungen, die der Dienst vornimmt, tragen den Namen des Clients als Autor, und Business Events sehen ihn als Benutzer.
  • Der Schlüssel nennt seinen Workspace (ask_api_{workspace}.{secret}), eine Auswahl des Workspaces ist also nicht nötig. Bewahre ihn in einem Secret Store auf, niemals im Code.

Weiter geht es mit Einen Endpunkt aufrufen.

Als Benutzer verbinden: Anmelden

Nutze ein eigenes Konto, nicht das einer Person. Ein Token aus einer Anmeldung kann alles, was dieser Benutzer in der App kann, nicht nur die Data API aufrufen. Gib dem Konto deshalb die vergebene Rolle und sonst nichts, und bewahre sein Passwort in einem Secret Store auf. Für einen Dienst ist ein API-Schlüssel die bessere Wahl.

Sende E-Mail-Adresse und Passwort des Kontos per POST an /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": "..."}'

Die Antwort enthält die Tokens:

{
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "refreshToken": "q8Zr0dW3...",
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "expiresIn": 900,
  "redirectHost": null
}
  • token ist das Access Token. Schicke es bei jedem Aufruf mit. Es läuft nach expiresIn Sekunden ab, standardmäßig nach 15 Minuten.
  • refreshToken holt dir ein neues Paar ohne Passwort (siehe Eine Benutzer-Session am Leben halten). Es gilt standardmäßig 30 Tage und funktioniert genau einmal.
  • idToken und redirectHost sind für die Browser-App gedacht, ignoriere sie.

Eine abgelehnte Anmeldung antwortet mit 401 und einem code: invalid_credentials, disabled (das Konto ist deaktiviert) oder no_tenant (das Konto gehört zu keinem aktiven Workspace). Zu viele Fehlversuche führen zu 429 mit locked_out. Außerdem ist die Anmeldung auf 20 Versuche pro Minute und IP-Adresse begrenzt. Melde dich also einmal an und behalte die Session, statt dich für jeden Aufruf neu anzumelden.

Einen Endpunkt aufrufen

Setze den API-Schlüssel oder das Access Token in den Authorization-Header. Unten steht $TOKEN für beides. Ein List-Aufruf liefert eine Seite mit Einträgen und die Gesamtzahl:

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
}

Die Feldnamen sind die, die der Admin dieser Version gegeben hat, und müssen nicht mit den Spalten in der Datenbank übereinstimmen. Einen einzelnen Eintrag lesen, anlegen, ändern und löschen sieht so aus:

# 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 ändert nur die Felder im Body. PUT macht dasselbe, lehnt aber einen Body ab, in dem ein Pflichtfeld fehlt.
  • Die ID kommt immer aus der URL. Eine id im Body eines Create wird ignoriert, ein Create überschreibt also nie einen bestehenden Eintrag.
  • Schreibgeschützte Felder im Body werden ignoriert, du kannst einen Eintrag also genau so zurückschicken, wie du ihn abgerufen hast. Ein Feld, das die Version nicht kennt, wird mit 400 abgelehnt, das fängt Tippfehler ab.
  • Schreibzugriffe verhalten sich genau wie Speichern in der App: Business Events werden ausgelöst, die Validierung läuft, und geöffnete Ansichten aktualisieren sich.

Ein Script Module aufrufen

Manche Endpunkte führen ein Script Module aus, statt die Zeilen einer Entity auszuliefern, für Arbeit, die mehr ist als Lesen oder Schreiben: den Preis eines Auftrags berechnen, einen Termin buchen, einen Bericht erstellen. Jede Version eines solchen Endpunkts antwortet auf genau eine Methode, und das OpenAPI-Dokument sagt, auf welche: POST nimmt die Parameter als JSON-Objekt im Body, GET im 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"

Die Antwort ist das, was das Modul zurückgibt, als JSON:

{
  "quantity": 3,
  "total": 172.5,
  "deliveryDate": "2026-09-28"
}
  • Die Parameternamen sind die, die der Admin der Version gegeben hat. Jeder Wert wird vor dem Lauf des Moduls gegen seinen Typ geprüft: Ganzzahl, Zahl, true oder false, ein Datum mit oder ohne Uhrzeit (2026-09-25, 2026-09-25T14:30:00Z) oder Text. Ein Wert des falschen Typs, ein Parameter, den die Version nicht kennt, und ein fehlender Pflichtparameter ergeben 400; null zählt als weggelassen.
  • Ein Modul, das nichts zurückgibt, antwortet mit 204 No Content. Eines, das fehlschlägt, antwortet mit 500 und der eigenen Meldung des Moduls in detail.
  • Der Aufruf braucht eine Rolle, der auf dem Endpunkt die Operation Call gewährt wurde. Ein Modul, das der Admin als admin only markiert hat, antwortet nur Admin und Owner, ein API-Schlüssel erreicht es also nie.
  • Das Modul läuft echt, genau wie wenn die App es ausführt: Was es schreibt, löst Business Events aus, und Mails, die es verschickt, gehen wirklich raus.
  • Ein Aufruf mit der anderen Methode oder auf einem Item-Pfad wie /quote/1 ergibt 405, und der Allow-Header nennt die richtige Methode.

Eine Benutzer-Session am Leben halten

Das gilt nur für einen angemeldeten Benutzer, ein API-Schlüssel braucht nichts davon. Bevor das Access Token abläuft, oder wenn ein Aufruf mit 401 antwortet, tauschst du das Refresh Token gegen ein neues Paar:

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

Die Antwort hat dieselbe Form wie bei der Anmeldung, mit einem neuen Refresh Token. Speichere es und wirf das alte weg: Ein Refresh Token funktioniert nur einmal. Wird ein bereits verbrauchtes nach Ablauf einer Minute erneut vorgelegt, gilt das als Diebstahl und beendet die gesamte Session, danach musst du dich neu anmelden. Gib deshalb jedem Prozess oder Worker eine eigene Session und teile nie ein Refresh Token zwischen ihnen.

Ein Refresh antwortet mit 401, wenn die Session beendet ist: Sie ist abgelaufen, das Konto wurde deaktiviert oder aus dem Workspace entfernt, oder die Session wurde per Logout beendet. Melde dich in diesem Fall neu an. Rollenänderungen landen beim nächsten Refresh im Token, eine entzogene Rolle wirkt also spätestens nach 15 Minuten nicht mehr. So beendest du eine Session selbst:

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

Ein Konto in mehr als einem Workspace

Die Anmeldung wählt einen Workspace für die Session: den, zu dem die aufgerufene Adresse gehört, sofern das Konto dort Mitglied ist, andernfalls den zuletzt genutzten. Deshalb sollte ein eigenes Integrationskonto nur zu einem Workspace gehören. Gehört es zu mehreren, liste sie auf und wechsle mit der tenantId des gewünschten:

# 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-..."}'

Der Wechsel liefert ein neues Token-Paar für diesen Workspace, mach damit weiter wie nach einer Anmeldung.

Filtern, Sortieren und Paging

# 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"
ParameterBedeutung
pageSeitennummer, beginnend bei 1.
pageSizeEinträge pro Seite, standardmäßig 50. Jede Version legt ein Maximum fest (100, sofern der Admin es nicht geändert hat), mehr anzufordern ergibt einen 400.
sortEin Feld, mit vorangestelltem - für absteigende Reihenfolge. Ohne Angabe die neuesten zuerst.
searchText, der mit jedem Feld abgeglichen wird, das die Version zurückgibt.
filter[field]Gleich dem Wert.
filter[field][op]Vergleich mit einem Operator, siehe unten. Bei mehreren Filtern müssen alle zutreffen.
OperatorTrifft zu auf
eq, neGleich, ungleich.
gt, gte, lt, lteGrößer oder kleiner als. Zahlen, Datumswerte und Text.
like, startswith, endswithText, der den Wert enthält, damit beginnt oder damit endet, ohne Beachtung der Groß- und Kleinschreibung. Nur Textfelder.
inEiner der Werte aus einer kommagetrennten Liste: filter[id][in]=1,2,3.
nulltrue für leere Werte, false für gefüllte.

Werte werden gegen den Typ des Felds geprüft, filter[creditLimit]=lots ergibt also einen 400, der das Feld nennt. Filtern, sortieren und durchsuchen lassen sich nur Felder, die die Version zurückgibt, und ein unbekannter Parameter wird abgelehnt statt ignoriert, sodass ein Tippfehler nie stillschweigend alles zurückliefert. Parameter, die mit _ beginnen, werden ignoriert, damit du sie zum Cache-Busting nutzen kannst.

Versionen

Die Version ist Teil des Pfads, und mehrere Versionen eines Endpunkts können nebeneinander live sein. Eine veröffentlichte Version ändert sich nie so, dass deine Integration bricht: Felder werden nie entfernt oder umbenannt, und nichts wird nachträglich zur Pflicht. Eine Version, die ein Script Module aufruft, behält ebenso ihre Methode und ihre Parameter und bekommt nur optionale hinzu. Solche Änderungen kommen als neue Version.

  • Active- und Deprecated-Versionen antworten.
  • Eine Version, die nicht existiert oder noch ein Draft ist, antwortet mit 404 und listet die vorhandenen Versionen auf.
  • Eine Retired-Version antwortet mit 410 Gone und nennt die Version, auf die du wechseln solltest.

Jede Antwort sagt, welche Versionen es gibt. Eine Deprecated-Version sagt das zusätzlich über sich selbst, und wann sie wegfällt:

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 ist der Zeitpunkt, zu dem sie auf Deprecated gesetzt wurde (eine Unix-Zeit), Sunset das Datum, ab dem sie nicht mehr antwortet, sofern der Admin eines gesetzt hat, und Link verweist auf die nächste Version. Bei jedem Deprecation-Header eine Warnung zu loggen ist der einfachste Weg, rechtzeitig davon zu erfahren.

Fehler

Eine Ablehnung durch die Data API ist ein standardkonformes Problem-Dokument (RFC 7807), dessen detail beschreibt, was falsch war:

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

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

Eine Ausnahme: Wenn die Validierung eines Business Events einen Schreibzugriff ablehnt, ist der 422-Body { "message": "...", "errorColumns": [...] } und nennt die betroffenen Felder mit den Spaltennamen der Datenbank selbst.

StatusBedeutung
400Die Anfrage ist fehlerhaft: ein unbekanntes Feld oder ein unbekannter Parameter, ein Wert vom falschen Typ, ein fehlendes Pflichtfeld.
401Token oder Schlüssel fehlt oder ist nicht mehr gültig. Ein Benutzer führt einen Refresh aus und versucht es erneut. Ein API-Schlüssel ist abgelaufen oder wurde rotiert, oder sein Client wurde deaktiviert oder gelöscht.
403Keine der Rollen des Kontos darf diese Operation auf diesem Endpunkt ausführen.
404Endpunkt, Version oder Eintrag existiert nicht.
405Diese Version bietet die Operation nicht an, z. B. Löschen, oder ein Endpunkt eines Script Modules wurde mit der anderen Methode aufgerufen.
409Die Daten stehen im Konflikt: Ein eindeutiger Wert existiert bereits, oder andere Datensätze verweisen noch auf diesen.
410Die Version wurde außer Betrieb genommen (Retired).
422Eine Geschäftsregel oder eine Tabellenregel hat die Werte abgelehnt.
429Zu viele Anfragen. Warte kurz und versuche es erneut.
500Das Script Module hinter einem Endpunkt ist fehlgeschlagen; detail enthält seine Meldung.

OpenAPI-Beschreibung

GET /api/data/openapi.json, mit deinem Token aufgerufen, liefert ein OpenAPI-3-Dokument mit genau den Versionen und Operationen, die dein Konto nutzen darf, samt jedem Feld und seinem Typ. Importiere es in Postman oder Insomnia, oder generiere daraus einen typisierten Client. Rufe es erneut ab, nachdem der Admin eine neue Version veröffentlicht hat.

Beispiele in Python

Mit einem API-Schlüssel genügt der 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"])

Meldest du dich als Benutzer an, musst du dich um die Session kümmern. Dieses Beispiel meldet sich an, führt eine Minute vor Ablauf des Tokens einen Refresh aus, fängt einen 401 ab, liest alle Seiten und legt einen Eintrag an. Hinterlege die Adresse und die Zugangsdaten in Umgebungsvariablen.

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"))
Trial-Umgebungen: Dort können nur das Owner-Konto des Trials und die vom Owner angelegten API clients die API aufrufen.

Wie es weitergeht

  • Business Entities - woraus ein Endpunkt liest und warum eine Entity, die eine One-to-Many-Tabelle joint, einen Eintrag pro gejointer Zeile zurückgibt.
  • User Management - lege das Integrationskonto und seine Rolle an.