JouloDocs

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

  1. Upload the customer with POST /partner/customers and send_invite: false. Send the address, and for BlueBird Power the charger id. See Upload customers.
  2. Register your return URL once, in the partner portal under API-koppeling. It switches the in-app flow on. See Your return URL.
  3. Mint an auth-link with POST /partner/drafts/auth-link the moment the customer opens the screen.
  4. Open auth_url in an in-app browser. See Which browser to use.
  5. Catch the return URL. We send the customer there with a status.
  6. Confirm with the API. Read GET /partner/customers and check registration_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:

StepWhat happensWhen it shows
WelcomeThe customer confirms they want to continue. Business customers enter their KvK number.First visit only
ChargerBlueBird Power customers type their charger id, checked live.Only when no charger is linked yet
RegistrationName, 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/me returns it as app_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.
ErrorStatusMeaning
return_url_not_registered400You sent return_url, but the portal has none.
return_url_not_allowed400The 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:

statusMeaningWhat to do
submittedThe registration is in.Close the browser. Confirm with GET /partner/customers.
closedThe customer tapped "Terug naar …" before finishing.Offer to continue later.
resumeThe 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.
failedThe 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: true in 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_claimable and a reason. 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_claimableMeaning
registration_submittedDone. The customer signs in themselves from now on.
account_has_detailsThe customer already entered an IBAN or verified their identity.
own_accountThe 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_closedThe 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:

  1. The bank app opens.
  2. The bank sends the customer to the system browser, not back to your app.
  3. That page shows a "Terug naar …" button to your return URL, with status=resume&reason=idin.
  4. 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, with callbackURLScheme set 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

PhaseEndpointField
UploadPOST /partner/customerscreated[].ean_status
ClaimGET /partner/drafts?external_ref=status: invited → claimed → active
RegistrationGET /partner/customersregistration_status: concept → ingediend → in_review → goedgekeurd
MoneyGET /partner/ere-positionsper 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.