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. Only when bank verification is on, see below. | 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,
charger_not_linked. With finish_failed the registration may well be in:
check the API.
charger_not_linked only occurs when you push sessions
yourself. We could not attach your pre-coupled charge point to the customer,
usually because another Joulo account already holds it. The customer has no
coupling screen for it, so we pick it up and contact the customer.
ean_conflict means another customer already holds the EAN on an active
draft. The draft then moves to conflict in GET /partner/drafts, and nothing
changes on the customer's account: no link to you, no new energy company, no
IBAN.
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
A customer who is already with Joulo can activate from your app too. Upload
them with send_invite: false, as for any customer. The answer is the same as
for a new address: a partner never learns who is already a Joulo customer.
An energy supplier's customer who already chose that supplier in Joulo needs no
sign-in at all. We activate the draft ourselves when all of these hold: the
customer has your company as energy company in Joulo, did not turn off sharing
with you, signed the authorisation, submitted their registration, and
registered the EAN you sent. That customer is already in your
GET /partner/ere-positions, so you learn nothing
new; the row now carries your external_ref. We check this when you upload the
draft, every 15 minutes after that, and when you request an auth-link. The
auth-link call then answers
409 draft_not_claimable with status: "active" and reason: "own_account":
read that as done, and show the customer their ERE position.
The same goes for a customer who registered the EAN of your draft on another
Joulo account, for example under another email address. When their row in
GET /partner/ere-positions
carries your external_ref with external_ref_match: "ean", and they signed
the authorisation and submitted their registration, we activate your draft on
that account. This also applies when the draft is claimed or conflict, or
active on an empty account that your auth-link made. Empty means: no EAN, no
charger and no charging sessions. We check every 15 minutes and when you
request an auth-link. The row then shows external_ref_match: "claim".
Otherwise, when the customer opens your auth-link, one of two things happens:
- The account is empty. Many customers opened a Joulo account once and never finished it. "Empty" means: no charger, no charging sessions, no signed authorisation and no submitted registration. We replace the account with a new one, and the flow continues as for a new address. An IBAN or EAN the customer entered there does not count; the flow asks for them again.
- The account holds more. The link then logs nobody in, because a partner
link never opens an account the customer already had. The screen asks the
customer to sign in themselves. They then confirm the link with you in one
tap. A customer whose registration was already submitted then returns to
your return URL with
status=submitted.
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.
Activating the draft of an energy supplier makes that supplier the customer's
energy company in Joulo, also when another one was set. That is the same as a
switch in the customer's own account. From then on the customer is in your
GET /partner/ere-positions with your
external_ref, and in your GET /partner/customers. The supplier they came
from no longer sees them.
An active draft of the same customer at another partner, such as their
installer, does not block the activation, also not on the same EAN. Only an
EAN that is active for another customer ends in ean_conflict.
We send no claim mail and no reminders for a draft on an existing account. Your
app delivers the link. With send_invite: true we would mail the claim link,
and that mail asks the reader to create an account. Such an address is
therefore still refused with existing_user when send_invite is true.
Send the EAN or the address with every upload. Without them, a customer who uses another email address at Joulo can end up with a second account. The screen does offer to sign in with the existing one.
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)
New customers on joulo.nl can verify their identity with their bank. The
embedded flow skips that step, so your customers never see it and resume
does not occur today.
Are you an energy supplier, and do you send the EAN with the upload? Then you
vouch for who the customer is, and they never get the step, also not on the
claim page or in the sandbox. That holds while the customer keeps that EAN. A
customer who registers another EAN gets the step. An address alone does not
vouch: no EAN means iDIN. That is why the upload refuses a row from an energy
partner without an EAN (missing_ean).
If we turn it on for your customers, we tell you first. The bank step then 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: a row once the EAN is known, figures once ere_eligible is true |
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.