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
customFieldscontains 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.