API

Read out your passports and your supply chain register, add suppliers, and have us tell you as soon as something changes. The API is part of Plus. You create the key in My Ketenpas under API; it is shown once.

Create a key in My Ketenpas

Authentication

Every request carries the key as a bearer token. Responses are json, including errors.

curl https://ketenpas.nl/api/v1/paspoorten \
  -H "Authorization: Bearer kp_uw_sleutel"

Limits

120 requests per minute per organisation, across all routes together. Invitations count separately on top of that: 25 per hour, the same bucket the screen uses.

Entities

A key belongs to one organisation. If you work with entities, you create a key per entity; the parent's API does not show a subsidiary's data.

Routes

  • GET /api/v1/paspoorten

    All passports of your organisation, with score, validity and the shareable link.

  • POST /api/v1/paspoorten/{id}/intrekken

    Withdraws a passport. The shared link stops working immediately, including for anyone who already had it.

  • GET /api/v1/keten

    Your supply chain register: every supplier with status, score and validity.

  • POST /api/v1/keten

    Adds a supplier. With "uitnodigen": true the invitation goes out straight away.

  • POST /api/v1/keten/{id}/uitnodiging

    Sends the invitation (again) to a supplier already in the register.

  • DELETE /api/v1/keten/{id}

    Removes a supplier from the register. The invitation link stops working.

A passport

FieldTypeMeans
iduuidThe id you pass when withdrawing.
labelstring | nullThe name you gave the passport.
status"geldig" | "verlopen" | "ingetrokken"The link only works while this is geldig.
scorenumber0 to 100, over the full questionnaire.
niveaustringThe wording that belongs to the score.
beoordeeldOpstringISO 8601, the date of the underlying scan.
geldigTotstringISO 8601.
ingetrokkenOpstring | nullISO 8601, empty while the passport runs.
urlstring | nullThe shareable link; empty once the passport is no longer valid.

A supplier

FieldTypeMeans
iduuidThe id for the routes below.
naamstring
emailstringWhere the invitation is sent.
contactpersoonstring | null
status"uitgenodigd" | "gekoppeld" | "vervallen"Uitgenodigd is also the state of someone who has not been mailed yet.
scorenumber | nullFilled as soon as a valid passport is linked.
niveaustring | null
beoordeeldOpstring | nullISO 8601.
geldigTotstring | nullISO 8601.
notitiestring | nullYour own note about this supplier.

Adding a supplier

naam and email are required, contactpersoon and notitie are optional. If the address is already in your register, no second row is added; you get a 409 with the id of the existing one, so a nightly sync does not build up duplicates.

curl -X POST https://ketenpas.nl/api/v1/keten \
  -H "Authorization: Bearer kp_uw_sleutel" \
  -H "Content-Type: application/json" \
  -d '{"naam":"Van Dijk Techniek","email":"security@vandijk.nl","uitnodigen":true}'

Errors

Every error is json with one field: fout, containing a sentence you can log. The status code is what you branch on.

CodeMeans
400The body is not json, or a required field is missing.
401Missing or unknown key.
403The key is valid but the plan is not, or your chain limit is reached.
404The id does not exist, or belongs to another organisation.
409The action no longer applies: already withdrawn, or the address is already in your register.
429Too many requests. See the limits above.
502We could not send the invitation. The supplier was still added.

Webhooks

Instead of fetching the register every minute, you can give us one https address to call. You set that under API in My Ketenpas, where you also find the secret and the outcome of the last attempt.

EventWhen
leverancier.gekoppeldA supplier has shared their passport with you.
leverancier.vervallenA supplier's linked passport has expired or been withdrawn. Sent once, in the nightly run.
testThe test message from the button in My Ketenpas.
POST https://uw-systeem.nl/ketenpas
X-Ketenpas-Gebeurtenis: leverancier.gekoppeld
X-Ketenpas-Handtekening: t=1785926400,v1=9f2c...

{
  "gebeurtenis": "leverancier.gekoppeld",
  "verstuurdOp": "2026-08-05T09:20:00.000Z",
  "gegevens": {
    "leverancier": {
      "id": "8f1d...",
      "naam": "Van Dijk Techniek",
      "email": "security@vandijk.nl"
    }
  }
}

Verifying the signature

Every message carries the X-Ketenpas-Handtekening header with a timestamp and an hmac-sha256 over timestamp, dot, and the literal body. Recompute it with your secret and compare in constant time. Reject a message whose timestamp is more than five minutes old.

const [t, v1] = kop.split(",").map((deel) => deel.split("=")[1]);
const verwacht = crypto
  .createHmac("sha256", geheim)
  .update(`${t}.${ruweBody}`)
  .digest("hex");

const klopt =
  verwacht.length === v1.length &&
  crypto.timingSafeEqual(Buffer.from(verwacht), Buffer.from(v1)) &&
  Math.abs(Date.now() / 1000 - Number(t)) < 300;

What we promise and what we do not

We try three times: immediately, after two seconds and after eight seconds, waiting five seconds per attempt. Answer with 2xx and we are done; with 4xx we stop straight away, because that will not be better in eight seconds. If your side is down for longer, that message is gone: we keep no queue. Use GET /api/v1/keten as the floor under your records and the webhook to hear about it sooner.

Versions

The path carries the version. Within v1 fields are only added, never removed; a field that disappears or changes meaning becomes v2. So write your side to ignore fields it does not know.