Onboarding¶
Two ways to get a client from prospect to signed. They produce identical outcomes and identical webhooks — the only difference is who renders the screens.
You build the screens, push each section to us, and submit. We validate, generate the financing contract, and hand back a signing link for you to redirect to.
Best when you already hold most of this data and want the flow entirely inside your own product.
Use the onboarding.url returned when you create the client. The flow carries your logo, colours, and copy; users stay on a page that looks like yours end to end.
Best when you want zero screens to build and no KYC field maintenance as rules change.
Either way, signing works the same
Every signatory must sign. When the person filling the form is one of them, they sign in-session and we return a redirect URL. Otherwise everyone signs via emailed links and we return {"method": "email"}.
Both paths finish with the same client.onboarding_completed webhook.
Signing also records the client's consent to share financial data, so a client onboarded this way needs no separate consent object.
Mode A — push sections, get a signing link¶
Six sections. Push them in any order; the application is created on the first push. PUT is a full overwrite of that section, so it is safe to resend as the user edits.
Push a section¶
| Step | Carries |
|---|---|
applicant |
firstName, lastName, email, phone |
company |
officialName, tradeName, registeredAddress, mainIndustryCode, vatNumber (BTW), iban |
offer |
invoiceAmount, paymentTermDays (1–365), financingPercentage (1–100) |
signatories |
userIsSignatory, plus 1–20 signatories with dateOfBirth, role, canSignAlone |
ownership |
UBOs (max 20), senior managing officials, activitiesAbroad, taxLiabilities |
funding |
hasExternalFunding and, when true, each funding's financierType, sourceType, amount, terms |
applicant¶
{
"firstName": "Sanne",
"lastName": "van Dijk",
"email": "sanne@vandijk-installatie.nl",
"phone": "+31612345678"
}
company¶
{
"officialName": "Van Dijk Installatietechniek B.V.",
"tradeName": "Van Dijk Installatie",
"registeredAddress": {
"street": "Keizersgracht",
"houseNumber": "241-B",
"postalCode": "1016 EA",
"city": "Amsterdam",
"country": "NL"
},
"mainIndustryCode": "4322",
"vatNumber": "NL861234567B01",
"iban": "NL91ABNA0417164300"
}
offer¶
{
"invoiceAmount": { "amount": 424250, "currency": "EUR" },
"paymentTermDays": 30,
"financingPercentage": 80
}
signatories¶
{
"userIsSignatory": true,
"signatories": [{
"firstName": "Sanne",
"lastName": "van Dijk",
"email": "sanne@vandijk-installatie.nl",
"phone": "+31612345678",
"dateOfBirth": "1987-04-19",
"role": "director",
"canSignAlone": true
}]
}
role is one of director, authorized_representative, other. Every listed signatory becomes a required signer on the contract.
ownership¶
{
"ubos": [{
"firstName": "Sanne",
"lastName": "van Dijk",
"dateOfBirth": "1987-04-19",
"nationality": "NL",
"countryOfResidence": "NL",
"ownershipPercentage": 100,
"natureOfControl": "ownership",
"isPEP": false
}],
"seniorManagingOfficials": [],
"activitiesAbroad": { "countries": ["BE", "DE"] },
"taxLiabilities": [
{ "country": "NL", "taxNumber": "NL861234567B01" }
]
}
natureOfControl is one of ownership, voting, other. When no UBO reaches the ownership threshold, send an empty ubos array and list seniorManagingOfficials instead — that is the documented fallback and it is expected, not an error.
funding¶
{
"hasExternalFunding": true,
"fundings": [{
"financierType": "company",
"sourceType": "loan",
"name": "Rabobank",
"registrationNumber": "30046259",
"amount": { "amount": 5000000, "currency": "EUR" },
"terms": "Business loan, 5 year term"
}]
}
Send {"hasExternalFunding": false} when there is none. The section is still required.
Response · 200¶
{
"countryCode": "NL",
"registrationNumber": "68123456",
"step": "ownership",
"updatedAt": "2026-09-18T13:41:22Z"
}
Submit¶
Runs cross-section validation, creates the financing contract, and sends it for signature. Idempotent — replaying returns the same signing instruction rather than a second contract.
{
"submittedAt": "2026-09-18T13:58:11Z",
"signing": {
"method": "redirect",
"url": "https://scrive.com/s/9f21a.../sign",
"expiresAt": "2026-09-25T13:58:11Z",
"signerCount": 1
}
}
{
"submittedAt": "2026-09-18T13:58:11Z",
"signing": {
"method": "email",
"signerCount": 2
}
}
When method is redirect, send the user to url. When it is email, tell them to check their inbox — there is nothing to redirect to.
Errors¶
| Status | Code | When |
|---|---|---|
| 409 | already_submitted |
Submitted under a different idempotency key. |
| 422 | validation_failed |
A required section is missing or internally inconsistent. fields names each one. |
| 503 | signing_unavailable |
The signing provider is temporarily unreachable. Retry — nothing was consumed. |
Mode B — hand off to a white-labelled flow¶
Redirect the user to the onboarding.url from client creation. It carries your theme, logo, and copy, plus the locale you set. Nothing else is required of you.
- Create the client. Keep
onboarding.urlfrom the response. - Open it — new tab, or embedded in your own page.
- The user completes KYC and signs. You render nothing.
- You receive
client.onboarding_completed, thenclient.status_changedwhen underwriting finishes.
Mint a fresh session¶
Use this after a URL expires, or to resume a half-finished application.
language accepts nl, en or fi.
{
"url": "https://aanvraag.noja.nl/?partnerAccess=p_7f2a91&ref=0193f2a1",
"expiresAt": "2026-10-14T09:31:04Z",
"resumesFrom": "signatories"
}
returnUrl sends the user back into your product after signing. resumesFrom tells you which section they stopped at, if you want to set expectations before the redirect.
What you control¶
| Element | Configured by |
|---|---|
| Logo, colours, typography | Your brand assets, supplied once at partner setup |
| Page copy and value propositions | You, per-section overrides |
| Domain | aanvraag.noja.nl, or your own subdomain via CNAME |
| Language | Per-session, nl, en or fi |
| Return destination | Per-session returnUrl |
Brand assets are configured once during partner setup, not per request.
Check onboarding state¶
Everything we hold for the application, in either mode. Useful for resyncing after a failed push, or for showing progress in your own product while a user is inside the hosted flow.
{
"state": "submitted",
"mode": "hosted",
"steps": {
"applicant": { "complete": true, "updatedAt": "2026-09-18T13:20:11Z" },
"company": { "complete": true, "updatedAt": "2026-09-18T13:28:44Z" },
"offer": { "complete": true, "updatedAt": "2026-09-18T13:31:02Z" },
"signatories": { "complete": true, "updatedAt": "2026-09-18T13:44:19Z" },
"ownership": { "complete": true, "updatedAt": "2026-09-18T13:51:37Z" },
"funding": { "complete": true, "updatedAt": "2026-09-18T13:55:08Z" }
},
"submittedAt": "2026-09-18T13:58:11Z",
"signing": {
"state": "partially_signed",
"provider": "scrive",
"signedCount": 1,
"signerCount": 2
}
}
state |
Meaning |
|---|---|
draft |
Application open — sections may be incomplete |
submitted |
Cross-section validation passed, signing started |
signed |
Every signatory signed; the financing contract is sealed |
signing_failed |
Signing cancelled, declined, or expired |
There is no state for "client exists but has no application" — the endpoint returns 404 until the first section is pushed or the hosted flow is opened.
From signing_failed you can mint a fresh session and the client resumes from where they stopped; the application returns to draft.