Skip to content

Leden (People) API Documentation

This document describes how to use the Rondo Club REST API to add and update “leden” (people/contacts).

All endpoints are relative to your WordPress installation:

https://your-site.com/wp-json/

The API supports two authentication methods:

Section titled “Method 1: Application Password (Recommended for External Integrations)”

Use HTTP Basic Authentication with a WordPress Application Password. This is the recommended method for scripts, external services, and API integrations.

  1. Generate an Application Password in WordPress: Users → Profile → Application Passwords
  2. Use your WordPress username and the generated password (with spaces)
Terminal window
curl -X GET "https://your-site.com/wp-json/wp/v2/people" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx"

Or with the Authorization header:

Terminal window
curl -X GET "https://your-site.com/wp-json/wp/v2/people" \
-H "Authorization: Basic $(echo -n 'username:xxxx xxxx xxxx xxxx xxxx xxxx' | base64)"

For requests from the Rondo Club frontend (same browser session), use the REST nonce:

X-WP-Nonce: {nonce_value}

The nonce is available in window.rondoConfig.nonce when logged in to Rondo Club.


Access Control: Users can only see and modify people they created themselves. Sharing and workspace visibility can extend access to other users.


MethodEndpointDescription
GET/wp/v2/peopleList all accessible people
GET/wp/v2/people/{id}Get single person
POST/wp/v2/peopleCreate new person
PUT/wp/v2/people/{id}Update person
DELETE/wp/v2/people/{id}Delete person
POST/rondo/v1/people/bulk-updateUpdate multiple people
POST/rondo/v1/people/{id}/photoUpload profile photo
POST/rondo/v1/people/{id}/provisionProvision WordPress user account (admin only)

FieldTypeDescription
acf.first_namestringPerson’s first name (required for auto-title generation)
FieldTypeDescriptionValues/Format
acf.first_namestringFirst nameAny string
acf.infixstringTussenvoegsel (e.g., van, de, van der)Any string. Read-only in UI, synced from Sportlink
acf.last_namestringLast nameAny string
acf.nicknamestringNicknameAny string
acf.genderstringGendermale, female, non_binary, other, prefer_not_to_say
acf.pronounsstringPronounse.g., “hij/hem”, “zij/haar”
acf.birthdatestringBirthdateY-m-d format (e.g., “1982-02-06”). Read-only in UI, synced from Sportlink
FieldTypeDescriptionValues
acf.former_memberbooleanWhether the person is a former member (oud-lid)true, false (default)

Note: This field is managed by rondo-sync. When a member is no longer found in Sportlink data, they are automatically marked as a former member. The field defaults to false for all new and active members.

Contact info is stored as a repeater field with multiple entries:

"acf": {
"contact_info": [
{
"contact_type": "email",
"contact_label": "Werk",
"contact_value": "[email protected]"
},
{
"contact_type": "mobile",
"contact_label": "Privé",
"contact_value": "+31612345678"
}
]
}

Contact Types:

  • email - E-mailadres
  • phone - Telefoon (vast)
  • mobile - Mobiel
  • website - Website
  • linkedin - LinkedIn
  • twitter - Twitter/X
  • bluesky - Bluesky
  • threads - Threads
  • instagram - Instagram
  • facebook - Facebook
  • calendar - Agenda link
  • other - Anders

Addresses are stored as a repeater field:

"acf": {
"addresses": [
{
"address_label": "Thuis",
"street": "Hoofdstraat 123",
"postal_code": "1234 AB",
"city": "Amsterdam",
"state": "Noord-Holland",
"country": "Nederland"
}
]
}

Link people to teams with their role history:

"acf": {
"work_history": [
{
"team": 42,
"job_title": "Aanvoerder",
"description": "Aanvoerder van het eerste elftal",
"start_date": "2020-08-01",
"end_date": "",
"is_current": true
}
]
}
FieldTypeDescription
teamintegerTeam post ID
job_titlestringPosition/role title
descriptionstringRole description
start_datestringStart date (Y-m-d)
end_datestringEnd date (Y-m-d), empty if current
is_currentbooleanCurrently in this role

Link people to other people:

"acf": {
"relationships": [
{
"related_person": 123,
"relationship_type": 5,
"relationship_label": ""
}
]
}
FieldTypeDescription
related_personintegerRelated person post ID
relationship_typeintegerRelationship type taxonomy term ID
relationship_labelstringCustom label override

These fields relate to the user provisioning system. They are included in the API response when a person has a linked WordPress user account.

FieldTypeDescription
linked_user_idint|nullWordPress user ID linked to this person
welcome_email_sent_atstring|nullISO timestamp of when the welcome email was sent

These fields are automatically calculated and should not be set manually. They are included in the API response but ignored on create/update.

FieldTypeDescriptionValues
acf.huidig-vrijwilligerstringCurrent volunteer status, auto-calculated from work history"1" (volunteer) or "0" (not)
acf.is_deceasedbooleanWhether the person is deceasedtrue, false
acf.birth_yearstring/nullBirth year (derived from birthdate field)e.g., "1990" or null

Volunteer status logic: A person is considered a current volunteer (huidig-vrijwilliger = "1") if they have an active position where:

  • The position is in a commissie (any role, unless the commissie is exempt), OR
  • The position is in a team with a staff role (not a player role like Aanvaller, Keeper, etc.)

Positions with honorary/membership roles (Donateur, Erelid, etc.) are excluded. The status is recalculated automatically whenever the person is saved.

FieldTypeDescriptionValues
acf._visibilitystringWho can see this personprivate, workspace, shared
acf._assigned_workspacesarrayWorkspace term IDs[1, 2, 3]

Request:

POST /wp/v2/people
Content-Type: application/json
X-WP-Nonce: {nonce}

Body:

{
"status": "publish",
"acf": {
"first_name": "Jan",
"last_name": "de Vries",
"gender": "male",
"contact_info": [
{
"contact_type": "email",
"contact_label": "Werk",
"contact_value": "[email protected]"
},
{
"contact_type": "mobile",
"contact_label": "Privé",
"contact_value": "+31612345678"
}
],
"addresses": [
{
"address_label": "Thuis",
"street": "Sportlaan 45",
"postal_code": "1234 AB",
"city": "Amsterdam",
"country": "Nederland"
}
],
"_visibility": "private"
}
}

Response (201 Created):

{
"id": 456,
"date": "2026-01-25T14:30:00",
"slug": "jan-de-vries",
"status": "publish",
"type": "person",
"title": {
"rendered": "Jan de Vries"
},
"author": 1,
"acf": {
"first_name": "Jan",
"infix": "",
"last_name": "de Vries",
"gender": "male",
"birthdate": "",
"former_member": false,
"contact_info": [...],
"addresses": [...],
"work_history": [],
"relationships": [],
"huidig-vrijwilliger": "0",
"is_deceased": false,
"birth_year": null,
"_visibility": "private"
}
}

Note: The title is automatically generated from first_name, infix, and last_name. You don’t need to set it manually.


Request:

PUT /wp/v2/people/456
Content-Type: application/json
X-WP-Nonce: {nonce}

Body (partial update):

{
"acf": {
"contact_info": [
{
"contact_type": "email",
"contact_label": "Werk",
"contact_value": "[email protected]"
},
{
"contact_type": "mobile",
"contact_label": "Privé",
"contact_value": "+31612345678"
}
]
}
}

Important: When updating repeater fields (contact_info, addresses, work_history, relationships), you must send the complete array. Partial updates will replace the entire field.

Response (200 OK): Returns the full updated person object.

Example - Mark a member as former (used by rondo-sync):

Terminal window
curl -X PUT "https://your-site.com/wp-json/wp/v2/people/456" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{"acf": {"former_member": true}}'

Request:

GET /wp/v2/people/456
X-WP-Nonce: {nonce}

Response:

{
"id": 456,
"title": { "rendered": "Jan de Vries" },
"acf": {
"first_name": "Jan",
"infix": "",
"last_name": "de Vries",
"nickname": "",
"gender": "male",
"pronouns": "",
"birthdate": "",
"former_member": false,
"photo_gallery": [],
"contact_info": [...],
"addresses": [...],
"work_history": [],
"relationships": [],
"huidig-vrijwilliger": "0",
"is_deceased": false,
"birth_year": null,
"_visibility": "private",
"_assigned_workspaces": []
}
}

Request:

GET /wp/v2/people?per_page=20&page=1
X-WP-Nonce: {nonce}

Query Parameters:

ParameterTypeDefaultDescription
per_pageint10Items per page (max: 100)
pageint1Page number
searchstring-Search in name
orderbystringdateSort by: date, title, modified
orderstringdescSort order: asc or desc
_fieldsstring-Limit fields returned (comma-separated)

Example - Search for people named “Jan”:

GET /wp/v2/people?search=Jan&per_page=50

Example - Get only IDs and names (faster):

GET /wp/v2/people?_fields=id,title,acf.first_name,acf.last_name

Request:

DELETE /wp/v2/people/456
X-WP-Nonce: {nonce}

Response (200 OK):

{
"deleted": true,
"previous": { ... }
}

Update multiple people at once (e.g., assign to workspace, add labels).

Request:

POST /rondo/v1/people/bulk-update
Content-Type: application/json
X-WP-Nonce: {nonce}

Body:

{
"ids": [456, 457, 458],
"updates": {
"visibility": "workspace",
"assigned_workspaces": [5],
"organization_id": 42
}
}

Available bulk updates:

FieldTypeDescription
visibilitystringSet visibility for all
assigned_workspacesarraySet workspace IDs
organization_idintSet team association

Response:

{
"success": true,
"updated": [456, 457, 458],
"failed": []
}

Request:

POST /rondo/v1/people/456/photo
Content-Type: multipart/form-data
X-WP-Nonce: {nonce}

Form Data:

  • file: Image file (JPEG, PNG, GIF, WebP)

Response:

{
"success": true,
"attachment_id": 789,
"filename": "jan-de-vries.jpg",
"thumbnail_url": "https://your-site.com/wp-content/uploads/2026/01/jan-de-vries-150x150.jpg",
"full_url": "https://your-site.com/wp-content/uploads/2026/01/jan-de-vries.jpg"
}

Common Error Responses:

401 Unauthorized:

{
"code": "rest_not_logged_in",
"message": "You are not currently logged in.",
"data": { "status": 401 }
}

403 Forbidden:

{
"code": "rest_forbidden",
"message": "Sorry, you are not allowed to edit this person.",
"data": { "status": 403 }
}

404 Not Found:

{
"code": "rest_post_invalid_id",
"message": "Invalid person ID.",
"data": { "status": 404 }
}

400 Bad Request (validation error):

{
"code": "rest_invalid_param",
"message": "Invalid parameter(s): acf",
"data": { "status": 400 }
}

const API_BASE = 'https://your-site.com/wp-json';
const nonce = window.rondoConfig?.nonce || 'your-nonce';
// Create a person
async function createPerson(data) {
const response = await fetch(`${API_BASE}/wp/v2/people`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
credentials: 'include',
body: JSON.stringify({
status: 'publish',
acf: data,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Update a person
async function updatePerson(id, data) {
const response = await fetch(`${API_BASE}/wp/v2/people/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
credentials: 'include',
body: JSON.stringify({ acf: data }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Usage
const newPerson = await createPerson({
first_name: 'Jan',
last_name: 'de Vries',
gender: 'male',
contact_info: [
{ contact_type: 'email', contact_label: 'Werk', contact_value: '[email protected]' }
],
});
console.log('Created person:', newPerson.id);
<?php
// Create a person programmatically
$person_id = wp_insert_post([
'post_type' => 'person',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
]);
if ($person_id && !is_wp_error($person_id)) {
// Set ACF fields
update_field('first_name', 'Jan', $person_id);
update_field('last_name', 'de Vries', $person_id);
update_field('gender', 'male', $person_id);
// Set contact info (repeater)
update_field('contact_info', [
[
'contact_type' => 'email',
'contact_label' => 'Werk',
'contact_value' => '[email protected]',
],
], $person_id);
}
// Update a person
update_field('nickname', 'Jan-Jan', $person_id);
Terminal window
# Create a person
curl -X POST "https://your-site.com/wp-json/wp/v2/people" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{
"status": "publish",
"acf": {
"first_name": "Jan",
"last_name": "de Vries",
"gender": "male"
}
}'
# Update a person (change name)
curl -X PUT "https://your-site.com/wp-json/wp/v2/people/456" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{
"acf": {
"first_name": "Johannes"
}
}'
# List people
curl -X GET "https://your-site.com/wp-json/wp/v2/people?per_page=10" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx"
# Delete a person
curl -X DELETE "https://your-site.com/wp-json/wp/v2/people/456" \
-u "username:xxxx xxxx xxxx xxxx xxxx xxxx"

  1. Auto-generated Title: The post title is automatically created from first_name + infix + last_name (e.g., “Jan van de Berg”). You don’t need to set it.

  2. Repeater Fields: When updating contact_info, addresses, work_history, or relationships, always send the complete array. WordPress will replace the entire field.

  3. Access Control: Each user only sees people they created. Use visibility settings and sharing to extend access.

  4. Authentication Choice:

    • Application Passwords: Best for external scripts, integrations, and automation. No expiration, revocable per-app.
    • Nonces: Best for browser-based requests. Expire after 24 hours.
  5. Rate Limiting: There’s no built-in rate limiting, but be mindful of server resources when making bulk requests.


Documentation generated: 2026-01-25