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.
Base-URL
https://wkrhvuxmsbvzqluwigst.supabase.co/functions/v1/v1
Quickstart — je eerste verificatie in 5 minuten
- Log in op de console en maak een sandbox-sleutel (
mxa_test_…). - 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:
| scenario | verdict |
|---|---|
approved | approved (alle checks pass) |
review | review (gezichtsvergelijking twijfel) |
rejected | rejected (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_…"
}
}
| status | type |
|---|---|
| 400 | invalid_request_error (bv. invalid_json) |
| 401 | authentication_error (missing/invalid/expired key) |
| 403 | authentication_error (insufficient_scope) / invalid_request_error (not_sandbox) |
| 404 | not_found_error |
| 409 | invalid_request_error (already_completed) |
| 422 | idempotency_error / invalid_request_error |
| 429 | rate_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.
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
| Endpoint | Waarom niet |
|---|---|
POST /v1/income-checks | neemt 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-checks | lijst 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_ref | het 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
| Veld | Waarom niet |
|---|---|
reference | door 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_ref | zelfde als reference; is het DB-veld eronder. |
subject_ref | vrij tekstveld over de te verifiëren persoon — per definitie persoonsgegeven. |
dossier_ref | verwijzing 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_token | geeft toegang tot de verificatieflow van de eindgebruiker. |
verify_url | bevat 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. |
reasons | vrije tekst uit de beslislogica. Machineleesbare equivalent is verdict.checks; vrije tekst kan onbedoeld extractie-details bevatten. |
summary | de 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.
| Tool | Wat het doet | Parameters |
|---|---|---|
healthalleen 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_verificationschrijft · 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_verificationalleen 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_verificationsalleen 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_verificationschrijft · 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_webhookschrijft · 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_webhooksalleen 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.
| code | betekenis |
|---|---|
-32700 | body is geen geldige JSON |
-32601 | onbekende JSON-RPC-methode |
-32602 | onbekende tool, of een tool die in deze omgeving niet bestaat |
-32001 | ontbrekende, ongeldige, ingetrokken of verlopen API-sleutel (HTTP 401) |
-32003 | de sleutel mist de vereiste scope (HTTP 403) |
-32029 | rate 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).
/healthLiveness-check
Antwoorden: 200
/verificationsStart 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
/verificationsLijst verificaties (cursor-paginatie)
Parameters: limit (query), starting_after (query)
Antwoorden: 200, 401, 429
/verifications/{id}Haal één verificatie op (status + verdict + dossier-ref)
Parameters: VerificationId
Antwoorden: 200, 401, 404
/verifications/{id}/completeRond 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
/webhooksRegistreer 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
/webhooksLijst webhook-endpoints (zonder secret)
Antwoorden: 200
/webhooks/{id}Haal één webhook-endpoint op
Parameters: WebhookId
Antwoorden: 200, 404
/webhooks/{id}Deactiveer een webhook-endpoint
Parameters: WebhookId
Antwoorden: 200, 404
Changelog
- v1.1.0 — MCP-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