Onboarding inside your app
Take a customer from your app to a submitted ERE registration and back, without our portal
This guide is for partners with their own app. Your customer never sees the Joulo site or dashboard. They open one screen from your app, finish their registration there, and land back in your app.
The screen is ours. The customer signs the machtiging with Joulo B.V., and we have to show the NEa who signed, when, and which clause text they saw. Your logo sits next to ours at the top.
The sequence
- Upload the customer with
POST /partner/customersandsend_invite: false. Send the address, and for BlueBird Power the charger id. See Upload customers. - Register your return URL once, in the partner portal under API-koppeling. It switches the in-app flow on. See Your return URL.
- Mint an auth-link with
POST /partner/drafts/auth-linkthe moment the customer opens the screen. - Open
auth_urlin an in-app browser. See Which browser to use. - Catch the return URL. We send the customer there with a
status. - Confirm with the API. Read
GET /partner/customersand checkregistration_status.
curl -X POST https://api.joulo.nl/functions/v1/api/partner/drafts/auth-link \
-H "Authorization: Bearer jpk_live_..." \
-H "Content-Type: application/json" \
-d '{ "draft_id": "8f2c...", "return_url": "bluebird://joulo/done" }'{
"ok": true,
"auth_url": "https://joulo.nl/claim-auth/...",
"expires_in": 300,
"resume": false,
"return_url": "bluebird://joulo/done"
}The link is a login credential and lives 5 minutes. Never store it and never email it. Mint a fresh one each time the customer opens the screen. A new link makes the previous one invalid.
What the customer does
The link signs the customer in. They then walk through these steps:
| Step | What happens | When it shows |
|---|---|---|
| Welcome | The customer confirms they want to continue. Business customers enter their KvK number. | First visit only |
| Charger | BlueBird Power customers type their charger id, checked live. | Only when no charger is linked yet |
| Registration | Name, mobile number, address, connection (EAN), IBAN and the machtiging. | Always, until submitted |
| Done | "Je aanmelding is binnen", then back to your app. | After submit |
What you sent at upload shortens the registration:
- The address fills in postcode and house number. We look up street, city and EAN from it.
- One EAN on the address is filled in for them. With several, the customer picks one. They always confirm that their charger sits behind that EAN.
- A pre-coupled charger skips the charger step completely.
The customer signs the machtiging once, on the last step of the registration. It names the EAN and the holder of the connection. Nothing before that step binds anything.
Business customers (zakelijk, vve) also upload a KvK extract and an energy
bill in the flow. Those documents cannot come through the API.
Your return URL
Register it once in the partner portal, page API-koppeling. It is your
app's deep link (bluebird://joulo) or an https address you own. No query
string and no fragment: we add ?status= ourselves.
- An API key cannot set it. A leaked key must not be able to point the end of our flow at a page of its choosing.
GET /partner/mereturns it asapp_return_url, so your integration can check it.- An auth-link may carry its own
return_url, but it must start with the registered one, followed by/,?or nothing. - Without a registered URL, the auth-link opens the classic claim page. That page ends in the Joulo dashboard, not in your app.
- The done screen always tells the customer they can close it, and then opens your URL.
| Error | Status | Meaning |
|---|---|---|
return_url_not_registered | 400 | You sent return_url, but the portal has none. |
return_url_not_allowed | 400 | The URL does not start with the registered one. |
What comes back to your app
We open your return URL with a status, and sometimes a reason:
status | Meaning | What to do |
|---|---|---|
submitted | The registration is in. | Close the browser. Confirm with GET /partner/customers. |
closed | The customer tapped "Terug naar …" before finishing. | Offer to continue later. |
resume | The customer verified with their bank and came back through the system browser. reason=idin. | Mint a new auth-link and open it again. |
failed | The flow cannot continue. reason says why. | Show your own message. Contact us for conflict or ean_conflict. |
reason values for failed: expired, conflict, ean_conflict,
not_found, invalid_token, not_claimed, finish_failed. With
finish_failed the registration may well be in: check the API.
Treat status as a hint. Anyone can open a URL with ?status=submitted.
The API is the source of truth.
Picking up where they left off
Customers leave halfway. An auth-link works again on the same draft while the account is still empty: a concept registration, no IBAN stored and no verified identity.
resume: truein the response means the draft was already activated.- The form saves as the customer types, so it opens with their answers filled in.
- After that, the link is refused with
409 draft_not_claimableand areason. A partner key must never be a login to an account that holds a payout account, a verified identity or a signed machtiging. - The customer then signs in themselves. See the next section.
reason on draft_not_claimable | Meaning |
|---|---|
registration_submitted | Done. The customer signs in themselves from now on. |
account_has_details | The customer already entered an IBAN or verified their identity. |
own_account | The draft sits on an account this link did not create, such as the customer's existing Joulo account. We never log in to that one. |
account_closed | The account no longer exists. |
Customers who already have an account
An upload for an address that already has a Joulo account is refused with
existing_user. An account can still appear between your upload and the
customer's first tap. The link then logs nobody in, because a partner link must
never open an account the customer already had. The screen asks the customer
to sign in themselves.
The same happens when the EAN we found at the address already carries a submitted registration. That can be this customer under another email address, or the previous occupant. The screen explains both. You do not see this status: a partner can upload any address, and it must not learn which households are Joulo customers.
The screen offers Google, Apple and an email link. Google refuses to sign in inside a bare webview, but works in the in-app browsers below. An email link opens from the mail app, so the flow continues in the system browser. It still ends at your return URL.
Bank verification (iDIN)
Some accounts verify their identity with their bank before signing. It is off for most customers today. When it is on, the bank step leaves your app:
- The bank app opens.
- The bank sends the customer to the system browser, not back to your app.
- That page shows a "Terug naar …" button to your return URL, with
status=resume&reason=idin. - You mint a new auth-link. Our screen waits up to three minutes until the verification is in, then continues.
Which browser to use
Use the in-app browser of the platform, not a bare webview:
- iOS:
ASWebAuthenticationSession, withcallbackURLSchemeset to your scheme. It closes itself when we open your return URL. - Android: Custom Tabs, with an intent filter on your return URL.
A bare WKWebView or Android WebView works for the flow itself. You then have
to do two things yourself:
- Watch navigations (
decidePolicyFor/shouldOverrideUrlLoading), and close the view when it reaches your return URL. - Let bank links open outside the view. Otherwise the bank app never opens, and iDIN stays on the bank's website.
Tracking progress
| Phase | Endpoint | Field |
|---|---|---|
| Upload | POST /partner/customers | created[].ean_status |
| Claim | GET /partner/drafts?external_ref= | status: invited → claimed → active |
| Registration | GET /partner/customers | registration_status: concept → ingediend → in_review → goedgekeurd |
| Money | GET /partner/ere-positions | per EAN, once eligible sessions exist |
Draft active means the customer started. registration_status: ingediend
means they finished. There is no webhook: poll a few times a day, and once
right after status=submitted.