API-sleutels en programmatische toegang
Maak formulieren, kloon ze en genereer client-intakelinks vanuit je eigen backend met werkruimte-gebonden API-sleutels.
Als je al een CRM, een dossierbeheersysteem of een interne portal draait, hoef je niet via het dashboard elk formulier handmatig aan te maken. Met API-sleutels kan je backend de v1 API van DS160.io rechtstreeks aanroepen — formulieren maken, klonen en client-intakelinks genereren wanneer het jou uitkomt.
API-sleutels zijn beschikbaar voor Business-werkruimtes. Elke sleutel is gebonden aan één werkruimte en kan alleen daarbinnen acties uitvoeren.
API base URL
Alle v1-endpoints staan onder:
https://ds160.io/api/v1
Deze documenten kunnen op het white-label-domein van je agency draaien, maar API-aanroepen gaan altijd naar ds160.io. Elk codevoorbeeld op deze pagina gebruikt de volledige URL zodat je kunt knippen en plakken zonder te herschrijven.
Endpoint-referentie
/v1 is stabiel — paden en veldnamen veranderen niet zonder een /v2-major-versie (zie Versionering en support). De paden in de tabel hieronder staan relatief ten opzichte van de werkruimte-prefix /workspaces/:workspaceId/; volledige request- en response-vormen staan in de sectie van elk endpoint.
| Methode | Pad | Scope | Request body |
|---|---|---|---|
POST | /forms | forms:write | name? |
POST | /forms/:formId/clone | forms:clone | disabledSections? |
POST | /forms/:formId/client-links | client-links:write | expiresInDays, defaultLanguage, hideBranding? |
GET | /forms | forms:read / forms:write | query: limit?, cursor? |
GET | /forms/:formId | forms:read / forms:write | — |
Alle bodies en responses zijn JSON. De forms:read-scope geeft alleen-lezen-toegang tot formulier-metadata; forms:write geeft zowel lezen als schrijven, dus een sleutel die alleen ooit schrijft krijgt lezen er gratis bij.
1. Maak een API-sleutel
Open Werkruimte-instellingen → API-sleutels en klik op API-sleutel aanmaken. Kies een omschrijvende naam (we raden één sleutel per integratie aan, bv. Production CRM of Staging webhook handler) en kies de scopes die de integratie nodig heeft:
forms:read— alleen formulier-metadata lezen (lijst / ophalen)forms:write— formulieren aanmaken; geeft ook lezenforms:clone— een bestaand formulier duplicerenclient-links:write— client-intakelinks genereren
Alleen Eigenaren en Beheerders van de werkruimte kunnen sleutels aanmaken.
Als je op Sleutel aanmaken klikt, toont het platform het volledige secret één keer. Kopieer het direct naar je secret manager — na het sluiten van het venster zijn alleen nog de laatste vier karakters zichtbaar. Als je een sleutel kwijtraakt, trek hem in en maak een nieuwe aan.
2. Authenticeer
Alle v1-endpoints verwachten een Authorization: Bearer <secret> header.
export DS160_KEY="...the secret you just copied..."
export DS160_WORKSPACE="your-workspace-id"
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" Het werkruimte-id staat in de URL van elke dashboardpagina (/workspaces/<workspaceId>/...). De werkruimte van de sleutel is server-side vastgelegd — een endpoint van een andere werkruimte aanroepen met de verkeerde sleutel geeft 403 Forbidden.
Tip: de taaltabs op deze pagina zijn gesynchroniseerd. Kies één keer je taal en de rest van de pagina volgt.
3. Maak een formulier
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Smith / B1 — 2026-05" }'
# → { "formId": "65f2…" } Elke aanroep verbruikt één formulierkrediet van je facturatieplan (zoals een formulier maken via het dashboard). Als je werkruimte zonder krediet zit, geeft de aanroep 402 Payment Required — vul je tegoed aan voordat je het opnieuw probeert.
Het optionele veld name stelt een leesbaar label voor het formulier in (maximaal 200 tekens). Laat het weg om een automatisch gegenereerde naam te krijgen — je kunt het formulier hoe dan ook later vanuit het dashboard hernoemen.
4. Kloon een bestaand formulier
Heb je een sjabloon-formulier dat je vaak hergebruikt (bijvoorbeeld hetzelfde J-1-programma van dezelfde werkgever), kloon het dan in plaats van vanaf nul te beginnen. Geef optioneel disabledSections mee om bepaalde formulierpagina’s over te slaan — alles wat je opgeeft wordt vervangen door lege velden in het nieuwe formulier, zodat de klant het opnieuw invult:
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$SOURCE_FORM_ID/clone \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "disabledSections": ["spouse-info-page", "security-background-page-5"] }'
# → { "formId": "65f3…" } Klonen kost één formulierkrediet, net als een nieuw formulier aanmaken.
Geldige waarden voor disabledSections
Geef een lege array (of laat het veld weg) om alle pagina’s mee te kopiëren. Gebruik anders een willekeurige combinatie van de identifiers hieronder — elke andere waarde geeft 400 Bad Request.
| Identifier | Pagina |
|---|---|
personal-info-page-1 | Personal Information - Part 1 |
personal-info-page-2 | Personal Information - Part 2 |
visa-purpose-page | Purpose of Visa |
travel-companions-page | Travel Companions |
previous-us-travel-page | Previous U.S. Travel History |
address-and-phone-page | Address and Phone Details |
passport-page | Passport Information |
contact-info-page | Contact Information |
family-info-page | Family Information |
spouse-info-page | Spouse Information |
deceased-spouse-info-page | Deceased Spouse Information |
former-spouse-info-page | Former Spouse Information |
present-occupation-page | Current Occupation |
previous-occuptation-page | Previous Occupation |
additional-occuptation-page | Additional Occupation Details |
security-background-page-1 | Security Background - Part 1 |
security-background-page-2 | Security Background - Part 2 |
security-background-page-3 | Security Background - Part 3 |
security-background-page-4 | Security Background - Part 4 |
security-background-page-5 | Security Background - Part 5 |
student-visa-page-1 | Student Visa Details - Part 1 |
student-visa-page-2 | Student Visa Details - Part 2 |
temporary-visa-page | Temporary Visa Information |
crew-visa-page | Crew Visa Information |
Alleen secties die relevant zijn voor de visumcategorie van het formulier zijn daadwerkelijk aanwezig; een pagina opgeven die niet in het bronformulier zit, heeft geen effect.
5. Genereer een client-intakelink
Zodra een formulier bestaat, kun je een URL met token genereren waarmee de klant het kan invullen:
curl -X POST https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID/client-links \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "expiresInDays": 7, "defaultLanguage": "en" }' Request body
| Veld | Verplicht | Toelichting |
|---|---|---|
expiresInDays | ja | Geheel getal 1–365. Geen default — weglaten geeft 400 Bad Request. De expiresAt van de link wordt berekend als now + expiresInDays en de onderliggende token-record wordt op dat moment automatisch verwijderd. |
defaultLanguage | ja | Een van de codes in Geldige waarden voor defaultLanguage. Bepaalt de locale-prefix in de teruggegeven url en de taal waarin het formulier opent. |
hideBranding | nee | Boolean. Als true, wordt het formulier zonder branding getoond — het whitelabel-logo en -thema van het agentschap worden voor deze link onderdrukt. Default false (de whitelabel-chrome van de werkruimte wordt getoond als die is geconfigureerd). |
Voorbeeldrespons (200 OK)
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NmMxYmQ0ZjZmNGFkNTQwMzMxNDhmYmEiLCJmb3JtSWQiOiI2NWYyYTkxMTNkZjAxYzAwMTI3YjY4MmEiLCJ3b3Jrc3BhY2VJZCI6IjY1ZjJhOTAwM2RmMDFjMDAxMjdiNjgwYSIsImV4cCI6MTc0ODA0ODI0NywiaWF0IjoxNzQ3NDQzNDQ3LCJkZWZhdWx0TGFuZ3VhZ2UiOiJlbiIsImhpZGVCcmFuZGluZyI6ZmFsc2V9.SiGNATuRe",
"url": "https://intake.your-agency.com/client-intake/65f2a9113df01c00127b682a?token=eyJhbGciOi…",
"expiresAt": "2026-05-24T01:50:46.000Z"
}
| Veld | Toelichting |
|---|---|
token | De JWT zit ook in url. Meestal heb je hem niet direct nodig — klanten openen url en de server valideert de ingebedde token. De jti claim van de token is de unieke link-ID; bewaar die in je systeem als je deze specifieke link later mogelijk wilt intrekken (zie Een client-intakelink intrekken). |
url | De deelbare link. Gebruikt je custom domain als die geverifieerd is en SSL heeft voor de werkruimte; anders valt hij terug op ds160.io. De locale-prefix in het pad (/es, /cn, …) wordt bepaald door defaultLanguage. |
expiresAt | ISO-8601-tijdstempel. De bijbehorende record wordt op dat moment automatisch verwijderd, dus links kunnen na verloop niet meer hergebruikt worden. |
Stuur de url naar je klant. De token in de URL is voor eenmalig gebruik en verloopt op expiresAt — er is geen extra authenticatie nodig om in te vullen.
Een client-intakelink intrekken
Er is geen v1-endpoint om uitgegeven client-links in te trekken — trek in vanuit het dashboard (Workspace → Formulier → Delen → Link intrekken). Intrekking is direct en permanent: de volgende aanvraag met de link geeft 401. Als je een lek vermoedt en niemand beschikbaar is om vanuit de UI in te trekken, is het veiligste alternatief de link gewoon te laten verlopen (begrens expiresInDays overeenkomstig).
Geldige waarden voor defaultLanguage
defaultLanguage bepaalt de taal van de intake-UI wanneer de ontvanger de link voor het eerst opent. Gebruik een van de codes hieronder — elke andere waarde geeft 400 Bad Request. De korte codes (en, cn, …) zijn de interne locale-identifiers van DS160.io; de BCP-47-kolom toont wat we uitsturen in <html lang>, hreflang en de Intl.*-API’s. Gebruik de korte code in API-aanroepen; de BCP-47-vorm is informatief.
| Code | Taal | Eigen naam | BCP-47 |
|---|---|---|---|
en | English | English | en-US |
ru | Russian | Русский | ru-RU |
ro | Romanian | Română | ro-RO |
es | Spanish | Español | es-ES |
cn | Chinese | 中文 | zh-CN |
vi | Vietnamese | Tiếng Việt | vi-VN |
hi | Hindi | हिन्दी | hi-IN |
nl | Dutch | Nederlands | nl-NL |
Behandel client-links als wachtwoorden
De url is een bearer-credential — iedereen die hem heeft kan het formulier invullen tot het verloopt, zonder tweede factor.
- Verstuur via een vertrouwd kanaal; plaats hem niet waar hij kan lekken (publieke chat, geïndexeerde pagina’s, gedeelde bundels).
- Maskeer
?token=in logs. Dejticlaim alleen is veilig om te loggen. - Trek in bij vermoeden van een lek — intrekking is direct en permanent.
expiresInDaysis een afweging: korter = kleiner lekvenster, langer = minder hoeven heruitgeven.
6. Formulieren opvragen of opsommen
# All forms in the workspace
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY"
# A single form by id
curl https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID \
-H "Authorization: Bearer $DS160_KEY" Deze leesendpoints accepteren de forms:read-scope (alleen-lezen) of forms:write (geeft ook lezen).
Paginering
GET /v1/workspaces/:workspaceId/forms is gepagineerd. Geef de query-parameters ?limit= en ?cursor= mee om door de resultaten te lopen:
| Query-parameter | Betekenis |
|---|---|
limit | Maximaal aantal formulieren per pagina. Standaard 50, harde limiet 200. |
cursor | Formulier-id uit de nextCursor van de vorige pagina. Laat hem weg bij de eerste aanvraag om bij het nieuwste formulier te beginnen. |
Elke respons bevat een nextCursor veld. Als er geen pagina’s meer zijn, is nextCursor gelijk aan null. Voorbeeld:
{
"forms": [
{ "id": "65f2a911…", "name": "Smith / B1", "status": "in_progress", "workspaceId": "65f2a900…", "userId": "65f2a8f0…", "preferredConsulate": null, "createdAt": "2026-05-14T09:12:33.000Z", "archivedAt": null },
{ "id": "65f2a8a3…", "name": "Garcia / F1", "status": "completed", "workspaceId": "65f2a900…", "userId": "65f2a8f0…", "preferredConsulate": "MAD", "createdAt": "2026-05-13T18:01:09.000Z", "archivedAt": null }
],
"nextCursor": "65f2a8a3…"
}
Loop verder door bij de volgende aanvraag ?cursor=65f2a8a3…&limit=50 mee te geven. Formulieren zijn gesorteerd van nieuw naar oud op id, dus een cursor verankert je leespositie zelfs als er tussen aanroepen nieuwe formulieren bijkomen — je krijgt geen duplicaten en mist niets dat bestond op het moment dat je begon te pagineren.
status-waarden van een formulier
Het status-veld op elke formulier-metadata-respons is één van:
| Waarde | Wanneer van toepassing |
|---|---|
not_started | Formulier is aangemaakt (via API of dashboard) maar er is nog geen enkel veld ingevuld. |
in_progress | Minstens één veld is ingevuld. De meeste formulieren brengen het grootste deel van hun levensduur in deze status door. |
completed | De aanvrager heeft de intake voltooid en ingediend; de agency kan nu de DS-160-PDF downloaden/indienen. |
archived | Het formulier is gearchiveerd vanuit het dashboard. Uitgesloten uit standaard-lijstresultaten tenzij je expliciet om gearchiveerde items vraagt. |
Het wire-formaat gebruikt underscores (in_progress, niet in-progress). Vergelijking met de bovenstaande strings as-is werkt altijd.
PII-toegang via de API
De v1-formulier-metadata-endpoints (GET /workspaces/:workspaceId/forms en GET /workspaces/:workspaceId/forms/:formId) geven uitsluitend operationele metadata terug: id, name, status, workspaceId, userId, preferredConsulate, createdAt, archivedAt. Ze tonen niet de antwoorden van de aanvrager — geen namen, geboortedata, paspoortnummers, reisgeschiedenis of welk ander DS-160-veld dan ook is bereikbaar via /v1.
Voltooide DS-160-PDFs en volledige velddata zijn alleen bereikbaar via het dashboard, onder de audit-gelogde eigenaar van de aanvrager. Als je integratie door de aanvrager ingevulde data moet lezen, doe dat dan via je eigen client-intake-flow (je CRM verzamelt de data eerst, jij stuurt ze naar DS160.io) — ga er nooit van uit dat de API ze teruggeeft.
Rate limits
Elke sleutel is beperkt tot 60 verzoeken per minuut. Elke respons bevat:
| Header | Betekenis |
|---|---|
X-RateLimit-Limit | De limiet (60). |
X-RateLimit-Remaining | Resterende verzoeken in het huidige venster. |
X-RateLimit-Reset | Unix-tijdstempel waarop het venster reset. |
Bij overschrijding geeft de API 429 Too Many Requests met een Retry-After header. Als je integratie legitiem een hogere limiet nodig heeft, neem contact op met support — we kunnen het maximum voor specifieke sleutels verhogen.
Idempotentie en retries
De v1-API ondersteunt geen Idempotency-Key-header. Concreet:
POST /formsenPOST /forms/:formId/clonezijn niet idempotent en verbruiken bij elke aanroep een formulierkrediet. Een naïeve retry na een netwerktimeout maakt een duplicaatformulier aan en verbrandt een tweede krediet.POST /forms/:formId/client-linksis ook niet idempotent maar verbruikt geen krediet — een herhaalde aanroep slaat simpelweg een tweede link met eigenjtienexpiresAt.- Alle
GET-endpoints zijn vrij te retryen.
Aanbevolen retry-patroon voor create/clone: als een POST /forms (of /clone) timeoutet of een 5xx geeft, retry niet blind. Roep in plaats daarvan GET /workspaces/:workspaceId/forms aan (formulieren zijn gesorteerd van nieuw naar oud) en controleer of er in de afgelopen seconden een formulier met de door jou opgegeven name is aangemaakt. Zo ja, behandel de oorspronkelijke aanroep als geslaagd en sla de retry over. Zo nee, dan is de oorspronkelijke schrijfactie zeker niet aangekomen en is een retry veilig.
We zouden Idempotency-Key-ondersteuning in een toekomstige minor release kunnen toevoegen; integraties moeten erop plannen maar er vandaag niet op vertrouwen.
Een sleutel intrekken
Als een sleutel is gelekt, uit gebruik raakt of niet meer wordt gebruikt, trek hem dan in via Werkruimte-instellingen → API-sleutels → Intrekken. Intrekking werkt direct — het volgende verzoek met de sleutel geeft 401 Unauthorized. Ingetrokken sleutels blijven in de lijst voor auditing, maar kunnen niet meer worden geactiveerd; maak een nieuwe aan als de integratie moet blijven draaien.
Best practices
- Eén sleutel per integratie. Makkelijker om gericht in te trekken als een systeem wordt uitgefaseerd, en de
Last used-kolom in het dashboard toont welke integraties nog actief zijn. - Minimale scopes. Een sleutel die alleen client-links genereert hoeft geen
forms:clonete hebben. Kleinere scopes betekenen een kleiner impactgebied als het secret lekt. - Bewaar secrets in een vault. Commit ze nooit naar versiebeheer en stop ze niet in een frontend-bundle — elke browsertab zou ze blootstellen.
- Roteer volgens schema. Maak een nieuwe sleutel aan, schakel de integratie over, trek dan de oude in. Het dashboard toont wanneer een sleutel voor het laatst is gebruikt zodat je de overgang kunt bevestigen voor je intrekt.
Fouten
Alle niet-2xx-responses delen dezelfde JSON-vorm:
{ "error": "leesbaar bericht" }
De error-waarde is een string. Voor 400 Bad Request uit schema-validatie is het een geserialiseerd JSON-array dat elke geschonden constraint beschrijft; voor de rest is het een kort bericht dat je direct kunt tonen of loggen.
| Status | Wanneer het optreedt | Voorbeeldbody |
|---|---|---|
400 | Request body of pad-parameters faalden de validatie (vereist veld ontbreekt, waarde buiten bereik, onbekende enum-waarde, enz.). | { "error": "[{\"keyword\":\"required\",\"params\":{\"missingProperty\":\"expiresInDays\"}}]" } |
401 | Authorization-header ontbreekt, misvormde Bearer-token, het secret komt met geen sleutel overeen, of de sleutel is ingetrokken. | { "error": "Missing API key" } · { "error": "Invalid API key" } · { "error": "API key revoked" } |
402 | De werkruimte heeft geen formulierkrediet meer. Alleen create/clone. Vul aan en probeer opnieuw. | { "error": "Workspace has no remaining credits" } |
403 | Sleutel mist de vereiste scope, of :workspaceId in het pad komt niet overeen met de gebonden werkruimte van de sleutel. | { "error": "Missing required scope: forms:write" } · { "error": "API key does not match workspace" } |
404 | Formulier niet gevonden, of het formulier bestaat maar in een andere werkruimte dan die van de sleutel. (Teruggegeven als 404 in plaats van 403 om geen bestaan over werkruimtes te lekken.) | { "error": "Form not found" } |
429 | Per-sleutel rate limit overschreden (default 60 req/min). Wacht tot de Retry-After-waarde (seconden) en probeer opnieuw. | { "error": "Rate limit exceeded" } |
5xx | Tijdelijke serverfout. GETs zijn veilig te retryen; voor schrijf-endpoints zie Idempotentie en retries voor je retryt. | — |
End-to-end-voorbeeld
Een complete intake-flow — een formulier aanmaken, een client-link slaan, versturen en pollen tot voltooiing — met curl. Echte integraties gebruiken hun eigen HTTP-client; de vorm is in elke taal hetzelfde.
# 0. credentials (één keer instellen)
export DS160_KEY="..."
export DS160_WORKSPACE="65f2a900..."
# 1. Maak een formulier. Vang de formId uit de respons.
FORM_ID=$(curl -s -X POST \
https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Smith / B1 — 2026-05" }' \
| jq -r .formId)
# 2. Sla een client-intakelink die 14 dagen geldig is.
LINK_JSON=$(curl -s -X POST \
https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID/client-links \
-H "Authorization: Bearer $DS160_KEY" \
-H "Content-Type: application/json" \
-d '{ "expiresInDays": 14, "defaultLanguage": "en" }')
CLIENT_URL=$(echo "$LINK_JSON" | jq -r .url)
# 3. Stuur $CLIENT_URL naar de aanvrager via je gebruikelijke kanaal
# (e-mail, veilig portaal, enz.). De aanvrager vult het formulier in.
# 4. Poll op voltooiing. Status-overgangen:
# not_started → in_progress → completed
curl -s https://ds160.io/api/v1/workspaces/$DS160_WORKSPACE/forms/$FORM_ID \
-H "Authorization: Bearer $DS160_KEY" \
| jq .status
# → "completed" zodra de aanvrager klaar is
# 5. Download de DS-160-PDF vanuit het dashboard
# (geen v1-endpoint hiervoor vandaag).
Wat opmerkingen bij dit recept:
- Polling-cadans: elke 5–15 minuten is ruim voldoende. Met een plafond van 60 req/min kan één sleutel comfortabel duizenden lopende formulieren pollen.
- Webhooks zijn nog niet beschikbaar. Als je push-notificaties bij voltooiing nodig hebt, hou deze pagina in de gaten of neem contact op met support — het staat op de roadmap.
- Stap 5 (het voltooide PDF downloaden) vereist vandaag een dashboard-sessie. De v1-API toont met opzet geen antwoorden van de aanvrager — zie PII-toegang via de API.
Versionering en support
Stabiliteit. /v1 is het stabiele API-oppervlak. We voegen nieuwe optionele velden, nieuwe endpoints en nieuwe enum-waarden binnen /v1 toe zonder de major-versie te verhogen, maar we hernoemen, verwijderen of veranderen het type van bestaande velden niet. Als we ooit een breaking change nodig hebben, komt die onder /v2 en blijft /v1 minstens 12 maanden ernaast werken.
Backward-compatible toevoegingen waarop je moet plannen:
- Er zullen na verloop van tijd nieuwe optionele request-velden verschijnen. Bestaande requests die ze niet meesturen blijven werken.
- Er kunnen nieuwe response-velden bijkomen. Behandel onbekende velden als negeerbaar — faal niet dicht als er een toekomstig veld verschijnt.
- Er komen nieuwe enum-waarden voor
status,defaultLanguageendisabledSectionsbij. Crash niet op onbekende waarden; log en val door.
Ondersteuning:
- Hogere rate limits, custom domains, vroege toegang tot webhooks en integratievragen: neem contact op met je account manager of support@ds160.io.
- Bugreports tegen de v1-API: zelfde adres. Elke respons draagt een
x-request-id-header — neem die op in je rapport zodat we de exacte call in onze logs kunnen vinden.