Klare Koek ↔ Jobfeed — integratiehandleiding
Laatst bijgewerkt: Versie 1.9.1 — 1 september 2026
Deze handleiding beschrijft de huidige, live situatie. De versiehistorie staat als bijlage achterin.
In één zin
Jullie halen onze vacatures op via een REST-API; bij elke wijziging sturen wij een webhook naar het endpoint dat jullie daarvoor instellen, zodat jullie niet hoeven te pollen; en jullie voegen zelf nieuwe klanten bij ons toe.
Eén set endpoints voor alle omgevingen
Dit is het belangrijkste ontwerpprincipe van de integratie:
- Elke omgeving gebruikt exact dezelfde endpoints, met dezelfde URL's en dezelfde query-syntax. Er bestaat geen apart endpoint voor productie, staging of dev.
- Alleen de API-key verschilt per omgeving. De key bepaalt server-side welke omgeving je ziet en beheert; dat is door de client niet te omzeilen. Jullie prod kan dus nooit staging/dev-vacatures zien, en andersom.
- Gevolg: wat jullie op dev of staging testen is byte-voor-byte hetzelfde codepad als productie. Alleen de key wisselt.
Alle endpoints staan op dezelfde base-URL:
https://yxbnxdoirwwgwiugpunj.functions.supabase.co
| Endpoint | Methode | Doel |
|---|---|---|
/get-vacancies |
GET | Vacatures ophalen (§4) |
/add-source |
POST | Nieuwe klant (jobfeed) toevoegen (§5) |
/manage-source/<source_id> |
PATCH / DELETE | Klant wijzigen of uitzetten (§5b) |
/preview |
GET | Aantal vacatures in een feed checken vóór onboarding (§6) |
Daarnaast sturen wíj webhooks naar jullie endpoints (§3).
1. Wat je van ons krijgt
| Type | Wat | Wanneer |
|---|---|---|
| Webhook | new event |
Nieuwe vacature in onze feed |
| Webhook | update event |
Bestaande vacature is gewijzigd |
| Webhook | close event |
Vacature is gesloten (verdween uit de bronfeed) |
| Webhook | source_down event |
Bronfeed van een klant is onbereikbaar |
| REST GET | Vacature-details | Op basis van het ID uit een webhook |
| REST POST | Nieuwe klant toevoegen | Wanneer jullie een nieuwe klant onboarden |
| REST PATCH | Klant wijzigen | Feed-URL, webhook-URL, naam, land, … aanpassen (§5b) |
| REST DELETE | Klant uitzetten | Soft-delete; vacatures sluiten de eerstvolgende nacht (§5b) |
| REST GET | Preview-aantal | Tijdens setup om aantal vacatures te tonen |
2. Sleutels en omgevingen
Er zijn twee soorten sleutels, beide per omgeving:
| Sleutel | Waarvoor | Hoe gebruikt |
|---|---|---|
| API-key, één per omgeving | Authenticeert Klare Koek richting ons | Authorization: Bearer <key> op alle vier de endpoints hierboven. Zeven keys met jullie labels: 360-prod, 360-staging, 360-dev, dev-jelle, dev-mark, dev-remco, dev-jouke. Het label bepaalt de omgeving: een key maakt bronnen aan in, beheert en leest alléén de eigen omgeving. |
| Webhook-secret, één per omgeving | Authenticeert onze webhooks richting jullie | Bearer-token-check op de Authorization-header (§3.4). Een event draagt het secret van de omgeving waar de bijbehorende bron in staat. |
De sleutels delen we uitsluitend via een secure kanaal (1Password-links) — nooit via mail of chat. Belandt een sleutel toch ooit in een onveilig kanaal, dan vervangen wij dat paar uit voorzorg en delen we het nieuwe via 1Password; de overige omgevingen blijven dan ongemoeid.
Overgang: de oude gedeelde
KK_API_KEYwerkt nog en telt als de360-prod-key, zodat er geen moment is waarop bestaande bronnen onbeheerbaar zijn. Zodra jullie bevestigen dat alle omgevingen via/get-vacancieslezen (§8), trekken wij die oude key in.
SUPABASE_ANON_KEYis niet meer nodig — en per 1 september ingetrokken. De anon-key die jullie in juni kregen werkt niet meer; jullie kunnen hem uit jullie configuratie verwijderen. Vacatures ophalen gaat via/get-vacanciesmet je gewone API-key (§4). De publieke database-views waar de anon-key toegang toe gaf zijn er alleen nog voor onze eigen website en tonen uitsluitend productie-data; voor de integratie spelen ze geen rol.
3. Webhooks die wij naar jullie sturen
3.1 Aanleveren door Klare Koek — per jobfeed een eigen endpoint
Elke jobfeed (bron) heeft een eigen, optionele webhook_url. Zo kan
elke developer/omgeving (lokaal, dev, staging, prod) z'n eigen endpoint
ontvangen. Je geeft hem op bij het aanmaken (POST /add-source, §5) en
wijzigt hem wanneer je wilt (PATCH /manage-source/<id>, §5b):
{ "webhook_url": "https://staging.klarekoek.nl/jobfeed/webhook" }
Regels:
- https verplicht en de host moet publiek bereikbaar zijn.
webhook_urlopnullzetten = terug naar het globale fallback-endpoint (het ene adres dat jullie ons voor productie leveren; bronnen zonder eigenwebhook_urlgebruiken dat).- Een wijziging geldt per direct, óók voor events die al klaarstonden maar nog niet geleverd zijn (endpoint wordt op verzend-moment bepaald).
- Het bearer-secret (§3.4) is per omgeving: een event krijgt het secret van de omgeving waar z'n bron in staat. Gevolg: als twee omgevingen hetzelfde endpoint gebruiken, krijgen ze wél twee aparte POSTs (elk met hun eigen token) in plaats van één gebundelde. Er gaat nooit een event verloren.
We sturen alle vier event-types naar het endpoint van de betreffende
bron. Type staat in de body. Webhooks gaan gebatched — één POST per
endpoint per event-type per pipeline-run, niet per individuele vacature.
Uitzondering: source_down wordt per getroffen bron als aparte POST
geleverd (de sources-array bevat dan één bron) — vallen er meerdere
bronnen uit in één run, dan krijg je dus meerdere source_down-POSTs.
Elke body bevat ook een idempotency_key waarmee je dubbele leveringen
kunt herkennen (zie §3.5).
3.2 Frequentie
Onze pipeline draait elke dag om 01:00 UTC. Direct daarna sturen wij
alle events van die run: per endpoint één POST per event-type dat
voorkwam, plus per getroffen bron een aparte POST voor source_down
(§3.1). Zijn jullie tijdelijk onbereikbaar, dan parkeren we de events
en proberen we de volgende dag opnieuw. Max 10 pogingen per event
(zie §3.6).
3.3 Payload-voorbeelden
new event — nieuwe vacatures:
{
"event": "new",
"occurred_at": "2026-05-29T01:15:00Z",
"idempotency_key": "9f2c0b1e…(sha256-hex)",
"vacancy_ids": [
"5ce220c0-a9ad-4de7-871c-3630e78f3b5c",
"c228f56b-01c9-4260-b1c6-caefdceec2f5"
],
"vacancies": [
{ "id": "5ce220c0-a9ad-4de7-871c-3630e78f3b5c",
"source_id": "ab50c37a-23a4-46de-b823-7c924cf3f062" },
{ "id": "c228f56b-01c9-4260-b1c6-caefdceec2f5",
"source_id": "29c01d3b-5715-494d-b95b-49fc7c9473dc" }
]
}
update event — gewijzigde vacatures:
{
"event": "update",
"occurred_at": "2026-05-29T01:15:00Z",
"idempotency_key": "a1b2c3d4…(sha256-hex)",
"vacancy_ids": ["a689f51b-c778-4cc8-be77-9b158f6242bd"],
"vacancies": [
{ "id": "a689f51b-c778-4cc8-be77-9b158f6242bd",
"source_id": "ab50c37a-23a4-46de-b823-7c924cf3f062" }
]
}
close event — gesloten vacatures (verdwenen uit bron):
{
"event": "close",
"occurred_at": "2026-05-29T01:15:00Z",
"idempotency_key": "e5f6a7b8…(sha256-hex)",
"vacancy_ids": ["13fa266f-6668-4482-919d-0b8206b8e6be"],
"vacancies": [
{ "id": "13fa266f-6668-4482-919d-0b8206b8e6be",
"source_id": "29c01d3b-5715-494d-b95b-49fc7c9473dc" }
]
}
vacanciesnaastvacancy_ids. Elkenew/update/close-body bevat beide:vacancy_ids(alleen de ID's) en eenvacancies-array met per vacature ook hetsource_idvan z'n jobfeed (dezelfde UUID als in de add-source-response, §5). De arrays zijn index-uitgelijnd:vacancies[i].id == vacancy_ids[i], altijd. Een batch op het globale fallback-endpoint kan vacatures van meerdere bronnen bevatten; gebruik dus hetsource_idper entry, niet één waarde voor de hele batch. Een eigen source-id in de per-bron webhook-URL verwerken (jullie eerdere workaround) mag ook nog steeds, maar is hiermee niet meer nodig.⚠️ Een vacature die
closeheeft gehad blijft bij ons in de database staan metactive=false. Je kunt de details nog steeds ophalen via/get-vacancies(zie §4). Als de vacature later weer in de bron verschijnt, krijg je opnieuw eennewevent met hetzelfde ID.
source_down event — bronfeed van een klant kapot:
{
"event": "source_down",
"occurred_at": "2026-05-29T01:15:00Z",
"idempotency_key": "c9d0e1f2…(sha256-hex)",
"sources": [
{
"source_id": "29c01d3b-5715-494d-b95b-49fc7c9473dc",
"client_name": "Some Pharma BV",
"url": "https://careers.somepharma.com/feed.xml",
"old_status": "healthy",
"new_status": "broken",
"message": "HTTP 503 bij ophalen URL",
"locked": false
}
]
}
3.4 Authenticatie — bearer-token verifiëren
Elke POST draagt een vaste header:
Authorization: Bearer <webhook-secret van de omgeving>
Vergelijk die header met het secret en weiger met 401 als hij niet klopt.
Gebruik een constant-time vergelijking om timing-aanvallen te voorkomen. Er is
geen raw-body, hash of timestamp nodig — een eenvoudige bearer-token dus,
geen HMAC.
Welk secret een POST draagt, hangt af van de omgeving van de bron waar het event bij hoort. Een endpoint dat events uit meerdere omgevingen ontvangt, moet dus meerdere secrets accepteren — controleer de header tegen de set secrets die bij dat endpoint hoort. Elke POST bevat events uit precies één omgeving.
Verificatie in Node.js:
const crypto = require("crypto");
function verifyJobfeedAuth(authHeader, secret) {
const a = Buffer.from(authHeader || "");
const b = Buffer.from(`Bearer ${secret}`);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: if (!verifyJobfeedAuth(req.get("authorization"), SECRET)) return res.sendStatus(401);
Verificatie in Python:
import secrets
def verify_jobfeed_auth(auth_header: str, secret: str) -> bool:
return secrets.compare_digest(auth_header or "", f"Bearer {secret}")
De check kijkt alleen naar de header, niet naar de body. Je mag de JSON dus gewoon door je normale parser laten afhandelen. Verifieer de token vóór je de body verwerkt.
3.5 Idempotency — dubbele leveringen herkennen
Elke POST draagt een idempotency-key: dezelfde waarde staat in de body
(idempotency_key) én in een header:
X-Idempotency-Key: 9f2c0b1e…(sha256-hex)
De key is deterministisch — een sha256 over het event-type plus de gesorteerde
ID's. Een re-send van exact dezelfde batch levert dus exact dezelfde key op
(alleen de occurred_at-timestamp in de body verschilt per verzending).
De vacancies-array telt niet mee in de key: die wordt alleen uit
event-type + ID's afgeleid.
Batches worden per endpoint samengesteld (§3.1) met disjuncte ID-sets, dus
keys zijn ook over meerdere endpoints heen uniek; dedup per omgeving op
alleen de key blijft correct.
In zeldzame gevallen kunnen wij een batch twee keer sturen — bijvoorbeeld een
time-out waarbij onze kant de levering niet als geslaagd registreerde. Dedup
hierop: heb je deze X-Idempotency-Key al verwerkt, antwoord dan 200 en
negeer de inhoud. Zo voorkom je dubbele vacatures aan jullie kant.
Bewaar verwerkte keys daarbij een beperkte tijd — een paar weken is ruim voldoende, want een re-send komt binnen dagen (§3.6). De key bevat bewust geen datum: een legitieme latere levering die toevallig exact dezelfde ID-set heeft (bijvoorbeeld een heropende vacature, §3.3) krijgt dezelfde key en moet niet op een maandenoude dedup-entry stranden.
3.6 Response
Stuur HTTP 200–299 bij succes — geef ook een 2xx bij een al-geziene
X-Idempotency-Key (zie §3.5). Bij 400+ markeren wij de batch als gefaald en
proberen we het de volgende run opnieuw. Binnen één leverronde proberen we
tot 4× met korte tussenpozen (met dezelfde X-Idempotency-Key); zo'n ronde
telt als één van de max 10 pogingen (§3.2). Wij volgen geen redirects:
jullie endpoint moet terminal zijn — een 3xx behandelen wij als
leveringsfout. Na 10 mislukte pogingen zetten wij het event stil; wij
krijgen daarvan een interne alert en nemen contact op.
4. Vacature-data ophalen (REST)
Vacatures haal je op via /get-vacancies, met de API-key van de
omgeving waarvoor je leest (§2). Zoals overal geldt: zelfde endpoint
voor alle omgevingen, de key bepaalt wat je ziet.
GET https://yxbnxdoirwwgwiugpunj.functions.supabase.co/get-vacancies
Headers: Authorization: Bearer <API-key van jullie omgeving>
De query-syntax is PostgREST:
# Eén vacature op basis van een webhook-ID
curl "https://yxbnxdoirwwgwiugpunj.functions.supabase.co/get-vacancies?id=eq.<UUID>" \
-H "Authorization: Bearer <API-key>"
# Alleen actieve vacatures, compacte payload
curl "https://yxbnxdoirwwgwiugpunj.functions.supabase.co/get-vacancies?active=eq.true&select=id,title,source_id" \
-H "Authorization: Bearer <API-key>"
Spelregels:
- Standaard krijg je álle pharma-relevante vacatures van je omgeving,
inclusief gesloten (mét
closed_at). Wil je alleen actieve, voegactive=eq.truetoe. select=,order=,limit=/offset=en alle kolomfilters (eq./gte./in./…) werken zoals bij PostgREST. Max 1000 rijen per response — pagineer metlimit/offset+ een vasteorder=id. StuurPrefer: count=exactmee om het totaal in deContent-Range-header te krijgen.- Een
?environment=…-parameter wordt geweigerd met400(de omgeving ligt vast in je key), net alsselect=met resource-embedding (haakjes) en de parametersapikey,on_conflictencolumns. Verder gedraagt het endpoint zich in de query-syntax als PostgREST; van de request-headers gaat alleenPrefer: count=…mee — pagineren doe je dus metlimit/offset, niet met eenRange-header. - Een id uit een andere omgeving opvragen geeft géén 404 maar een
lege array
[]— PostgREST-semantiek: de rij valt buiten je scope. Behandel[]dus niet als fout. - Het leespad is strikt alleen-lezen en levert uitsluitend de gepubliceerde feed-kolommen; schrijven kan niet en onze ruwe tabellen zijn via dit endpoint niet bereikbaar.
- Zelfde statuscodes als de andere endpoints:
401(key),429(max 20 requests/min — een volledige sync past ruim binnen 1-2 requests),400(ongeldige query),405(iets anders dan GET) en500(interne fout).
Belangrijkste velden
| Veld | Type | Toelichting |
|---|---|---|
id |
uuid | 5ce220c0-... |
source_id |
uuid | Het id van de jobfeed (bron) waar de vacature bij hoort — dezelfde UUID als in de add-source-response (§5), /manage-source/<source_id> (§5b) en het source_down-event (§3.3). Gebruik dit om vacatures aan de juiste org/user te koppelen; company_name is presentatietekst, geen sleutel. |
external_url |
text | URL naar oorspronkelijke vacaturepagina |
title |
text | Engelstalig (uniform), origineel via description_markdown |
description_markdown |
text | Hoofdtekst in Markdown (originele taal) |
description_translated |
text | Engelse vertaling van de samenvatting |
company_name |
text | Bedrijfsnaam van de klant |
location_city, location_country |
text | Stad, land |
salary_min, salary_max, currency |
int / text | Salaris als bekend |
category, function_area, function_areas |
text / text[] | Categorie + functiegebieden |
hours_type, seniority_level, type_of_employment, education_level |
text | Job-metadata |
seniority_level_id, education_level_id, type_of_employment_id, category_id, function_area_id |
text | Het stabiele filter-id (ULID) uit jullie filters-API, horend bij het gelijknamige naamveld (seniority_level_id bij seniority_level, category_id bij category, enz.). Nachtelijk bijgewerkt. null betekent: die waarde kennen jullie (nog) niet als filter, of jullie gebruiken dezelfde naam voor twee filters (dan kunnen wij niet kiezen). |
function_area_ids |
text[] | De filter-id's van álle function_areas, index-uitgelijnd: function_area_ids[i] hoort altijd bij function_areas[i]. Een niet-gekoppelde waarde levert een null-element, géén kortere lijst — zo kun je veilig per index zippen. |
experience, education, type_of_contract (+ _id) |
text | Jullie eigen sleutelnamen, met dezelfde waarden als respectievelijk seniority_level, education_level en type_of_employment. type_of_contract is een alias van type_of_employment (bevestiging gevraagd in §8). |
info_about_next_job, info_about_next_colleagues, info_about_tasks_and_responsibilities, info_about_skills_experience, info_about_benefits, info_about_molecules_meet_opportunities |
text | Jullie sleutelnamen voor de secties, met dezelfde inhoud als de about_your_*-velden hieronder. Beide sets staan in de feed; kies er één (select= gebruiken scheelt payload). Voor only_apply_if (harde eisen) bestaat geen eigen 360-sleutelnaam; die staat alleen onder de bestaande sleutel. |
about_your_next_job, about_your_next_colleagues, about_your_tasks_and_responsibilities, about_your_skills_and_experience, about_your_benefits, where_molecules_meet_opportunities, only_apply_if |
text | Gestructureerde secties in Markdown: --bullets en spaarzaam **bold**, geen headings/links/HTML. Oudere rijen zijn platte tekst en daarmee óók geldige Markdown. Render ze door dezelfde Markdown-renderer (+ sanitizer) als description_markdown. |
environment |
text | Omgevings-label van de bron waar de vacature bij hoort (§2). Via /get-vacancies is dit per definitie (een alias van) de omgeving van je key — puur informatief dus. |
active |
bool | false = vacature is gesloten |
closed_at |
timestamptz | Wanneer dichtgegaan (alleen als active=false) |
posted_at |
timestamptz | Plaatsingsdatum uit de bronfeed (RSS <pubDate>, Atom <published>, custom <date>). NULL als de bron geen datum levert (typisch bij sitemaps) — gebruik dan created_at als fallback. Dit is de datum die jullie op een vacaturepagina als "geplaatst op" willen tonen. |
created_at |
timestamptz | Wanneer wij de rij voor het eerst aanmaakten in onze DB. Fallback voor posted_at |
updated_at |
timestamptz | Wanneer wij de rij voor het laatst hebben bijgewerkt |
last_seen_at |
timestamptz | Wanneer de URL voor het laatst in de bronfeed werd gezien. Voor actieve rijen typisch "vandaag"; voor gesloten rijen de laatste datum |
Lege velden komen als
nullof een lege string. Op jullie kant tonen jullie ze niet — wij garanderen alleen wat de bron aanlevert.De feed bevat daarnaast een paar velden voor onze eigen site (
science_parks,science_park_names); die mag je negeren.Voor een volledige veld-lijst:
GET .../get-vacancies?limit=1— dat is de bron van waarheid.
Waardelijsten — wij volgen die van jullie
Onze AI mag alléén waarden toekennen die op dat moment in jullie filters-API staan. Concreet:
- Wij lezen elke nacht
https://get360pharma.com/api/v1/vacancy-filtersen gebruikenexperiences,educations,types_of_employment,functions(de groepen → onzecategory) en de genesteitems(→ onzefunction_areas). - Voegen jullie een filter toe, dan kennen wij het de volgende nacht toe. Er hoeft bij ons niets aangepast of uitgerold te worden.
- Hernoemen jullie een filter, dan volgt de naam automatisch en blijft het id gelijk; bestaande vacatures houden hun koppeling.
- Verdwijnt een filter, dan stoppen wij het toe te kennen, maar behouden bestaande vacatures hun waarde en id. Wij krijgen daar een melding van.
hours_type(Voltijd/Deeltijd) heeft geen tegenhanger in jullie filters-API en houdt daarom geen id.
5. Nieuwe klant toevoegen (REST)
POST https://yxbnxdoirwwgwiugpunj.functions.supabase.co/add-source
Headers:
Authorization: Bearer <API-key van jullie omgeving>
Content-Type: application/json
Body:
{
"client_name": "Some Pharma BV",
"url": "https://careers.somepharma.com/feed.xml",
"country": "NL", // optioneel, default NL (volledige namen worden naar ISO genormaliseerd)
"source_type": "xml_feed", // optioneel, auto-detected
"min_description_chars": 50, // optioneel, per-klant tunable
"webhook_url": "https://dev.klarekoek.nl/jobfeed/webhook" // optioneel, per-bron endpoint (§3.1)
}
Mogelijke responses:
201 Created— bron actief, eerstvolgende run (dagelijks 01:00 UTC) verwerkt 'm400 validation— verplicht veld mist, foute URL of ongeldigewebhook_url401 unauthorized— verkeerde of geen API-key409 duplicate— de URL is al in gebruik binnen jullie omgeving door een actieve of gepauzeerde bron; de body bevat normaliter het bestaandesource_id+active(en bij een gepauzeerde bron eenhint: heractiveer metPATCH /manage-source/<id>, §5b). Een verwijderde bron blokkeert niet, en dezelfde URL in een ándere omgeving ook niet — dit 409 zie je dus alleen bij een échte dubbele.422 probe_failed— URL niet bereikbaar of geen geldige feed429 rate_limited— max 20 requests per minuut
Voorbeeldrespons (201):
{
"source_id": "ab50c37a-23a4-46de-b823-7c924cf3f062",
"client_name": "Some Pharma BV",
"source_type": "xml_feed",
"url": "https://careers.somepharma.com/feed.xml",
"country": "NL",
"min_description_chars": null,
"webhook_url": "https://dev.klarekoek.nl/jobfeed/webhook",
"environment": "dev",
"candidate_count": 1952,
"message": "Bron actief — eerstvolgende pipeline-run (dagelijks 01:00 UTC) verwerkt ~1952 vacatures"
}
⚠️
candidate_countis het totale aantal in de feed. Onze pipeline filtert percountry— voor een NL-klant blijven typisch minder vacatures over. Voor een nauwkeuriger preview zie §6.
Default-landen
- Niet meegegeven →
NL - Pilot dekt Nederland en België — voor
country: "BE"werkt alles direct - Land wordt genormaliseerd naar ISO 3166-1 alpha-2; volledige namen en
lokale varianten worden herkend voor
NL,BEenDE(Netherlands,België,Deutschland, …). Andere waarden slaan we ongewijzigd op. Het pipeline-filter matcht beide vormen.
5b. Klant wijzigen of uitzetten (REST)
Gebruik het source_id uit de add-source-response (of uit de 409-body).
Omgevings-scope: een key beheert alléén bronnen die met een key van dezelfde omgeving zijn aangemaakt. Een
source_iduit een andere omgeving geeft dezelfde404als een onbekend id.
Wijzigen
PATCH https://yxbnxdoirwwgwiugpunj.functions.supabase.co/manage-source/<source_id>
Headers:
Authorization: Bearer <API-key van jullie omgeving>
Content-Type: application/json
Body (alle velden optioneel, minstens één):
{
"client_name": "Some Pharma B.V.",
"url": "https://careers.somepharma.com/feed-v2.xml", // wordt opnieuw geprobed
"source_type": "xml_feed",
"country": "BE",
"min_description_chars": 80, // null = terug naar onze default
"webhook_url": "https://staging.klarekoek.nl/jobfeed/webhook", // null = globale fallback
"active": true // true = gedeactiveerde bron heractiveren
}
Bijzonderheden:
- Een nieuwe
urlwordt gecanonicaliseerd en opnieuw geprobed —422als hij niet bereikbaar/herkenbaar is,409als een andere bron in dezelfde omgeving hem al gebruikt. Zonder explicietesource_typevolgt het type de detectie van de nieuwe feed. - Een gewijzigde
webhook_urlgeldt per direct, óók voor events die nog in onze wachtrij staan (§3.1). - Heractiveren (
active: true) kan409geven als een nieuwe bron de URL inmiddels heeft geclaimd binnen jullie omgeving (de URL komt bij een DELETE immers per direct vrij).
Uitzetten (soft-delete)
DELETE https://yxbnxdoirwwgwiugpunj.functions.supabase.co/manage-source/<source_id>
Headers:
Authorization: Bearer <API-key van jullie omgeving>
- De bron gaat per direct op inactief; er wordt niets weggegooid.
- De feed-URL is per direct weer vrij binnen jullie omgeving — dezelfde
URL opnieuw aanmaken via
POST /add-sourcekan dus meteen. Dat is dan wél een nieuwe bron: de vacatures krijgen nieuwe id's, en de oude id's krijgen diezelfde nacht hunclose-event (beide batches kunnen in dezelfde nacht binnenkomen). - De eerstvolgende nachtelijke run sluit alle openstaande vacatures van
de bron en stuurt jullie de bijbehorende
close-webhook — daarna is de feed leeg voor die klant. - Idempotent: een tweede DELETE geeft opnieuw
200. - Heractiveren kan altijd:
PATCHmet{"active": true}(tenzij de URL intussen opnieuw is uitgegeven — dan409). Keert een vacature terug in de bron, dan krijg je opnieuw eennew-event met hetzelfde ID (§3.3).
Responses (beide methoden)
200 OK— body bevat de actuele bron +message400 validation— ongeldig veld, geen uuid in het pad, of lege PATCH-body401 unauthorized/404 not_found/409 duplicate/422 probe_failed429 rate_limited— max 20 requests per minuut
6. Preview tijdens setup (REST)
Vóórdat een klant in onze sources staat, kunnen jullie tonen hoeveel
vacatures er in de feed zitten.
GET https://yxbnxdoirwwgwiugpunj.functions.supabase.co/preview?url=<URL-encoded>
Headers:
Authorization: Bearer <API-key van jullie omgeving>
Respons (200):
{
"url": "https://careers.somepharma.com/feed.xml",
"source_type": "xml_feed",
"candidate_count": 1952,
"message": "1952 vacatures gevonden in xml_feed (root: <source>)"
}
Mogelijke errors: 400 (ongeldige URL), 401 (auth), 422 (URL is geen herkende feed of niet bereikbaar), 429 (max 20 requests per minuut), 500 (interne fout).
7. Bron-types die wij ondersteunen
| Type | Detectie | Voorbeeld-root |
|---|---|---|
xml_feed |
RSS/Atom/custom job-XML | <rss>, <feed>, <channel>, <source> (Indeed/Adzuna-stijl) |
sitemap |
XML sitemap | <urlset>, <sitemapindex> |
HTML-scraping is niet actief (uitgezet per 26 mei 2026). Jullie kunnen
alleen klanten toevoegen die een feed of sitemap publiceren. Bij twijfel:
gebruik eerst /preview om te checken of de URL detecteerbaar is.
8. Wat moeten jullie ons aanleveren?
- Globale fallback webhook-URL voor productie (per-omgeving endpoints
regelen jullie zelf per jobfeed via
webhook_url, §3.1/§5b) - Lijst extra landen naast NL/BE die jullie willen ondersteunen
- Bevestiging dat de bearer-token-check bij jullie werkt (§3.4)
- Bevestiging dat alle omgevingen vacatures ophalen via
/get-vacancies(§4) — daarna trekken wij de oude gedeeldeKK_API_KEYen de anon-leesroute voor jullie definitief in - Bevestiging dat
type_of_contracthetzelfde is alstype_of_employment. Wij leveren het als alias met dezelfde waarde en hetzelfde id. Bedoelen jullie er iets anders mee, laat het weten — dan bouwen we het als eigen veld.
9. Open punten / known issues
- Twee filters met dezelfde naam krijgen geen id. Als jullie binnen één
filtergroep twee keer dezelfde naam gebruiken, kunnen wij niet kiezen en
blijft het
_id-veldnull(de naam zelf komt gewoon door). Op jullie development-instantie komtRegulatory Affairsbijvoorbeeld voor onder zowel R&D als Regulatory Affairs (stand: augustus 2026). Wij krijgen daar een melding van. - Sectoren. Wij zien de nieuwe
sectors-velden op development en onze code verwerkt ze al. Zolang een filternaam binnen z'n groep uniek blijft, verandert er voor de koppeling niets. Gaan jullie per sector afwijkende filters met dezelfde naam gebruiken, laat het dan even weten — dan breiden wij de koppeling met een sector-dimensie uit. - Wij lezen productie, niet development. De development-instantie heeft
afwijkende, oudere content (10 i.p.v. 12
experiences, 3 i.p.v. 6types_of_employment). Zodra de nieuwe filters-API-structuur (metsectors) op jullie productie live staat, verandert er voor ons niets — we lezen productie al. - Opschoonverzoek filternamen. Een paar filternamen hebben een spatie
aan het eind (
Business Development,Chemical,Quality,Equipment Validation,Temporary to Permanent) en twee gebruiken een typografische apostrof (Bachelor’s Degree (HBO, WO),Master’s Degree (WO)). Wij normaliseren dat weg, dus het werkt gewoon — maar aan de bron opschonen is netter. - Preview-count is wereldwijd, ingestie is per-country. Voor een
klant met
country=Netherlandsziet Klare Koek in de preview het hele feed-totaal, maar in de feed komt alleen NL-deel terecht. Te verbeteren met?country=param op de preview-call — open punt. - Smart-close lag. Een vacature die uit een gezonde bronfeed verdwijnt
krijgt 1 dag later een
close-event. Voor bronnen die als "wankel" (degraded/broken) bekend staan houden we een buffer van 3 dagen aan om mass-closure bij storingen te voorkomen. - Bestaande vacatures opnieuw exporteren. Bij de eerste koppeling met
Klare Koek krijgen jullie geen "bulk-export" automatisch. Optie: één keer
handmatig
/get-vacanciesleegpagineren voor de initiële vulling (inclusiefsource_idper rij). - Update-detectie heeft een 2-daags venster. Een URL die wij <2 dagen
geleden nog in de feed zagen wordt niet opnieuw geïnhaleerd; een
gewijzigde vacaturetekst levert dus pas een
update-event zodra de URL buiten dat venster valt. In de praktijk: updates komen door, maar niet altijd de volgende ochtend al.
10. Test-feed
Voor jullie implementatietest hosten wij een kleine, door ons bestuurbare test-jobfeed met 3 fictieve pharma-vacatures:
https://yxbnxdoirwwgwiugpunj.supabase.co/storage/v1/object/public/testfeeds/kk/feed.xml
Zo gebruik je hem:
GET /preview?url=<encoded feed-url>→ hoortcandidate_count: 3te geven.POST /add-sourcemet deze URL,client_name: "KK Testfeed"en jullie eigenwebhook_url(bv. een dev-tunnel) →201metsource_id.- Wij draaien de pipeline on demand tijdens jullie testvenster en muteren
de feed in stappen, zodat jullie achtereenvolgens een
new-,update-,close- en desgewenstsource_down-batch ontvangen. - Test
PATCH/DELETEop ditzelfdesource_id(§5b) — inclusief het wisselen vanwebhook_urltussen jullie omgevingen. - Na afloop ruimen wij de testbron en -vacatures op.
Plan het testvenster even met ons in — dan draaien wij de runs en de feed-mutaties op afroep, en kijken we live mee met de logs.
Bijlage — versiehistorie
Eén regel per versie; de details staan verwerkt in de hoofdtekst.
- 1.9.1 (1 sep 2026) — de in juni gedeelde
SUPABASE_ANON_KEYis ingetrokken (vacatures ophalen gaat via/get-vacancies, §4). - 1.9 (17 aug 2026) — geen inhoudelijke wijzigingen; document herschreven als beschrijving van de huidige situatie.
- 1.8 (17 aug 2026) — leespad per omgeving geïsoleerd via
/get-vacancies(bestaande keys, geen nieuwe);source_idin elke vacature én in de webhook-bodies (vacancies-array); oude publieke views alleen nog voor onze eigen site (alleen productie-data). - 1.7 (aug 2026) — filters-API wordt nachtelijk gevolgd (jullie lijst is leidend); alle filter-id's in de feed; jullie eigen sleutelnamen als extra velden; 7 API-keys met jullie labels; webhook-secret per omgeving.
- 1.6 (juli 2026) — secties in Markdown; eerste filter-id-kolommen; API-key per omgeving; feed-URL per omgeving en vrij na verwijderen.
- 1.5 (juli 2026) — per-jobfeed
webhook_url;PATCH/DELETEop/manage-source; alle veldnamen Engels; test-feed. - 1.4 en eerder — basisintegratie: webhooks met bearer-token,
leespad,
/add-source,/preview.