Handleiding - 15 min

Verbinden met de Data API

De Data API stelt business entities beschikbaar als geversioneerde REST-endpoints onder /api/data/v{version}/{route}, bijvoorbeeld /api/data/v1/customers. Een admin bepaalt welke entities beschikbaar zijn, welke velden elke versie kent, en welke rollen de lijst mogen opvragen en items mogen lezen, aanmaken, bijwerken of verwijderen. Een endpoint kan ook een script module aanroepen: het neemt parameters aan en antwoordt met wat de module teruggeeft. Deze handleiding is voor wie de integratie schrijft: hoe je verbinding maakt, als service met een API-sleutel of als gebruiker die inlogt, en hoe je de endpoints aanroept.

Voordat je begint

Vraag de admin van de werkruimte om vier dingen:

  1. Het API-adres. Dezelfde host waarmee de app zelf praat. In het scherm API Endpoints toont het Try-paneel het in zijn curl-regel.
  2. Het endpoint en de versie, bv. customers versie 1. Alleen een versie met de status Active of Deprecated geeft antwoord, een Draft niet.
  3. Een rol met rechten op dat endpoint voor de bewerkingen die je nodig hebt, ingesteld in het Who can call it-raster van het endpoint. Admin en owner kunnen altijd alles aanroepen.
  4. Toegang met die rol: een API-sleutel voor een service, of een gebruikersaccount voor een persoon of een snel script.

Gebruik voor alles wat zonder toezicht draait, zoals een nachtelijke synchronisatie, een webshop of een rapportagetool, een API-sleutel. Die heeft geen wachtwoord, geen login en geen refresh nodig, en geeft toegang tot de Data API en tot niets anders.

Een service verbinden met een API-sleutel

Een admin maakt een API client aan in het scherm API Clients: een naam, de rollen die de client krijgt, en optioneel een vervaldatum. Admin en owner kunnen niet aan een client worden toegekend, dus een client bereikt alleen wat zijn rollen op elk endpoint mogen. De sleutel wordt precies één keer getoond, bij het aanmaken van de client. Alleen een vingerafdruk ervan wordt opgeslagen, dus niemand kan hem later nog opzoeken, ook een admin niet.

Stuur de sleutel bij elke aanroep mee als bearer token:

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

curl "$API/api/data/v1/customers" -H "Authorization: Bearer $KEY"
  • De sleutel werkt alleen onder /api/data. Overal elders in het product wordt hij geweigerd, dus met een uitgelekte sleutel kom je niet bij de andere data of de instellingen van de app.
  • Hij blijft geldig tot hij verloopt, de client wordt uitgeschakeld of verwijderd, of een admin hem roteert en zo door een nieuwe vervangt. Elk daarvan geldt vanaf de volgende aanroep, die dan 401 antwoordt.
  • Wijzigingen die de service maakt, krijgen de naam van de client als auteur, en business events zien de client als de gebruiker.
  • De sleutel bevat de naam van zijn werkruimte (ask_api_{workspace}.{secret}), dus je hoeft geen werkruimte te kiezen. Bewaar hem in een secret store, nooit in de code.

Ga verder met Een endpoint aanroepen.

Verbinden als gebruiker: inloggen

Gebruik een apart account, niet dat van een persoon. Met het token van een ingelogd account kun je alles wat die gebruiker in de app kan, niet alleen de Data API aanroepen. Geef het account dus de toegekende rol en verder niets, en bewaar het wachtwoord in een secret store. Voor een service is een API-sleutel de betere keuze.

Stuur het e-mailadres en het wachtwoord van het account met een POST naar /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": "..."}'

Het antwoord bevat de tokens:

{
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "refreshToken": "q8Zr0dW3...",
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "expiresIn": 900,
  "redirectHost": null
}
  • token is het access token. Stuur het mee bij elke aanroep. Het verloopt na expiresIn seconden, standaard 15 minuten.
  • Met refreshToken krijg je een nieuw paar zonder het wachtwoord (zie Een gebruikerssessie in leven houden). Het is standaard 30 dagen geldig en werkt precies één keer.
  • idToken en redirectHost zijn voor de browser-app, die kun je negeren.

Een geweigerde login antwoordt 401 met een code: invalid_credentials, disabled (het account is uitgeschakeld) of no_tenant (het account hoort bij geen enkele actieve werkruimte). Na te veel mislukte pogingen volgt 429 met locked_out. Inloggen is bovendien beperkt tot 20 pogingen per minuut per IP-adres, dus log één keer in en houd de sessie aan, in plaats van voor elke aanroep opnieuw in te loggen.

Een endpoint aanroepen

Zet de API-sleutel of het access token in de Authorization-header; hieronder staat $TOKEN voor een van beide. Een lijstaanroep geeft één pagina items terug, plus het totaal:

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
}

De veldnamen zijn de namen die de admin in deze versie heeft gekozen, en die hoeven niet overeen te komen met de kolommen in de database. Eén item lezen, aanmaken, wijzigen en verwijderen gaat zo:

# 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 wijzigt alleen de velden in de body. PUT doet hetzelfde, maar weigert een body waarin een verplicht veld ontbreekt.
  • Het id komt altijd uit de URL. Een id in de body bij het aanmaken wordt genegeerd, dus aanmaken overschrijft nooit een bestaand item.
  • Alleen-lezen velden in een body worden genegeerd, dus je kunt een item precies zo terugsturen als je het hebt opgehaald. Een veld dat de versie niet kent, wordt geweigerd met 400, zodat typfouten meteen opvallen.
  • Schrijfacties gedragen zich precies als opslaan in de app: business events worden getriggerd, validatie draait en open schermen worden ververst.

Een script module aanroepen

Sommige endpoints draaien een script module in plaats van de rijen van een entity te leveren, voor werk dat meer is dan lezen of schrijven: de prijs van een order berekenen, een tijdslot boeken, een rapport. Elke versie van zo'n endpoint antwoordt op één methode, en het OpenAPI-document zegt welke: POST neemt de parameters als JSON-object in de body, GET neemt ze uit de querystring.

# 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"

Het antwoord is wat de module teruggeeft, als JSON:

{
  "quantity": 3,
  "total": 172.5,
  "deliveryDate": "2026-09-28"
}
  • De parameternamen zijn de namen die de admin de versie gaf. Elke waarde wordt tegen haar type gecontroleerd voordat de module draait: geheel getal, getal, true of false, een datum met of zonder tijd (2026-09-25, 2026-09-25T14:30:00Z), of tekst. Een waarde van het verkeerde type, een parameter die de versie niet kent en een ontbrekende verplichte parameter geven een 400; null telt als weggelaten.
  • Een module die niets teruggeeft, antwoordt 204 No Content. Een module die faalt, antwoordt 500, met de eigen melding van de module in detail.
  • Aanroepen vraagt een rol die op het endpoint de bewerking Call kreeg. Een module die de admin als admin only markeerde, antwoordt alleen admin en owner, dus een API-sleutel bereikt haar nooit.
  • De module draait echt, precies zoals wanneer de app haar draait: wat ze schrijft, triggert business events, en mail die ze verstuurt, gaat echt de deur uit.
  • Aanroepen met de andere methode, of op een itempad zoals /quote/1, geeft een 405 waarvan de Allow-header de methode noemt die je moet gebruiken.

Een gebruikerssessie in leven houden

Dit geldt alleen voor een ingelogde gebruiker, een API-sleutel heeft er niets van nodig. Voordat het access token verloopt, of wanneer een aanroep 401 antwoordt, ruil je het refresh token in voor een nieuw paar:

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

Het antwoord heeft dezelfde vorm als bij het inloggen, met een nieuw refresh token. Bewaar dat en gooi het oude weg: een refresh token werkt maar één keer. Wie een verbruikt token na meer dan een minuut opnieuw aanbiedt, wordt behandeld als een dief: de hele sessie wordt beëindigd en je moet opnieuw inloggen. Geef daarom elk proces of elke worker een eigen sessie, en deel nooit één refresh token tussen meerdere.

Een refresh antwoordt 401 wanneer de sessie voorbij is: ze is verlopen, het account is uitgeschakeld of uit de werkruimte verwijderd, of er is uitgelogd. Log in dat geval opnieuw in. Rolwijzigingen komen bij de volgende refresh in het token terecht, dus een ingetrokken rol werkt binnen 15 minuten niet meer. Zo beëindig je zelf een sessie:

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

Een account in meer dan één werkruimte

Bij het inloggen wordt één werkruimte voor de sessie gekozen: de werkruimte waartoe het adres dat je aanroept behoort, als het account daar lid is, en anders de werkruimte die het laatst gebruikt werd. Daarom hoort een apart integratieaccount maar bij één werkruimte. Hoort het bij meerdere, vraag ze dan op en wissel met de tenantId van de werkruimte die je wilt:

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

Het wisselen geeft een nieuw tokenpaar voor die werkruimte terug. Ga daarmee verder zoals na het inloggen.

Filteren, sorteren en pagineren

# 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"
ParameterBetekenis
pagePaginanummer, beginnend bij 1.
pageSizeItems per pagina, standaard 50. Elke versie stelt een maximum in (100, tenzij de admin dat heeft gewijzigd); wie meer vraagt, krijgt een 400.
sortEén veld, met een - ervoor voor aflopende volgorde. Zonder deze parameter komen de nieuwste eerst.
searchTekst die wordt gezocht in elk veld dat de versie teruggeeft.
filter[field]Gelijk aan de waarde.
filter[field][op]Vergeleken met een operator, zie hieronder. Bij meerdere filters moeten ze allemaal kloppen.
OperatorKomt overeen met
eq, neGelijk, niet gelijk.
gt, gte, lt, lteGroter of kleiner dan. Getallen, datums en tekst.
like, startswith, endswithTekst die de waarde bevat, ermee begint of ermee eindigt, ongeacht hoofdletters. Alleen tekstvelden.
inEen van de waarden in een kommagescheiden lijst: filter[id][in]=1,2,3.
nulltrue voor lege waarden, false voor ingevulde.

Waarden worden gecontroleerd tegen het type van het veld, dus filter[creditLimit]=lots geeft een 400 die het veld noemt. Je kunt alleen filteren, sorteren en zoeken op velden die de versie teruggeeft, en een onbekende parameter wordt geweigerd in plaats van genegeerd, zodat een typfout nooit stilletjes alles teruggeeft. Parameters die met _ beginnen worden genegeerd, voor cache busting.

Versies

De versie maakt deel uit van het pad, en meerdere versies van één endpoint kunnen naast elkaar live staan. Een gepubliceerde versie verandert nooit op een manier die jouw integratie breekt: velden worden nooit verwijderd of hernoemd, en niets wordt verplicht. Een versie die een script module aanroept, houdt zo ook haar methode en parameters, en krijgt er alleen optionele bij. Zulke wijzigingen komen in een nieuwe versie.

  • Versies met de status Active en Deprecated geven antwoord.
  • Een versie die niet bestaat, of nog een Draft is, antwoordt 404 en somt de versies op die wel bestaan.
  • Een Retired versie antwoordt 410 Gone en noemt de versie waarnaar je moet overstappen.

Elk antwoord vermeldt welke versies er bestaan. Een versie die Deprecated is, meldt dat ook, en wanneer ze verdwijnt:

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 is het moment waarop de versie op Deprecated werd gezet (een Unix-tijd), Sunset de datum waarop ze niet meer antwoordt, als de admin die heeft ingesteld, en Link verwijst naar de volgende versie. Een waarschuwing loggen zodra Deprecation verschijnt, is de eenvoudigste manier om er op tijd van te horen.

Fouten

Een weigering van de Data API is een standaard probleemdocument (RFC 7807), waarvan detail zegt wat er mis was:

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

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

Eén uitzondering: wanneer de validatie van een business event een schrijfactie weigert, is de body van de 422 { "message": "...", "errorColumns": [...] }, met de foute velden onder de kolomnamen die de database zelf gebruikt.

StatusBetekenis
400Het verzoek is ongeldig: een onbekend veld of een onbekende parameter, een waarde van het verkeerde type, een ontbrekend verplicht veld.
401Geen token of sleutel, of die is niet meer geldig. Een gebruiker doet een refresh en probeert opnieuw. Bij een API-sleutel is die verlopen of geroteerd, of is de client uitgeschakeld of verwijderd.
403Geen van de rollen van het account mag dit doen op dit endpoint.
404Dit endpoint, deze versie of dit item bestaat niet.
405Deze versie biedt de bewerking niet aan, bv. verwijderen, of een endpoint van een script module werd met de andere methode aangeroepen.
409De data conflicteert: een unieke waarde bestaat al, of andere records verwijzen nog naar dit record.
410De versie is buiten gebruik gesteld (Retired).
422Een bedrijfsregel of een tabelregel heeft de waarden geweigerd.
429Te veel verzoeken. Wacht even en probeer opnieuw.
500De script module achter een endpoint faalde; detail bevat haar melding.

OpenAPI-beschrijving

GET /api/data/openapi.json, aangeroepen met je token, geeft een OpenAPI 3-document terug met precies de versies en bewerkingen die jouw account mag gebruiken, met elk veld en zijn type. Importeer het in Postman of Insomnia, of genereer er een getypeerde client mee. Haal het opnieuw op nadat de admin een nieuwe versie heeft gepubliceerd.

Voorbeelden in Python

Met een API-sleutel is de header alles wat je nodig hebt:

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"])

Wie als gebruiker inlogt, moet de sessie zelf onderhouden. Dit script logt in, doet een refresh een minuut voordat het token verloopt, herstelt van een 401, leest alle pagina's en maakt een item aan. Zet het adres en de inloggegevens in omgevingsvariabelen.

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-omgevingen: daar kunnen alleen het account van de eigenaar van de trial en API clients die de eigenaar aanmaakt de API aanroepen.

Waar nu naartoe

  • Business Entities - waar een endpoint uit leest, en waarom een entity die een one-to-many-tabel joint één item per gejoinde rij teruggeeft.
  • User Management - maak het integratieaccount en zijn rol aan.