Dit artikel beschrijft hoe je vanuit een extern systeem (CRM, ERP, webshop, of welk ander platform dan ook) data in Zendesk Custom Objects krijgt, én hoe je die data live up-to-date houdt.
We werken in dit artikel met een herkenbaar voorbeeld: een webshop met klanten, producten en bestellingen. Vervang dit gerust met wat in jouw situatie van toepassing is.
Wat ga je inrichten
In Zendesk gebruiken we Custom Objects als "single source of truth" voor data uit jouw eigen systeem. Een maatwerk zijbalk app in Zendesk laat een agent vervolgens zien wat er bekend is over de klant of de bestelling waar het ticket over gaat.
Het idee in vier lijnen:
- We definiëren in Zendesk een datamodel (welke objecten, welke velden, welke relaties).
- We maken dat datamodel aan via de Zendesk API (objecttypes, velden, lookup-relaties).
- Het externe systeem pusht records naar Zendesk via de API.
- Bij elke wijziging in het externe systeem stuurt dat systeem direct een update naar Zendesk, zodat de data live blijft.
Voorbereiding: API authenticatie
Elke call naar de Zendesk API gebruikt Basic Auth met een agent-e-mailadres en een API-token. Het token maak je aan in Zendesk via Admin Center > Apps and integrations > Zendesk API > Settings.
De Authorization-header bouw je als volgt op:
Authorization: Basic <base64({email}/token:{api_token})>In curl ziet dat er zo uit:
curl https://<subdomain>.zendesk.com/api/v2/custom_objects \ -u "agent@bedrijf.nl/token:abc123xyz"
Belangrijk: het account waarmee je authenticeert moet de rol Admin hebben om custom objects en velden te kunnen aanmaken.
Stap 1: bepaal je datamodel
Voor je iets aanmaakt, leg eerst vast wat je nodig hebt. Dit doe je samen met cloudmotion. Voor het webshop-voorbeeld krijg je bijvoorbeeld dit:
| Object | Standaardvelden | Custom velden | Relaties |
|---|---|---|---|
klant |
name, external_id | email, telefoonnummer, klantsinds | (geen) |
product |
name, external_id | sku, prijs, voorraad | (geen) |
bestelling |
name, external_id | status, besteldatum, totaalbedrag | klant (lookup), product (lookup) |
Een paar regels die altijd gelden:
-
Elk object krijgt automatisch twee standaardvelden:
nameenexternal_id. Die hoef je niet apart aan te maken. -
De
keyvan een object en van een veld is permanent. Je kunt die later niet meer wijzigen. Kies dus een logische, technische naam (kleine letters, underscores, geen spaties).
Stap 2: maak het custom object type aan
Voor elk object roep je deze endpoint aan:
POST /api/v2/custom_objectsVoorbeeld voor het object klant:
{
"custom_object": {
"key": "klant",
"title": "Klant",
"title_pluralized": "Klanten"
}
}cURL:
curl -X POST https://<subdomain>.zendesk.com/api/v2/custom_objects \
-u "agent@bedrijf.nl/token:abc123xyz" \
-H "Content-Type: application/json" \
-d '{
"custom_object": {
"key": "klant",
"title": "Klant",
"title_pluralized": "Klanten"
}
}'Doe ditzelfde voor product en bestelling. Pas op:
-
De
keymag later niet meer aangepast worden. Tikfouten betekenen object verwijderen en opnieuw aanmaken (alle records gaan dan ook verloren). - Wil je vooraf checken of een object al bestaat? Gebruik
GET /api/v2/custom_objects/{key}. Die geeft een 404 als het object er nog niet is.
Stap 3: voeg velden toe aan het object
Voor elk veld in jouw datamodel roep je deze endpoint aan:
POST /api/v2/custom_objects/{custom_object_key}/fieldsIn de body geef je een type mee. De volgende veldtypes zijn beschikbaar:
| Veldtype | Wanneer gebruiken |
|---|---|
text |
Korte tekst (standaard als je geen type opgeeft) |
textarea |
Lange tekst, meerdere regels |
checkbox |
Boolean (true/false) |
integer |
Heel getal |
decimal |
Getal met komma's |
currency |
Bedrag in een specifieke valuta |
date |
Datum (ISO 8601 formaat) |
dropdown |
Eén keuze uit een lijst |
multiselect |
Meerdere keuzes uit een lijst |
regexp |
Tekst met regex-validatie |
lookup |
Verwijzing naar een ander record (zie Stap 4) |
Een paar voorbeelden voor het object klant:
{
"custom_object_field": {
"key": "email",
"title": "E-mailadres",
"type": "text"
}
}{
"custom_object_field": {
"key": "klantsinds",
"title": "Klant sinds",
"type": "date"
}
}Voor een dropdown geef je ook de keuzeopties mee:
{
"custom_object_field": {
"key": "status",
"title": "Status",
"type": "dropdown",
"custom_field_options": [
{ "name": "Nieuw", "value": "nieuw" },
{ "name": "Verwerkt", "value": "verwerkt" },
{ "name": "Verzonden", "value": "verzonden" },
{ "name": "Geannuleerd", "value": "geannuleerd" }
]
}
}name is wat de agent ziet, value is de onderliggende tag waarmee je in triggers en automations werkt.
Stap 4: maak relaties tussen objects (lookup fields)
Een bestelling heeft een klant en bevat producten. Die relaties leg je vast met lookup fields. Dit is gewoon een veld op het ene object dat naar een ander object verwijst.
Voor een lookup field heb je naast type: "lookup" ook relationship_target_type nodig. Dat vertelt Zendesk naar welk objecttype dit veld verwijst:
relationship_target_type |
Verwijst naar |
|---|---|
zen:user |
Een Zendesk gebruiker |
zen:organization |
Een Zendesk organisatie |
zen:ticket |
Een ticket |
zen:custom_object:<key> |
Een record van een ander custom object |
Voorbeeld: een lookup van bestelling naar klant:
{
"custom_object_field": {
"key": "klant",
"title": "Klant",
"type": "lookup",
"relationship_target_type": "zen:custom_object:klant"
}
}Belangrijk: omdat dit veld verwijst naar een ander custom object, moet dat object eerst bestaan. Lookup fields kun je niet aanmaken voor een object dat er nog niet is. Hou daar in de volgorde rekening mee (zie verderop in dit artikel).
Stap 5: vul records vanuit het externe systeem
Nu het datamodel staat, kan het externe systeem records gaan pushen. Er zijn drie patronen, afhankelijk van wat je nodig hebt:
- Eén record aanmaken: simpele POST, gebruik dit zelden in een sync-context.
- Eén record upserten (aanmaken of bijwerken op external_id): de standaard voor live updates per event.
- Meerdere records tegelijk pushen (bulk jobs): voor de initiële vulling en voor batches groter dan 10 records.
Een nieuw record aanmaken
POST /api/v2/custom_objects/{custom_object_key}/recordsVoorbeeld:
{
"custom_object_record": {
"name": "Jan Jansen",
"external_id": "EXT-CRM-12345",
"custom_object_fields": {
"email": "jan@voorbeeld.nl",
"telefoonnummer": "+31612345678",
"klantsinds": "2023-04-15"
}
}
}Een record updaten op basis van external_id (upsert)
Dit is de endpoint die je in de praktijk het meest gaat gebruiken voor live syncs. Hij maakt een record aan als dat met de gegeven external_id nog niet bestaat, en update hem als hij wel bestaat.
PATCH /api/v2/custom_objects/{custom_object_key}/records?external_id={external_id}Voorbeeld: een wijziging in het CRM voor klant EXT-CRM-12345 triggert deze call:
curl -X PATCH "https://<subdomain>.zendesk.com/api/v2/custom_objects/klant/records?external_id=EXT-CRM-12345" \
-u "agent@bedrijf.nl/token:abc123xyz" \
-H "Content-Type: application/json" \
-d '{
"custom_object_record": {
"name": "Jan Jansen",
"custom_object_fields": {
"email": "jan.jansen@voorbeeld.nl",
"telefoonnummer": "+31687654321"
}
}
}'Alleen de velden die je meestuurt worden bijgewerkt. Velden die je niet meestuurt blijven staan zoals ze waren.
Dit is dé manier om Zendesk in sync te houden met het externe systeem bij losse wijzigingen: één PATCH-call per event, idempotent (een tweede keer dezelfde call doet geen kwaad).
Meerdere records tegelijk pushen (bulk jobs)
Voor de initiële vulling van een object, of voor batches groter dan ongeveer 10 records, gebruik je de bulk jobs endpoint. Hiermee push je tot 100 records per request, wat veel efficiënter is dan honderd losse PATCH-calls (en je houdt de rate limit beter binnen, zie verderop).
POST /api/v2/custom_objects/{custom_object_key}/jobsDe beschikbare actions zijn:
| Action | Wat doet het |
|---|---|
create |
Nieuwe records aanmaken |
update |
Bestaande records updaten op Zendesk-id |
create_or_update_by_external_id |
Upsert op external_id (aanbevolen voor sync) |
create_or_update_by_name |
Upsert op record name |
delete |
Records verwijderen op Zendesk-id |
delete_by_external_id |
Records verwijderen op external_id |
Een voorbeeld voor het bulk upserten van producten (let op het verschil met de enkele record-endpoint: het wrappende object heet hier job, en records gaan in een items-array):
{
"job": {
"action": "create_or_update_by_external_id",
"items": [
{
"external_id": "PROD-987654",
"name": "Voorbeeldproduct 500ml",
"custom_object_fields": {
"product_code": "PROD-987654",
"short_description": "Korte productomschrijving",
"unit_code": "ST",
"ean_code": "8712345678901"
}
},
{
"external_id": "PROD-987655",
"name": "Voorbeeldproduct 1L",
"custom_object_fields": {
"product_code": "PROD-987655",
"short_description": "Korte productomschrijving",
"unit_code": "ST",
"ean_code": "8712345678902"
}
}
]
}
}Het belangrijke verschil met de losse upsert: de respons is asynchroon. Je krijgt direct een job_status-object terug met een id. De daadwerkelijke verwerking gebeurt op de achtergrond.
Voorbeeld van wat je terugkrijgt:
{
"job_status": {
"id": "8b726f2eaf32a4f6e3...",
"url": "https://<subdomain>.zendesk.com/api/v2/job_statuses/8b726f2eaf32a4f6e3...",
"status": "queued",
"total": 2,
"progress": null,
"message": null,
"results": null
}
}Om de voortgang te volgen, poll je periodiek de Job Statuses endpoint:
GET /api/v2/job_statuses/{id}De status doorloopt queued → working → completed (of failed). Pas wanneer de status completed is, zijn de records daadwerkelijk doorgevoerd.
Een paar belangrijke beperkingen om in je integratie mee te nemen:
- Maximaal 100 records per job. Heb je er meer? Splits ze op in batches van 100.
- Maximaal 30 jobs tegelijk in queue of running per account. Probeer je een 31e te starten, dan krijg je een
TooManyJobs-fout. Houd hier rekening mee bij parallelle batches: poll de status van eerdere jobs voor je nieuwe queued. - Bij
create_or_update_by_external_idmoet elk item eenexternal_idbevatten, anders faalt dat item binnen de job.
Voor de losse, event-driven sync blijft de enkele PATCH-upsert eenvoudiger. Gebruik bulk jobs voor: initiële vulling, periodieke volledige syncs, en bursts van wijzigingen die anders je rate limit zouden raken.
Het belang van external_id
Vul external_id altijd. Echt altijd. Dit is de identifier die jouw externe systeem en Zendesk aan elkaar koppelt.
Waarom is dit zo belangrijk?
- Zonder external_id kun je een record niet terugvinden vanuit het externe systeem. Je weet wel dat hij in Zendesk staat, maar je hebt geen manier om met zekerheid te zeggen welk Zendesk-record bij welke entry in jouw systeem hoort.
- Updaten gaat altijd via external_id. De upsert-endpoints hierboven werken alleen als external_id gevuld is. Zonder external_id moet je het Zendesk-record-id opzoeken via een aparte zoekopdracht, en die op jouw kant opslaan. Dat is fragiel en foutgevoelig.
- Het voorkomt duplicates. Als je een record opnieuw probeert te pushen zonder external_id, krijg je gewoon een nieuw record. Met external_id wordt het bestaande record bijgewerkt.
Wat gebruik je als external_id? De unieke identifier uit het bronsysteem:
- Salesforce Account ID, bijvoorbeeld
0015g00000abc123 - SAP Customer Number, bijvoorbeeld
100045 - Webshop Order ID, bijvoorbeeld
ORDER-2026-04567
Wat je kiest maakt niet uit, zolang het maar uniek is per record en niet verandert over de tijd.
Lookup fields vullen met external_id
Stel: er komt een bestelling binnen vanuit de webshop. In het externe systeem heeft die bestelling een klant-ID (EXT-CRM-12345) en een product-ID (PROD-998). In Zendesk heeft elk van die records óók een eigen Zendesk-id (een lang nummer of ULID), maar dat hoef je niet op te zoeken.
Je kunt het lookup-veld direct vullen met de external_id van het gerelateerde record, door de waarde te prefixen met external_id::
{
"custom_object_record": {
"name": "Bestelling 2026-04567",
"external_id": "ORDER-2026-04567",
"custom_object_fields": {
"status": "verwerkt",
"besteldatum": "2026-05-15",
"totaalbedrag": 149.95,
"klant": "external_id:EXT-CRM-12345",
"product": "external_id:PROD-998"
}
}
}Zendesk zoekt zelf het bijbehorende record op en bewaart het juiste interne id. Dit scheelt je een extra API-call om eerst het Zendesk-id op te halen.
Wil je toch het Zendesk-id direct meegeven, dan kan dat ook gewoon:
"klant": "01HZX5QK9F8VRYAB2NCEXAMPLE"
Maar in 99% van de praktijksituaties is external_id: de juiste en simpelste route.
Volgorde van pushen
Lookup fields creëren een afhankelijkheid: een bestelling kan pas naar een klant verwijzen als die klant er al is. Hou daarom deze volgorde aan bij het vullen:
- Records zonder afhankelijkheden eerst (
klant,product) - Records die naar andere objecten verwijzen daarna (
bestelling)
Als je een bestelling pusht voordat de bijbehorende klant bestaat, faalt de call met een fout dat het gerefereerde record niet gevonden is.
In de praktijk betekent dit dat je in het externe systeem deze volgorde aanhoudt bij de initiële vulling, en dat je bij wijzigingen ervoor zorgt dat parent-records eerder gesynchroniseerd worden dan child-records.
Live updates: hoe blijft Zendesk in sync
Voor live updates zijn er twee patronen die werken:
Patroon 1: event-driven (aanbevolen). Bij elke wijziging in het externe systeem (create, update, delete) wordt direct een API-call richting Zendesk gedaan. Dit gaat het snelst en is het meest betrouwbaar. Dit is hoe het externe systeem normaal gesproken zelf de push verzorgt, vaak via webhooks of een eigen event-bus.
Patroon 2: periodieke sync. Elke X minuten/uren draait er een job die alle gewijzigde records sinds de vorige run pusht naar Zendesk. Eenvoudiger in te richten, maar data is niet realtime in Zendesk beschikbaar. Gebruik hier bij voorkeur de bulk jobs endpoint voor.
In beide patronen gebruik je de upsert-endpoint (PATCH .../records?external_id=... of bulk create_or_update_by_external_id). Idempotent, dus een retry doet geen kwaad.
Records ophalen
Soms wil je vanuit Zendesk of vanuit een sidebar app data uit custom objects ophalen.
Eén specifiek record op Zendesk-id
GET /api/v2/custom_objects/{custom_object_key}/records/{record_id}Records zoeken op external_id
Er is geen directe GET by external_id-endpoint, maar je kunt zoeken:
POST /api/v2/custom_objects/{custom_object_key}/records/searchBody:
{
"filter": {
"external_id": { "$eq": "EXT-CRM-12345" }
}
}Alle records van een object
GET /api/v2/custom_objects/{custom_object_key}/recordsMet pagination, want grote datasets worden in pagina's geleverd.
Records verwijderen
Als een record in het externe systeem verwijderd wordt, verwijder hem dan ook in Zendesk:
DELETE /api/v2/custom_objects/{custom_object_key}/records?external_id={external_id}Dit kan ook op Zendesk-id:
DELETE /api/v2/custom_objects/{custom_object_key}/records/{record_id}Let op: als andere records via een lookup naar dit record verwijzen, kan dat tot lege verwijzingen leiden. Hou daar rekening mee in je sync-strategie.
Een compleet voorbeeld: end-to-end
Stel: er komt een nieuwe bestelling binnen in de webshop. De webshop stuurt deze events naar Zendesk:
1. Klant aanmaken of bijwerken
PATCH /api/v2/custom_objects/klant/records?external_id=EXT-CRM-12345{
"custom_object_record": {
"name": "Jan Jansen",
"custom_object_fields": {
"email": "jan@voorbeeld.nl",
"telefoonnummer": "+31612345678",
"klantsinds": "2023-04-15"
}
}
}2. Product aanmaken of bijwerken
PATCH /api/v2/custom_objects/product/records?external_id=PROD-998{
"custom_object_record": {
"name": "Tuinstoel deluxe",
"custom_object_fields": {
"sku": "TS-DLX-998",
"prijs": 149.95,
"voorraad": 23
}
}
}3. Bestelling aanmaken of bijwerken
PATCH /api/v2/custom_objects/bestelling/records?external_id=ORDER-2026-04567{
"custom_object_record": {
"name": "Bestelling 2026-04567",
"custom_object_fields": {
"status": "verwerkt",
"besteldatum": "2026-05-15",
"totaalbedrag": 149.95,
"klant": "external_id:EXT-CRM-12345",
"product": "external_id:PROD-998"
}
}
}Als de status van de bestelling later verandert, stuurt de webshop simpelweg dezelfde call opnieuw met de nieuwe status:
{
"custom_object_record": {
"custom_object_fields": {
"status": "verzonden"
}
}
}Alleen het veld status wordt bijgewerkt. De overige velden blijven onveranderd.
Rate limits en hoe je 429-fouten voorkomt
Zendesk hanteert rate limits op alle API-endpoints. Stuur je te veel requests in een korte tijd, dan krijg je een 429 Too Many Requests-fout terug. Voor een sync die continu draait, is hier een goede strategie voor nodig.
Limieten per plan
De rate limits voor de Support en Custom Objects API zijn afhankelijk van het Zendesk-plan:
| Plan | Requests per minuut |
|---|---|
| Suite Team | 200 |
| Suite Growth | 400 |
| Suite Professional | 400 |
| Suite Enterprise | 700 |
| Suite Enterprise Plus | 2.500 |
Met de High Volume API add-on kun je deze limiet op een lager plan verhogen naar 2.500 requests per minuut.
De Retry-After header
Bij een 429-response stuurt Zendesk een Retry-After-header mee. Die geeft aan hoeveel seconden je moet wachten voor je weer een call mag doen. Voorbeeld:
HTTP/1.1 429 Too Many Requests
Retry-After: 93Wacht dan letterlijk 93 seconden voor je opnieuw probeert. Negeer deze header niet: blijf je requests sturen na een 429, dan loop je het risico op verdere blokkades en lege responses.
Headers die je proactief kunt uitlezen
Op elke response geeft Zendesk informatie mee over je huidige gebruik. Lees deze uit om vóór je de limiet raakt al af te remmen:
x-rate-limit: 700
x-rate-limit-remaining: 235
ratelimit-limit: 700
ratelimit-remaining: 235
ratelimit-reset: 41ratelimit-remaining vertelt je hoeveel calls je nog over hebt voor de huidige minuut. ratelimit-reset geeft het aantal seconden tot de teller weer op nul gaat.
Aanbevolen aanpak voor jouw sync
-
Vang 429 op en respecteer Retry-After. In code:
if response.status == 429: wait = int(response.headers["Retry-After"]) sleep(wait) retry() - Implementeer exponential backoff. Bij herhaalde 429-fouten: wacht steeds langer tussen retries (bijv. 1s, 2s, 4s, 8s, 16s). Stop na een redelijk aantal pogingen en log de fout.
- Gebruik bulk jobs voor grote batches. 100 records in één bulk-call is veel zuiniger op je rate limit dan 100 losse PATCH-calls. Voor initiële vullingen en periodieke syncs altijd bulk jobs gebruiken.
- Spreid de load over de tijd. Stuur niet alles in één keer maar voeg een kleine pauze toe tussen requests (bijvoorbeeld 50-100ms) als je in een tight loop pusht.
-
Hou de bulk-job-limiet in de gaten. Maximaal 30 jobs gelijktijdig queued of running. Wacht tot eerdere jobs
completedzijn voor je nieuwe submit, anders krijg je eenTooManyJobs-fout.
Tip: in Zendesk Admin Center kun je het actuele API-gebruik en het aantal 429-fouten monitoren via Account > Usage > API usage. Daar zie je ook welke endpoints en welke callers het meest hitten.
Referentie
Volledige documentatie van Zendesk:
- Rate limits - Zendesk Developer Docs
- Best practices for avoiding rate limiting
- Managing API usage in your Zendesk account
Veelvoorkomende fouten
422 Unprocessable Entity bij het aanmaken van een lookup field
Het object waar de lookup naar verwijst bestaat nog niet. Maak eerst het doelobject aan.
404 Not Found bij het pushen van een record met external_id: lookup
Het gerefereerde record met die external_id bestaat nog niet. Hou de pushvolgorde aan (parent eerst, dan child).
Records worden steeds opnieuw aangemaakt in plaats van bijgewerkt
Je gebruikt waarschijnlijk POST in plaats van PATCH, of je vergeet de external_id in de query parameter mee te geven bij de PATCH.
Een veld is niet bij te werken
Sommige veldproperties (zoals key, of de currency code van een currency field) zijn na aanmaak immutable. Je moet het veld dan verwijderen en opnieuw aanmaken.
TooManyJobs bij een bulk job
Je hebt al 30 jobs queued of running. Wacht tot eerdere jobs completed zijn voor je een nieuwe submit.
Wat doet cloudmotion en wat doe jij
Om dit werkend te krijgen verdelen we het werk zo:
cloudmotion zorgt voor:
- Het datamodel definiëren samen met jou
- Custom object types, velden en lookup-relaties aanmaken in Zendesk
- De sidebar app bouwen die de data toont in het ticket
- Het aanleveren van API-documentatie en de exacte object-keys en field-keys
Jij of jouw integrator zorgt voor:
- API-token aanmaken
- De koppeling bouwen vanuit het externe systeem naar de Zendesk API
- Triggeren van een API-call bij elke create/update/delete in het externe systeem
- external_id altijd gevuld doorgeven, zodat updates correct werken
- Pushvolgorde respecteren (parents voor children)
- Rate limits afvangen (429, Retry-After, exponential backoff)
Referenties
Officiële Zendesk-documentatie:
- Custom Objects (API) - Zendesk Developer Docs
- Custom Object Fields (API)
- Custom Object Records (API) (inclusief sectie Custom Object Record Bulk Jobs)
- Job Statuses (voor het pollen van bulk jobs)
- Lookup Relationships (API)
- Searching and filtering custom object records
- Creating custom objects (handboek)
- Rate limits
- Best practices for avoiding rate limiting
Vragen of loop je vast bij de implementatie? Neem contact op via support@cloudmotion.nl.