Schema

The schema endpoint provides the authoritative definition of the candidate request payload for your education region.

It describes exactly which fields are accepted when creating or updating a candidate—and how those fields are validated by the API.

Unlike earlier versions, the schema is now returned as a fully standards-compliant JSON Schema that is directly compatible with OpenAPI and mirrors the API’s runtime validation logic one-to-one.

This is made possible by Zod’s native JSON Schema support:
https://zod.dev/json-schema

Why this schema matters

The /schema endpoint returns a schema that:

  • Directly mirrors the API data structure

  • Uses the same validation rules enforced by the API

  • Is fully OpenAPI- and JSON Schema–compatible

  • Includes region-specific custom fields

  • Differentiates between create and update operations

There is no translation layer, no duplication, and no drift between documentation and runtime behavior.

If a payload validates against the returned schema, it will validate against the API.

When to use the /schema endpoint

Before creating or updating a candidate, you should retrieve the schema for your region using the /schema endpoint.

You should always base payload construction and validation on the response of this endpoint, especially when working with:

  • Required vs optional fields

  • Enumerated values

  • Arrays and nested objects

  • Region-specific custom fields

Caching and performance
The first time you request the /schema endpoint for a given region, you should expect a latency of approximately 3–5 seconds. During this initial request, the system performs database introspection to determine the region-specific schema. Subsequent requests return a cached response and are typically served much faster.

Create vs update schemas

The schema endpoint supports both candidate operations:

  • Create schema
    Describes the full payload required to create a new candidate, including required fields.

  • Update schema
    Describes a partial payload for updating an existing candidate. Required fields are relaxed to reflect patch semantics.

You can select the operation using the operation query parameter:

GET /schema?operation=create
GET /schema?operation=update

Each operation returns a schema that exactly matches the validation rules applied by the corresponding /candidates endpoint.

JSON Schema targets

By default, the schema is generated for OpenAPI 3.0 compatibility, making it ideal for API tooling, SDKs, and documentation generators such as Scalar.

You can optionally request a different JSON Schema dialect using the target query parameter:

GET /schema?target=openapi-3.0      (default)
GET /schema?target=draft-2020-12
GET /schema?target=draft-07
GET /schema?target=draft-04

Supported targets correspond to valid JSON Schema specifications:
https://json-schema.org/specification

This allows you to integrate the schema seamlessly with different validators, form generators, or AI structured-output tooling.

What the schema defines

The returned schema is a complete JSON Schema document and defines:

  • Which fields are available

  • Which fields are required

  • Which values are allowed

  • How arrays, nested objects, and unions are structured

  • Whether additional properties are permitted

At the top level, the schema explicitly defines:

  • type

  • properties

  • required

  • additionalProperties

This makes the schema immediately usable with standard JSON Schema validators.

Schema structure

The schema returned by /schema is scoped to the authenticated region and covers the entire candidate payload, consisting of three conceptual parts:

Shared fields

Shared fields represent the standardized candidate data model used across all education regions.

Examples include:

  • Identity and contact information

  • Privacy consent

  • Candidate phase

  • Sector, role, and subject preferences

  • Qualifications and motivation

  • Attachments and documents

Shared fields are always present in the schema and follow consistent validation rules across regions.

Interactions

Interactions represent contact moments or events related to a candidate, such as phone calls, emails, or meetings.

In the schema, interactions are modeled as an array of structured objects, each defining:

  • Required properties (such as interaction type and date)

  • Allowed interaction types

  • Optional descriptive fields (summary, notes, duration)

Interactions can be included when creating or updating a candidate, or managed separately through the dedicated interaction endpoints.

Custom fields

Custom fields are region-specific extensions to the shared candidate schema.

Key characteristics:

  • Returned as a nested schema under customFields

  • Dynamically generated per region

  • Structure, data types, and allowed values may differ between regions

  • Fully described using standard JSON Schema constructs

Custom fields are defined in the same way as shared fields, including:

  • Required vs optional

  • Data type

  • Enumerated values (if applicable)

  • Descriptions

Your integration must treat the custom fields schema as dynamic and must not assume a fixed or global set of custom fields.

Example Schema

Below is an example schema returned by the /schema endpoint for a create operation, in the draft-2020-12 target.

{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"name": {
			"title": "Candidate name",
			"description": "The full name of the candidate.",
			"examples": ["Jan Jansen"],
			"type": "string",
			"minLength": 1
		},
		"email": {
			"title": "Candidate email",
			"description": "The primary email address of the candidate.",
			"examples": ["jan.jansen@email.com"],
			"type": "string",
			"format": "email",
			"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
		},
		"privacyConsent": {
			"title": "Privacy consent",
			"description": "Indicates whether the candidate has given consent for data processing.",
			"examples": [true],
			"type": "boolean"
		},
		"phone": {
			"title": "Candidate phone",
			"description": "The primary phone number of the candidate.",
			"examples": ["+31612345678"],
			"type": "string",
			"minLength": 5
		},
		"linkedin": {
			"title": "LinkedIn profile",
			"description": "The LinkedIn profile URL of the candidate.",
			"examples": ["https://www.linkedin.com/in/example"],
			"type": "string",
			"format": "uri"
		},
		"dateOfBirth": {
			"title": "Date of birth",
			"description": "The date of birth of the candidate.",
			"examples": ["1995-06-01"],
			"type": "string",
			"format": "date",
			"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
		},
		"address": {
			"title": "Address",
			"description": "The residential address of the candidate.",
			"examples": ["Albert Cuypstraat 123"],
			"type": "string"
		},
		"city": {
			"title": "City",
			"description": "The city where the candidate resides.",
			"examples": ["Amsterdam"],
			"type": "string"
		},
		"postalCode": {
			"title": "Postal code",
			"description": "The postal code of the candidate's address.",
			"examples": ["1011AB", "1234 CD"],
			"type": "string"
		},
		"phase": {
			"title": "Candidate phase",
			"description": "Phase the candidate is currently in.",
			"examples": ["nieuw", "oriënteren", "in opleiding"],
			"default": "nieuw",
			"type": "string",
			"enum": [
				"nieuw",
				"oriënteren",
				"in opleiding",
				"matchen",
				"voorbereiden",
				"werkzaam",
				"uitgestroomd"
			]
		},
		"sectorPreferences": {
			"title": "Sector preferences",
			"description": "Sector(s) that the candidate is interested in.",
			"examples": [["Primair onderwijs", "Voortgezet onderwijs", "Speciaal onderwijs"]],
			"type": "array",
			"items": {
				"type": "string",
				"enum": [
					"Primair onderwijs",
					"Voortgezet onderwijs",
					"Speciaal onderwijs",
					"Middelbaar beroepsonderwijs",
					"Hoger onderwijs",
					"Praktijkonderwijs"
				]
			}
		},
		"rolePreferences": {
			"title": "Role preferences",
			"description": "Role(s), jobs and/or positions that the candidate is interested in.",
			"examples": [["Docent / leraar", "Instructeur (mbo)", "OOP"]],
			"type": "array",
			"items": {
				"type": "string",
				"enum": [
					"Docent / leraar",
					"Instructeur (mbo)",
					"OOP",
					"Ondersteuning",
					"Leerlingenzorg",
					"Midden management",
					"Schoolleiding",
					"Anders"
				]
			}
		},
		"desiredQualifications": {
			"title": "Desired qualifications",
			"description": "Qualification(s) that the candidate wants to obtain.",
			"examples": [["Primair onderwijs", "Beperkt tweedegraads", "Tweedegraads"]],
			"type": "array",
			"items": {
				"type": "string",
				"enum": [
					"Primair onderwijs",
					"Beperkt tweedegraads",
					"Tweedegraads",
					"Eerstegraads",
					"PDG",
					"BKO/BDB",
					"Kwalificatie onderwijs ondersteunend personeel",
					"Kwalificatie Instructeur (MBO)",
					"Geen: gastdocentschap",
					"Pabo",
					"Onbekend",
					"Nog geen idee",
					"Niet van toepassing"
				]
			}
		},
		"priorEducation": {
			"title": "Prior education",
			"description": "The highest level of education the candidate has completed.",
			"examples": ["geen", "praktijkonderwijs", "vmbo-bl"],
			"type": "string",
			"enum": [
				"geen",
				"praktijkonderwijs",
				"vmbo-bl",
				"vmbo-kl",
				"vmbo-gl",
				"vmbo-tl",
				"vmbo",
				"havo",
				"vwo",
				"mbo entree",
				"mbo 2",
				"mbo 3",
				"mbo 4",
				"mbo",
				"associate degree",
				"hbo propedeuse",
				"hbo bachelor",
				"hbo master",
				"hbo",
				"wo bachelor",
				"wo master",
				"wo",
				"PhD"
			]
		},
		"subjectPreferences": {
			"title": "Subject preferences",
			"description": "Subject(s) that the candidate is interested in. Relevant for sector \"Voortgezet onderwijs\".",
			"examples": [["Nederlands", "Engels", "Wiskunde"]],
			"type": "array",
			"items": {
				"type": "string",
				"enum": [
					"Nederlands",
					"Engels",
					"Wiskunde",
					"Wiskunde A",
					"Wiskunde B",
					"Wiskunde C",
					"Wiskunde D",
					"Frans",
					"Duits",
					"Spaans",
					"Latijnse taal en cultuur",
					"Griekse taal en cultuur",
					"Klassieke talen",
					"KCV",
					"Geschiedenis",
					"Aardrijkskunde",
					"Maatschappijleer",
					"Maatschappijkunde",
					"Maatschappijwetenschappen",
					"Filosofie",
					"Levensbeschouwing",
					"Economie",
					"Algemene economie",
					"Bedrijfseconomie",
					"Ondernemerschap - Academische vaardigheden",
					"Natuurkunde",
					"Scheikunde",
					"Biologie",
					"Rekenen",
					"NLT (Natuur, leven en techniek)",
					"Algemene Natuurwetenschappen",
					"NaSk",
					"Science",
					"Life sciences",
					"Informatica",
					"Techniek",
					"Technologie en toepassing",
					"Toepassingsgericht onderwijs",
					"Programmeren",
					"installeren en energie",
					"Produceren",
					"installeren",
					"energie (PIE)",
					"Art en Technologie",
					"Digital Creative Technology (DCT)",
					"Digital Technology",
					"Digitaal burgerschap",
					"STEAM",
					"T&T",
					"CKV",
					"Kunst & Techniek",
					"Kunst: algemeen",
					"Kunst: beeldend",
					"Kunst: beeldend / tekenen",
					"Kunst: dans",
					"Kunst: drama",
					"Kunst: podium",
					"Kunst: tekenen",
					"Kunst: theater",
					"Kunstgeschiedenis/CKV",
					"Handvaardigheid",
					"Muziek",
					"Vormgeving en media",
					"Lichamelijke opvoeding",
					"Lichamelijke opvoeding 2",
					"Bewegen sport en maatschappij (BSM)",
					"Dienstverlening en producten",
					"Praktijkgericht programma (havo)",
					"Praktijkprogramma zorg (mavo)",
					"Zorg en welzijn",
					"Mens en dienstverlening",
					"Mens en Maatschappij",
					"Mens en Natuur",
					"Onderzoeken & Ontwerp",
					"Onderzoekend leren",
					"Onderzoeksvaardigheden",
					"Millennium Skills",
					"Global perspectives",
					"Global citizenship",
					"Big History",
					"International Baccalaureate",
					"Psychologie",
					"Persoonlijke Vorming",
					"PGP maatschappij",
					"PGP P&O",
					"PGP technologie",
					"LOB/PWS",
					"PWS",
					"Zomer PWS",
					"Leefstijl",
					"Kennis van het geestelijk leven",
					"Vrede en Recht",
					"Wetenschap",
					"Wetenschapsoriëntatie",
					"Humaniora",
					"Arabisch",
					"Chinees",
					"Italiaans",
					"Russisch",
					"NT2",
					"Econasium",
					"E&O"
				]
			}
		},
		"motivation": {
			"title": "Motivation",
			"description": "Motivation letter of the candidate.",
			"examples": ["I am very motivated to start this program."],
			"type": "string"
		},
		"curriculumVitae": {
			"title": "Curriculum vitae",
			"description": "List of attachments representing the candidate's CV.",
			"examples": [
				[
					{
						"filename": "cv_jan_jansen.pdf",
						"url": "https://cdn.example.com/cv_jan_jansen.pdf"
					},
					{
						"filename": "cv_image_jan_jansen.png",
						"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
					}
				]
			],
			"type": "array",
			"items": {
				"title": "Attachment payload",
				"description": "Payload representing an attachment provided either by URL or base64-encoded data.",
				"anyOf": [
					{
						"type": "object",
						"properties": {
							"filename": {
								"title": "Attachment filename",
								"description": "The name of the file including its extension.",
								"examples": ["document.pdf", "image.png"],
								"type": "string",
								"minLength": 1
							},
							"url": {
								"title": "Attachment URL",
								"description": "Public URL where the file can be accessed.",
								"examples": ["https://cdn.example.com/file.png"],
								"type": "string",
								"format": "uri"
							}
						},
						"required": ["filename", "url"],
						"additionalProperties": false
					},
					{
						"type": "object",
						"properties": {
							"filename": {
								"title": "Attachment filename",
								"description": "The name of the file including its extension.",
								"examples": ["document.pdf", "image.png"],
								"type": "string",
								"minLength": 1
							},
							"data": {
								"title": "Base64 attachment data",
								"description": "Base64-encoded file contents including the data URI prefix.",
								"examples": ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."],
								"type": "string",
								"minLength": 1
							}
						},
						"required": ["filename", "data"],
						"additionalProperties": false
					}
				]
			}
		},
		"otherAttachments": {
			"title": "Other attachments",
			"description": "List of other attachments and documents provided by the candidate.",
			"examples": [
				[
					{
						"filename": "portfolio_jan_jansen.pdf",
						"url": "https://cdn.example.com/portfolio_jan_jansen.pdf"
					},
					{
						"filename": "avatar_jan_jansen.png",
						"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
					}
				]
			],
			"type": "array",
			"items": {
				"title": "Attachment payload",
				"description": "Payload representing an attachment provided either by URL or base64-encoded data.",
				"anyOf": [
					{
						"type": "object",
						"properties": {
							"filename": {
								"title": "Attachment filename",
								"description": "The name of the file including its extension.",
								"examples": ["document.pdf", "image.png"],
								"type": "string",
								"minLength": 1
							},
							"url": {
								"title": "Attachment URL",
								"description": "Public URL where the file can be accessed.",
								"examples": ["https://cdn.example.com/file.png"],
								"type": "string",
								"format": "uri"
							}
						},
						"required": ["filename", "url"],
						"additionalProperties": false
					},
					{
						"type": "object",
						"properties": {
							"filename": {
								"title": "Attachment filename",
								"description": "The name of the file including its extension.",
								"examples": ["document.pdf", "image.png"],
								"type": "string",
								"minLength": 1
							},
							"data": {
								"title": "Base64 attachment data",
								"description": "Base64-encoded file contents including the data URI prefix.",
								"examples": ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."],
								"type": "string",
								"minLength": 1
							}
						},
						"required": ["filename", "data"],
						"additionalProperties": false
					}
				]
			}
		},
		"interactions": {
			"title": "Interactions",
			"description": "List of interactions with the candidate.",
			"examples": [
				[
					{
						"type": "Telefonisch",
						"performedAt": "2024-01-01T10:00:00.000Z",
						"summary": "Initial intake call",
						"notes": "Candidate was enthusiastic",
						"duration": 1800
					},
					{
						"type": "E-mail",
						"performedAt": "2024-01-15T14:30:00.000Z"
					}
				]
			],
			"type": "array",
			"items": {
				"title": "Interaction",
				"description": "Schema for a single interaction with a candidate.",
				"type": "object",
				"properties": {
					"id": {
						"title": "Airtable record ID",
						"description": "Unique identifier of an Airtable record.",
						"examples": ["recABC123DEF456GH"],
						"type": "string",
						"pattern": "^rec[a-zA-Z0-9]{14}$"
					},
					"type": {
						"title": "Interaction type",
						"description": "Type of interaction associated with a candidate.",
						"examples": ["Telefonisch", "E-mail", "Inschrijving activiteit"],
						"type": "string",
						"enum": [
							"Telefonisch",
							"E-mail",
							"Inschrijving activiteit",
							"Persoonlijk",
							"Anders",
							"Doorverwijzing",
							"Adviesgesprek",
							"Kennismakingsgesprek",
							"Niet van toepassing"
						]
					},
					"performedAt": {
						"title": "Interaction date",
						"description": "Date and time when the interaction took place.",
						"examples": ["2024-01-01T10:00:00.000Z"],
						"type": "string",
						"format": "date-time",
						"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
					},
					"summary": {
						"title": "Interaction summary",
						"description": "Short summary of the interaction.",
						"examples": ["Initial intake call"],
						"type": "string"
					},
					"notes": {
						"title": "Interaction notes",
						"description": "Additional notes or details about the interaction.",
						"examples": ["Candidate was enthusiastic"],
						"type": "string"
					},
					"duration": {
						"title": "Interaction duration",
						"description": "Duration of the interaction in seconds.",
						"examples": [1800],
						"type": "integer",
						"exclusiveMinimum": 0,
						"maximum": 9007199254740991
					},
					"createdAt": {
						"title": "Interaction created at",
						"description": "Timestamp when the interaction record was created.",
						"examples": ["2024-01-01T10:00:00.000Z"],
						"type": "string",
						"format": "date-time",
						"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
					}
				},
				"required": ["id", "type", "performedAt", "createdAt"],
				"additionalProperties": false
			}
		},
		"customFields": {
			"title": "Candidate custom fields",
			"description": "Dynamically generated custom candidate fields derived from the Airtable base schema.",
			"type": "object",
			"properties": {
				"Example Custom Field": {
					"title": "Example Custom Field",
					"examples": ["example 1", "example 2"],
					"type": "array",
					"items": {
						"type": "string",
						"enum": ["example 1", "example 2"]
					}
				}
			},
			"additionalProperties": false
		}
	},
	"required": ["name", "email", "privacyConsent"],
	"additionalProperties": false
}

How to use the schema

The schema is primarily intended as a development and discovery tool. It allows you to:

  • Explore the complete candidate data model

  • Discover region-specific custom fields

  • Validate request payloads during development and integration

  • Generate forms or SDK validation logic if desired

While the schema can be used at runtime for dynamic payload construction or UI generation, this is optional.

Custom field validation behavior

Custom field validation is intentionally lenient by design to ensure that integrations remain stable even as region-specific schemas evolve.

Key characteristics:

  • Only known custom field properties are validated
    Fields that exist in the current region schema are validated against their defined type and constraints.

  • Unknown or outdated custom fields are ignored
    If your payload contains custom fields that are no longer defined (or not yet known), they are safely ignored and do not cause the request to fail.

  • Custom fields change infrequently
    The system is designed so that schema updates do not break existing integrations.

Disabling custom field validation

You can disable custom field validation entirely by setting the skipCustomFieldValidation query parameter.

When this flag is enabled:

  • No field-level validation is performed on customFields

  • The only check applied is whether customFields contains valid JSON

This can be useful in scenarios where:

  • You are migrating data

  • You are forwarding payloads from another system

  • You want to decouple schema updates from deployment timelines

For best results, retrieve and review the schema during development and update your integration when schema changes are introduced.