Quickstart
Four steps from a key to a working integration. Allow ten minutes.
1. Create a key
An administrator of the association creates keys in Ledenboek under Settings → API keys → New key. Give the key a name that tells you later what it is for, and choose its scopes. By default everything is read-only; for step 4 you also need leden:schrijven.
The key is shown once. Put it in an environment variable, not in your code:
export LEDENBOEK_API_KEY="lb_test_..."In a sample association the key starts with lb_test_. No email and no payment requests are sent there, so you can try everything without touching real members.
2. Your first request
GET /v2/organization works with any key and returns the association, the modules it has enabled and the scopes of your key. A good first check that everything is set up.
curl https://api.ledenboek.nl/v2/organization \
-H "Authorization: Bearer $LEDENBOEK_API_KEY"const response = await fetch("https://api.ledenboek.nl/v2/organization", {
headers: { Authorization: `Bearer ${process.env.LEDENBOEK_API_KEY}` },
});
const organization = await response.json();
console.log(organization.name, organization.api_key.scopes);<?php
$ch = curl_init("https://api.ledenboek.nl/v2/organization");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("LEDENBOEK_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$organization = json_decode(curl_exec($ch), true);
echo $organization["name"];import os
import requests
response = requests.get(
"https://api.ledenboek.nl/v2/organization",
headers={"Authorization": f"Bearer {os.environ['LEDENBOEK_API_KEY']}"},
)
organization = response.json()
print(organization["name"], organization["api_key"]["scopes"])The response:
{
"object": "organization",
"id": 42,
"name": "WSV De Kaag",
"slug": "wsv-de-kaag",
"country": "Nederland",
"language": "nl",
"website_url": "https://www.wsvdekaag.nl",
"modules": {
"groups": true,
"committees": true,
"participants": false,
"relationships": false,
"events": true,
"invoicing": true
},
"api_key": {
"name": "Website integration",
"prefix": "lb_test_8kQz",
"environment": "test",
"scopes": [
"leden:lezen",
"leden:schrijven"
]
}
}A 401 usually means the header is not exactly Authorization: Bearer lb_.... A 402 means the association has no valid subscription.
3. Page through members
With the leden:lezen scope you fetch members with GET /v2/members. Lists come in pages of up to 100 records (25 by default). As long as has_more is true, request the next page by passing next_cursor as starting_after.
curl "https://api.ledenboek.nl/v2/members?limit=100" \
-H "Authorization: Bearer $LEDENBOEK_API_KEY"
# Next page: pass next_cursor from the previous response
curl "https://api.ledenboek.nl/v2/members?limit=100&starting_after=1234" \
-H "Authorization: Bearer $LEDENBOEK_API_KEY"const headers = { Authorization: `Bearer ${process.env.LEDENBOEK_API_KEY}` };
const members = [];
let cursor = null;
do {
const url = new URL("https://api.ledenboek.nl/v2/members");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const page = await (await fetch(url, { headers })).json();
members.push(...page.data);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);<?php
$members = [];
$cursor = null;
do {
$query = http_build_query(array_filter(["limit" => 100, "starting_after" => $cursor]));
$ch = curl_init("https://api.ledenboek.nl/v2/members?" . $query);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("LEDENBOEK_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$page = json_decode(curl_exec($ch), true);
$members = array_merge($members, $page["data"]);
$cursor = $page["has_more"] ? $page["next_cursor"] : null;
} while ($cursor !== null);import os
import requests
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['LEDENBOEK_API_KEY']}"
members, cursor = [], None
while True:
params = {"limit": 100}
if cursor:
params["starting_after"] = cursor
page = session.get("https://api.ledenboek.nl/v2/members", params=params).json()
members.extend(page["data"])
if not page["has_more"]:
break
cursor = page["next_cursor"]A page looks like this (shortened; all fields are in the reference):
{
"object": "list",
"data": [
{
"object": "member",
"id": 1234,
"membership_number": "L042",
"first_name": "Anna",
"last_name": "de Vries",
"email": "anna@example.com",
"status": "active",
"member_since": "2024-03-01",
"created_at": "2024-03-01T10:12:00+01:00",
"updated_at": "2026-10-02T08:30:00+02:00"
}
],
"has_more": true,
"next_cursor": "1234"
}4. Create a member
With leden:schrijven you create a member with POST /v2/members. Send an Idempotency-Key: a unique value per new member, such as a UUID. If the connection drops and you send the same request again with the same key, you get the first response back and the member is not created twice.
curl https://api.ledenboek.nl/v2/members \
-X POST \
-H "Authorization: Bearer $LEDENBOEK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a3e-8d4b-4c1e-9a7f-2b5d0e9c4a11" \
-d '{"first_name": "Anna", "last_name": "de Vries", "email": "anna@example.com"}'// Store this key with your own record, so a retry sends the same one.
const idempotencyKey = crypto.randomUUID();
const response = await fetch("https://api.ledenboek.nl/v2/members", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LEDENBOEK_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ first_name: "Anna", last_name: "de Vries", email: "anna@example.com" }),
});
const member = await response.json();<?php
// Store this key with your own record, so a retry sends the same one.
$idempotencyKey = bin2hex(random_bytes(16));
$ch = curl_init("https://api.ledenboek.nl/v2/members");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LEDENBOEK_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: " . $idempotencyKey,
],
CURLOPT_POSTFIELDS => json_encode([
"first_name" => "Anna",
"last_name" => "de Vries",
"email" => "anna@example.com",
]),
CURLOPT_RETURNTRANSFER => true,
]);
$member = json_decode(curl_exec($ch), true);import os
import uuid
import requests
# Store this key with your own record, so a retry sends the same one.
idempotency_key = str(uuid.uuid4())
response = requests.post(
"https://api.ledenboek.nl/v2/members",
headers={
"Authorization": f"Bearer {os.environ['LEDENBOEK_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={"first_name": "Anna", "last_name": "de Vries", "email": "anna@example.com"},
)
member = response.json()If a field is invalid, you get a 422 that says what is wrong per field:
{
"type": "https://developers.ledenboek.nl/errors/validation_failed",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields are invalid; see errors.",
"instance": "/v2/members",
"errors": [
{
"field": "email",
"code": "invalid",
"message": "Email is invalid"
}
]
}Next steps
- Authentication: which scopes exist and what a test key does differently.
- Concepts: use
updated_sinceto fetch only what changed. - API reference: all endpoints. With the spec at /openapi.json you can generate a client in your own language, for example with OpenAPI Generator.