Developer docs

Maxiaa Partner API

Wwft-proof identiteitsverificatie, zonder gedoe. Sluit je software aan met een API-sleutel: start een verificatie, volg de status, en ontvang een ondertekende webhook zodra het verdict klaar is.

REST /v1API-key authWebhooks (HMAC)SandboxOpenAPI 3.0

Base-URL

https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1

Quickstart — je eerste verificatie in 5 minuten

  1. Log in op de console en maak een sandbox-sleutel (mxa_test_…).
  2. Start een verificatie, rond 'm af in sandbox, lees het verdict:
# 1. Start een verificatie (sandbox)
curl -X POST https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1/verifications \
  -H "Authorization: Bearer mxa_test_je_sleutel" \
  -H "Content-Type: application/json" \
  -d '{ "method": "document_liveness", "reference": "klant-8821",
        "sandbox": { "scenario": "approved", "auto_complete": false } }'

# 2. Rond de sandbox-verificatie af (simuleert de verify-link met een specimen)
curl -X POST https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1/verifications/{id}/complete \
  -H "Authorization: Bearer mxa_test_je_sleutel"

# 3. Vraag status + verdict op
curl https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1/verifications/{id} \
  -H "Authorization: Bearer mxa_test_je_sleutel"

Zelfde flow in Node:

import fetch from "node-fetch"; // of native fetch in Node 18+
const KEY = process.env.MAXIAA_KEY;         // mxa_test_…
const BASE = "https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1";
const h = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

const created = await (await fetch(`${BASE}/verifications`, {
  method: "POST", headers: h,
  body: JSON.stringify({ method: "document_liveness", reference: "klant-8821",
    sandbox: { scenario: "approved", auto_complete: false } }),
})).json();

await fetch(`${BASE}/verifications/${created.id}/complete`, { method: "POST", headers: h });

const result = await (await fetch(`${BASE}/verifications/${created.id}`, { headers: h })).json();
console.log(result.verdict.outcome); // "approved"

In sandbox zonder auto_complete:false is de verificatie meteen klaar (voorspelbaar verdict). Zet auto_complete:false om de asynchrone levenscyclus (pending → webhook → completed) na te bootsen.

Authenticatie

Elke aanroep gebruikt een API-sleutel in de Authorization-header:

Authorization: Bearer mxa_live_…   (productie)
Authorization: Bearer mxa_test_…   (sandbox)

De sleutel bepaalt de omgeving (sandbox/live) én de org. Sleutels worden alleen als SHA-256-hash bewaard — je ziet de volledige sleutel maar één keer. Scopes: verify:create, verify:read, webhooks:manage. Een sleutel zonder de juiste scope krijgt 403 insufficient_scope.

Sandbox-modus

Met een mxa_test_-sleutel draai je de volledige integratie zonder productiedata en zonder kosten. Sandbox-verificaties geven een voorspelbaar verdict op basis van het scenario:

scenarioverdict
approvedapproved (alle checks pass)
reviewreview (gezichtsvergelijking twijfel)
rejectedrejected (documentcheck fail)

Sandbox-verbruik verschijnt nooit op een factuur. Live-verificaties (mxa_live_) worden door de eindgebruiker via de verify_url doorlopen; afronden-via-API bestaat alleen in sandbox.

Rate limits

Per sleutel geldt een limiet per minuut (standaard 120). Elk antwoord bevat X-RateLimit-Limit en X-RateLimit-Remaining. Over de limiet → 429 rate_limit_exceeded met een Retry-After-header.

Idempotency

Stuur een Idempotency-Key-header op POST /verifications om veilig te herhalen bij netwerkfouten. Dezelfde sleutel + dezelfde body geeft exact hetzelfde antwoord (er ontstaat geen tweede verificatie). Dezelfde sleutel met een andere body → 422 idempotency_key_reused.

curl -X POST https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1/verifications \
  -H "Authorization: Bearer mxa_test_…" \
  -H "Idempotency-Key: 6f1c-…-a2" \
  -H "Content-Type: application/json" -d '{"reference":"klant-8821"}'

Foutmodel

Fouten zijn altijd JSON met een vast formaat + een request_id (ook als header X-Request-Id):

{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "Onbekende of ingetrokken API-sleutel.",
    "request_id": "req_…"
  }
}
statustype
400invalid_request_error (bv. invalid_json)
401authentication_error (missing/invalid/expired key)
403authentication_error (insufficient_scope) / invalid_request_error (not_sandbox)
404not_found_error
409invalid_request_error (already_completed)
422idempotency_error / invalid_request_error
429rate_limit_error

Webhooks & signing

Registreer een endpoint (het secret wordt éénmalig getoond):

curl -X POST https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1/webhooks \
  -H "Authorization: Bearer mxa_test_…" -H "Content-Type: application/json" \
  -d '{"url":"https://jouw-app.nl/webhooks/maxiaa","environment":"all"}'

Zodra een verdict klaar is, sturen wij een POST naar je URL met dit event:

{
  "id": "evt_2b1c…",
  "type": "verification.completed",
  "created": 1783900000,
  "environment": "sandbox",
  "data": {
    "verification_id": "b3f1…",
    "reference": "klant-8821",
    "status": "completed",
    "outcome": "approved",
    "assurance_level": "substantial",
    "reasons": []
  }
}

Headers: X-Maxiaa-Signature: t=<unix>,v1=<hex>, X-Maxiaa-Event, X-Maxiaa-Delivery (uniek event-id, gebruik het voor idempotentie aan jouw kant).

Signature verifiëren

Bereken HMAC-SHA256(secret, "<timestamp>.<ruwe body>") en vergelijk constant-time met v1. Verwerp events ouder dan 5 minuten (replay-bescherming).

import crypto from "node:crypto";

// Express-voorbeeld — verifieer de X-Maxiaa-Signature op je webhook-endpoint.
export function verifyMaxiaaSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay-bescherming
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}

app.post("/webhooks/maxiaa", express.raw({ type: "*/*" }), (req, res) => {
  const ok = verifyMaxiaaSignature(req.body.toString(), req.get("X-Maxiaa-Signature"), process.env.WHSEC);
  if (!ok) return res.status(400).send("bad signature");
  const event = JSON.parse(req.body.toString());
  if (event.type === "verification.completed") { /* verwerk event.data.outcome */ }
  res.sendStatus(200);
});
import hmac, hashlib, time

def verify_maxiaa_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:      # replay-bescherming
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

# Flask
@app.post("/webhooks/maxiaa")
def hook():
    if not verify_maxiaa_signature(request.get_data(), request.headers["X-Maxiaa-Signature"], WHSEC):
        return "bad signature", 400
    event = request.get_json()
    # event["type"] == "verification.completed" -> event["data"]["outcome"]
    return "", 200

Bij een non-2xx antwoord retryen we met exponentiële backoff (tot 6 pogingen, ~8 uur). Antwoord 2xx zodra je het event veilig hebt vastgelegd.

MCP-server — voor AI-agents en assistenten

Naast de REST-API draaien wij een MCP-server (Model Context Protocol). Daarmee kan een AI-assistent — Claude Code, Claude Desktop, of je eigen agent — zelf een verificatie starten en de status opvolgen, zonder dat jij daar integratiecode voor schrijft.

streamable HTTPJSON-RPC 2.0zelfde API-sleutelsandbox2025-06-18

Installeren

Eén regel. Gebruik je sandbox-sleutel om te oefenen:

claude mcp add maxiaa --scope user --transport http \
  https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp \
  --header "Authorization: Bearer mxa_test_je_sleutel"

Andere clients: endpoint https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp, transport streamable HTTP, en de sleutel als Authorization: Bearer-header. Er is geen aparte MCP-sleutel — het is dezelfde partner-API-sleutel, met dezelfde scopes, dezelfde rate limit en hetzelfde request-logboek.

De grens — lees dit vóór je hem aanzet

Deze server geeft geen persoonsgegevens en geen documenten terug. Geen naam, geen geboortedatum, geen BSN, geen documentbeeld, geen MRZ-velden, geen ruwe modeluitvoer en geen verwijzing naar het auditdossier. Wat een agent krijgt is: status en oordeel.

Dat is een bewuste keuze. Een antwoord aan een agent belandt in een taalmodel, een samenvatting, een chatlog en soms in de prompt van een derde partij. Elk veld dat wij hier weggeven, geven wij aan een onbekend aantal onbekende bestemmingen. Wil je de volledige gegevens inzien, dan doe je dat in de console — met een ingelogde mens en een toegangslogboek.

Ook je eigen reference krijg je niet terug: je mag erop filteren, maar wij echoën hem niet. In de praktijk staat daar een naam of dossiernummer in.

Endpoints die bewust géén tool zijn

EndpointWaarom niet
POST /v1/income-checksneemt loonstroken, arbeidsovereenkomsten en bankafschriften als base64 in en geeft netto-inkomen, werkgever en documentsignalen terug. Dat is precies de bulk-PII die een agent niet hoort te verwerken; deze module blijft achter de console en /v1.
GET /v1/income-checkslijst met inkomensuitkomsten per persoon — een lijst mét persoonsgegevens. Niet gebouwd.
GET /v1/income-checks/{id}bevat netto-inkomen, werkgever en bankmutatie-signalen van een individu.
DELETE /v1/webhooks/{id}zet de aflevering van een partner stil. Een agent mag configuratie aanmaken en lezen, maar een bestaande koppeling niet ongemerkt uitschakelen; dat blijft een menselijke handeling in de console of via /v1.
GET /v1/webhooks/{id}voegt niets toe naast list_webhooks en verbreedt de oppervlakte zonder reden.
GET /v1/verifications/{id} → dossier_refhet endpoint bestaat wél als tool, maar het dossierpad erin is uitgesloten via MCP_UITGESLOTEN: het wijst naar documentbeelden, MRZ-velden en geboortedatum.

Velden die nooit worden teruggegeven

VeldWaarom niet
referencedoor de partner meegegeven vrije tekst; bevat in de praktijk vaak een naam, dossier- of klantnummer. De agent die de verificatie start heeft de waarde al; teruggeven voegt geen mogelijkheid toe en verbreedt wel de PII-oppervlakte. Filteren op reference kan wél (list_verifications.reference) — schrijven zonder lezen.
external_refzelfde als reference; is het DB-veld eronder.
subject_refvrij tekstveld over de te verifiëren persoon — per definitie persoonsgegeven.
dossier_refverwijzing naar het volledige auditdossier (documentbeelden, MRZ, geboortedatum, BSN-veld). Een agent hoort daar niet bij te kunnen; dossiers gaan via de console met menselijke toegangscontrole.
link_tokengeeft toegang tot de verificatieflow van de eindgebruiker.
verify_urlbevat het link-token. Alleen de startende tool geeft hem terug (de partner moet hem naar zijn eindgebruiker sturen); nooit bij opvragen of in een lijst.
reasonsvrije tekst uit de beslislogica. Machineleesbare equivalent is verdict.checks; vrije tekst kan onbedoeld extractie-details bevatten.
summaryde ruwe verdict-samenvatting, inclusief modelsignalen en engine-details.

Beschikbare tools

Zonder geldige sleutel krijgt een agent géén toollijst. complete_verification bestaat alleen met een sandbox-sleutel; met een live-sleutel staat hij niet eens in de lijst. Lijsten zijn gepagineerd met een harde bovengrens van 25 per aanroep.

ToolWat het doetParameters
health
alleen lezen · geen scope
Controleert of de verificatiedienst bereikbaar is en met welke sleutel je praat (omgeving sandbox of live, welke rechten). Geeft geen klant- of persoonsgegevens terug. geen
start_verification
schrijft · scope verify:create
Start een identiteitsverificatie en geeft een verificatielink terug die je naar de eindgebruiker stuurt. De eindgebruiker doorloopt zelf de flow (document + eventueel selfie); deze tool doet zelf geen controle. De gezichtsvergelijking in het document_liveness-pad is een indicatieve AI-controle; een mens doet altijd de eindbeoordeling. Presenteer de uitkomst dus niet als zelfstandig biometrisch bewijs. In de sandbox krijg je een voorspelbaar oordeel zonder kosten en zonder echte documenten. method, reference, sandbox_scenario, auto_complete
get_verification
alleen lezen · scope verify:read
Haalt de status en het oordeel van één verificatie op: pending / in_progress / completed, en bij een afgerond verzoek approved / review / rejected met de losse controles. Geeft GEEN persoonsgegevens, documentbeelden, MRZ-velden of dossierverwijzing terug — daarvoor is de console met menselijke toegangscontrole. verification_id (verplicht)
list_verifications
alleen lezen · scope verify:read
Somt je verificaties op binnen de omgeving van je sleutel: id, status, methode, uitkomst en datum. Bewust GEEN namen, kenmerken of andere persoonsgegevens. Je kunt wél filteren op je eigen reference — die wordt vergeleken maar nooit teruggegeven. Maximaal 25 per aanroep; gebruik starting_after om verder te bladeren. limit, status, reference, starting_after
complete_verification
schrijft · alleen sandbox · scope verify:create
Alleen in de sandbox: rondt een openstaande verificatie af met een voorspelbaar oordeel, alsof de eindgebruiker de link met een specimen-document had doorlopen. Bestaat niet met een live-sleutel — echte verificaties worden altijd door de eindgebruiker zelf doorlopen. verification_id (verplicht), scenario
create_webhook
schrijft · scope webhooks:manage
Registreert een https-endpoint waar wij gebeurtenissen naartoe sturen (bijvoorbeeld verification.completed). Het ondertekeningsgeheim krijg je één keer terug — bewaar het, je kunt het daarna niet meer opvragen. Verwijderen of uitschakelen kan niet via een agent. url (verplicht), description, event_types, environment
list_webhooks
alleen lezen · scope webhooks:manage
Somt je geregistreerde webhook-endpoints op met status en laatste aflevering. Ondertekeningsgeheimen komen hier niet in voor. geen

Over de gezichtsvergelijking zijn de toolbeschrijvingen expliciet: het is een indicatieve AI-controle met een menselijke eindbeoordeling, geen zelfstandig biometrisch bewijs. Een agent die dat leest, presenteert approved hopelijk ook zo.

Sandbox-voorbeeld

Wil je het protocol handmatig zien, dan is dit de volledige rondrit met curl:

KEY=mxa_test_je_sleutel

# 1. handshake
curl -s https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"mijn-agent","version":"1.0"}}}'

# 2. welke tools heb ik?
curl -s https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 3. een verificatie starten (sandbox: meteen een voorspelbaar oordeel)
curl -s https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"start_verification",
                 "arguments":{"method":"document_liveness","reference":"klant-8821",
                              "sandbox_scenario":"approved"}}}'

Het antwoord op stap 3 — let op wat er niet in staat:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{ … }"
      }
    ],
    "structuredContent": {
      "id": "b3f1c8a2-…",
      "object": "verification",
      "status": "completed",
      "environment": "sandbox",
      "method": "document_liveness",
      "created_at": "2026-08-06T10:00:00.000Z",
      "verdict": {
        "outcome": "approved",
        "assurance_level": "substantial",
        "reviewed": false,
        "checks": [
          {
            "kind": "document_authenticity",
            "result": "pass",
            "confidence": 0.97
          },
          {
            "kind": "face_match",
            "result": "pass",
            "confidence": 0.93
          }
        ]
      },
      "verify_url": "https://…/?t=vr_…",
      "next_step": "Stuur verify_url naar de eindgebruiker. Volg daarna get_verification tot status 'completed'."
    },
    "isError": false
  }
}

Geen reference, geen dossier_ref, geen vrije tekst. De verify_url is de enige plek waar een token naar buiten komt — die moet jij naar je eindgebruiker sturen, en hij verschijnt nooit bij opvragen of in een lijst.

Fouten

Protocolfouten zijn JSON-RPC-fouten; een tool die zijn werk niet kan doen (verificatie niet gevonden, verkeerde omgeving) geeft een normaal resultaat met isError: true, zodat de agent zichzelf kan corrigeren.

codebetekenis
-32700body is geen geldige JSON
-32601onbekende JSON-RPC-methode
-32602onbekende tool, of een tool die in deze omgeving niet bestaat
-32001ontbrekende, ongeldige, ingetrokken of verlopen API-sleutel (HTTP 401)
-32003de sleutel mist de vereiste scope (HTTP 403)
-32029rate limit bereikt (HTTP 429, met Retry-After)

GET https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/mcp geeft bewust 405: deze server biedt geen server-naar-client-stroom aan en is stateless — er is dus ook geen sessie-id.

Endpoint-referentie

Gegenereerd uit de OpenAPI-spec (openapi.yaml · openapi.json).

GET/health

Liveness-check

Antwoorden: 200

POST/verifications

Start een verificatie

Maakt een verificatie-verzoek en retourneert een `verify_url` die je de eindgebruiker laat doorlopen. In de sandbox-omgeving wordt de verificatie standaard direct met een voorspelbaar verdict afgerond (stel `sandbox.auto_complete: false` in om de asynchrone levenscyclus na te bootsen).

Parameters: IdempotencyKey

Antwoorden: 201, 401, 403, 422, 429

GET/verifications

Lijst verificaties (cursor-paginatie)

Parameters: limit (query), starting_after (query)

Antwoorden: 200, 401, 429

GET/verifications/{id}

Haal één verificatie op (status + verdict + dossier-ref)

Parameters: VerificationId

Antwoorden: 200, 401, 404

POST/verifications/{id}/complete

Rond een sandbox-verificatie af (alleen sandbox)

Simuleert dat de eindgebruiker de verify-link met een specimen-document heeft doorlopen; levert een voorspelbaar verdict (= scenario) en vuurt de webhook af. Werkt uitsluitend met een sandbox-sleutel.

Parameters: VerificationId

Antwoorden: 200, 403, 404, 409

POST/webhooks

Registreer een webhook-endpoint

Retourneert het `secret` ÉÉNMALIG. Bewaar het veilig; het wordt daarna nooit meer getoond. Gebruik het om de `X-Maxiaa-Signature`-header te verifiëren.

Antwoorden: 201, 401, 403, 422

GET/webhooks

Lijst webhook-endpoints (zonder secret)

Antwoorden: 200

GET/webhooks/{id}

Haal één webhook-endpoint op

Parameters: WebhookId

Antwoorden: 200, 404

DELETE/webhooks/{id}

Deactiveer een webhook-endpoint

Parameters: WebhookId

Antwoorden: 200, 404

Changelog

  • v1.1.0MCP-server voor AI-agents (streamable HTTP, JSON-RPC 2.0) op dezelfde API-sleutel. Bewust smaller dan de REST-API: geen inkomensmodule, geen dossierverwijzing, geen persoonsgegevens. De REST-API is ongewijzigd.
  • v1.0.0 — Eerste publieke release: /v1/verifications, /v1/webhooks, API-key-auth, sandbox, HMAC-signed webhooks, rate-limiting, idempotency, OpenAPI 3.0-spec.

© 2026 Maxiaa · OpenAPI