Leden (People) API Documentation
This document describes how to use the Rondo Club REST API to add and update “leden” (people/contacts).
Base URL
Section titled “Base URL”All endpoints are relative to your WordPress installation:
https://your-site.com/wp-json/Authentication
Section titled “Authentication”The API supports two authentication methods:
Method 1: Application Password (Recommended for External Integrations)
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.
- Generate an Application Password in WordPress: Users → Profile → Application Passwords
- Use your WordPress username and the generated password (with spaces)
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:
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)"Method 2: Session + Nonce (Browser Use)
Section titled “Method 2: Session + Nonce (Browser Use)”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.
Endpoints Overview
Section titled “Endpoints Overview”| Method | Endpoint | Description |
|---|---|---|
GET | /wp/v2/people | List all accessible people |
GET | /wp/v2/people/{id} | Get single person |
POST | /wp/v2/people | Create new person |
PUT | /wp/v2/people/{id} | Update person |
DELETE | /wp/v2/people/{id} | Delete person |
POST | /rondo/v1/people/bulk-update | Update multiple people |
POST | /rondo/v1/people/{id}/photo | Upload profile photo |
POST | /rondo/v1/people/{id}/provision | Provision WordPress user account (admin only) |
Field Reference
Section titled “Field Reference”Required Fields
Section titled “Required Fields”| Field | Type | Description |
|---|---|---|
acf.first_name | string | Person’s first name (required for auto-title generation) |
Basic Information
Section titled “Basic Information”| Field | Type | Description | Values/Format |
|---|---|---|---|
acf.first_name | string | First name | Any string |
acf.infix | string | Tussenvoegsel (e.g., van, de, van der) | Any string. Read-only in UI, synced from Sportlink |
acf.last_name | string | Last name | Any string |
acf.nickname | string | Nickname | Any string |
acf.gender | string | Gender | male, female, non_binary, other, prefer_not_to_say |
acf.pronouns | string | Pronouns | e.g., “hij/hem”, “zij/haar” |
acf.birthdate | string | Birthdate | Y-m-d format (e.g., “1982-02-06”). Read-only in UI, synced from Sportlink |
Membership Status
Section titled “Membership Status”| Field | Type | Description | Values |
|---|---|---|---|
acf.former_member | boolean | Whether 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 Information
Section titled “Contact Information”Contact info is stored as a repeater field with multiple entries:
"acf": { "contact_info": [ { "contact_type": "email", "contact_label": "Werk", "contact_value": "jan@bedrijf.nl" }, { "contact_type": "mobile", "contact_label": "Privé", "contact_value": "+31612345678" } ]}Contact Types:
email- E-mailadresphone- Telefoon (vast)mobile- Mobielwebsite- Websitelinkedin- LinkedIntwitter- Twitter/Xbluesky- Blueskythreads- Threadsinstagram- Instagramfacebook- Facebookcalendar- Agenda linkother- Anders
Addresses
Section titled “Addresses”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" } ]}Team History
Section titled “Team History”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 } ]}| Field | Type | Description |
|---|---|---|
team | integer | Team post ID |
job_title | string | Position/role title |
description | string | Role description |
start_date | string | Start date (Y-m-d) |
end_date | string | End date (Y-m-d), empty if current |
is_current | boolean | Currently in this role |
Relationships
Section titled “Relationships”Link people to other people:
"acf": { "relationships": [ { "related_person": 123, "relationship_type": 5, "relationship_label": "" } ]}| Field | Type | Description |
|---|---|---|
related_person | integer | Related person post ID |
relationship_type | integer | Relationship type taxonomy term ID |
relationship_label | string | Custom label override |
User Linking (Read-Only)
Section titled “User Linking (Read-Only)”These fields relate to the user provisioning system. They are included in the API response when a person has a linked WordPress user account.
| Field | Type | Description |
|---|---|---|
linked_user_id | int|null | WordPress user ID linked to this person |
welcome_email_sent_at | string|null | ISO timestamp of when the welcome email was sent |
Computed Fields (Read-Only)
Section titled “Computed Fields (Read-Only)”These fields are automatically calculated and should not be set manually. They are included in the API response but ignored on create/update.
| Field | Type | Description | Values |
|---|---|---|---|
acf.huidig-vrijwilliger | string | Current volunteer status, auto-calculated from work history | "1" (volunteer) or "0" (not) |
acf.is_deceased | boolean | Whether the person is deceased | true, false |
acf.birth_year | string/null | Birth 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.
Visibility
Section titled “Visibility”| Field | Type | Description | Values |
|---|---|---|---|
acf._visibility | string | Who can see this person | private, workspace, shared |
acf._assigned_workspaces | array | Workspace term IDs | [1, 2, 3] |
Create a Person
Section titled “Create a Person”Request:
POST /wp/v2/peopleContent-Type: application/jsonX-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": "jan.devries@example.nl" }, { "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.
Update a Person
Section titled “Update a Person”Request:
PUT /wp/v2/people/456Content-Type: application/jsonX-WP-Nonce: {nonce}Body (partial update):
{ "acf": { "contact_info": [ { "contact_type": "email", "contact_label": "Werk", "contact_value": "jan@nieuwbedrijf.nl" }, { "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):
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}}'Get a Person
Section titled “Get a Person”Request:
GET /wp/v2/people/456X-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": [] }}List People
Section titled “List People”Request:
GET /wp/v2/people?per_page=20&page=1X-WP-Nonce: {nonce}Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
per_page | int | 10 | Items per page (max: 100) |
page | int | 1 | Page number |
search | string | - | Search in name |
orderby | string | date | Sort by: date, title, modified |
order | string | desc | Sort order: asc or desc |
_fields | string | - | Limit fields returned (comma-separated) |
Example - Search for people named “Jan”:
GET /wp/v2/people?search=Jan&per_page=50Example - Get only IDs and names (faster):
GET /wp/v2/people?_fields=id,title,acf.first_name,acf.last_nameDelete a Person
Section titled “Delete a Person”Request:
DELETE /wp/v2/people/456X-WP-Nonce: {nonce}Response (200 OK):
{ "deleted": true, "previous": { ... }}Bulk Update People
Section titled “Bulk Update People”Update multiple people at once (e.g., assign to workspace, add labels).
Request:
POST /rondo/v1/people/bulk-updateContent-Type: application/jsonX-WP-Nonce: {nonce}Body:
{ "ids": [456, 457, 458], "updates": { "visibility": "workspace", "assigned_workspaces": [5], "organization_id": 42 }}Available bulk updates:
| Field | Type | Description |
|---|---|---|
visibility | string | Set visibility for all |
assigned_workspaces | array | Set workspace IDs |
organization_id | int | Set team association |
Response:
{ "success": true, "updated": [456, 457, 458], "failed": []}Upload Profile Photo
Section titled “Upload Profile Photo”Request:
POST /rondo/v1/people/456/photoContent-Type: multipart/form-dataX-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"}Error Handling
Section titled “Error Handling”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 }}Code Examples
Section titled “Code Examples”JavaScript/TypeScript (fetch)
Section titled “JavaScript/TypeScript (fetch)”const API_BASE = 'https://your-site.com/wp-json';const nonce = window.rondoConfig?.nonce || 'your-nonce';
// Create a personasync 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 personasync 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();}
// Usageconst newPerson = await createPerson({ first_name: 'Jan', last_name: 'de Vries', gender: 'male', contact_info: [ { contact_type: 'email', contact_label: 'Werk', contact_value: 'jan@example.nl' } ],});
console.log('Created person:', newPerson.id);PHP (WordPress context)
Section titled “PHP (WordPress context)”<?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' => 'jan@example.nl', ], ], $person_id);}
// Update a personupdate_field('nickname', 'Jan-Jan', $person_id);cURL (with Application Password)
Section titled “cURL (with Application Password)”# Create a personcurl -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 peoplecurl -X GET "https://your-site.com/wp-json/wp/v2/people?per_page=10" \ -u "username:xxxx xxxx xxxx xxxx xxxx xxxx"
# Delete a personcurl -X DELETE "https://your-site.com/wp-json/wp/v2/people/456" \ -u "username:xxxx xxxx xxxx xxxx xxxx xxxx"-
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. -
Repeater Fields: When updating
contact_info,addresses,work_history, orrelationships, always send the complete array. WordPress will replace the entire field. -
Access Control: Each user only sees people they created. Use visibility settings and sharing to extend access.
-
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.
-
Rate Limiting: There’s no built-in rate limiting, but be mindful of server resources when making bulk requests.
Documentation generated: 2026-01-25