Accounts
Create a concept account (customer + connection), correct its EAN or address, and list your accounts with their status.
Accounts
An account is the administrative and legal anchor: the EAN, the address, the machtiging, the sessions, and the payout all hang off it. You create it as a concept — it exists and is attributed to you, but until the customer signs their machtiging it doesn't count toward anything.
Create an account
POST /v1/accounts — scope cpo:accounts:write
curl -X POST https://api.joulo.nl/functions/v1/api/v1/accounts \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"type": "particulier",
"display_name": "Jan de Vries",
"cpo_external_ref": "CUST-100482",
"payout_handler": "joulo_pays_customer",
"iban": "NL91ABNA0417164300",
"person": {
"full_name": "Jan de Vries",
"email": "jan@example.nl",
"phone": "+31612345678"
},
"connection": {
"ean_code": "871685920000123456",
"street": "Dorpsstraat",
"house_number": "1",
"postal_code": "6971AB",
"city": "Brummen"
}
}'{
"ok": true,
"account_id": "60092124-c768-4191-9c9d-55ef3fc49999",
"connection_id": "2eca20d8-06ed-4354-a80d-c8c840a1640d",
"location_id": "…",
"ean_validation_status": "matched",
"iban_stored": true,
"mandate_status": "pending",
"account": {
"id": "60092124-…",
"type": "particulier",
"display_name": "Jan de Vries",
"payout_handler": "joulo_pays_customer",
"cpo_external_ref": "CUST-100482",
"created_at": "2026-07-20T12:00:00Z"
}
}Body
| Field | Required | Description |
|---|---|---|
type | no | particulier (default), zakelijk, or vve. |
display_name | no | Human-readable name for the account. |
cpo_external_ref | recommended | Your customer number, unique per CPO. It is the idempotency key for this call. It also filters GET /v1/accounts and GET /v1/ere-positions, and addresses POST /v1/connections:update. The other endpoints take account_id. |
payout_handler | no | joulo_pays_customer (default) — Joulo pays the customer directly. via_cpo_invoicing — the payout settles through you, and Joulo does not pay the customer directly. |
iban | for eligibility | The customer's IBAN (15–34 chars, spaces ignored). Required before any account can become ERE-eligible, via_cpo_invoicing included: the eligibility gate asks for it regardless of who pays the customer. Stored encrypted. You can send it here or add it later by re-posting with the same cpo_external_ref. |
person.full_name | yes | The natural person who will sign the machtiging. |
person.email | no | Signatory email. |
person.phone | no | Signatory phone. |
connection.ean_code | yes | 18-digit EAN of the connection. |
connection.street house_number postal_code city | yes | Address, validated against the EAN. |
connection.house_number_addition | no | Addition (e.g. A). |
Idempotent. Re-posting with the same cpo_external_ref returns the
existing account ("idempotent": true) instead of creating a duplicate. A
re-post that includes iban stores it on the existing account, so you can
add or correct the IBAN after creation.
A re-post must still pass the body checks above. After those, it reads only
iban and ignores every other field. To correct the EAN or the address, use
POST /v1/connections:update.
The answer to a re-post carries ok, idempotent, account_id,
mandate_status, iban_stored and account. It has no connection_id,
location_id or ean_validation_status. Its mandate_status is the current
state: active once the customer signed the machtiging.
iban_stored in the response tells you whether the IBAN was saved (false
if you sent none, or if storage failed and you should retry).
EAN exclusivity
An EAN can be booked by exactly one party. If the EAN is already registered
(on your side or elsewhere), you get 409 ean_in_use. The address is checked
against the EAN; a clear mismatch returns 422 ean_address_mismatch.
On the sandbox that address check does not run, so any 18-digit EAN is accepted there.
New-build addresses are accepted. The Dutch EAN register often holds no
connection for a new address until months after delivery. In that case the
account is created with the connection marked unverified and queued for manual
review. The EAN must still look Dutch (871…), or you get
422 ean_not_dutch.
Errors
| HTTP | error |
|---|---|
| 400 | invalid_type, invalid_payout_handler, invalid_iban, person_required, invalid_ean, address_required |
| 409 | ean_in_use |
| 422 | ean_address_mismatch, ean_not_dutch |
Correct a connection
POST /v1/connections:update — scope cpo:accounts:write
Fixes a wrong EAN or address on a connection you already created. Send the fields you want to change; everything you leave out keeps its current value, so the EAN alone is a valid body.
Address the connection by connection_id, or by the account (account_id or
your own cpo_external_ref) when it has a single active connection.
curl -X POST https://api.joulo.nl/functions/v1/api/v1/connections:update \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"cpo_external_ref": "CUST-100482",
"connection": { "ean_code": "871685920000123456" }
}'{
"ok": true,
"changed": true,
"connection_id": "2eca20d8-…",
"account_id": "60092124-…",
"ean_code": "871685920000123456",
"ean_validation_status": "matched",
"registration_status": "concept",
"mandate_status": "pending",
"mandate_revoked": true
}Body
| Field | Required | Description |
|---|---|---|
connection_id | one of these three | The connection to correct. |
account_id | one of these three | The account, when it has one active connection. |
cpo_external_ref | one of these three | Your customer number, same key as on create. |
connection.ean_code | no | The corrected 18-digit EAN. |
connection.street house_number postal_code city | no | Corrected address fields. |
connection.house_number_addition | no | Send null to clear it. |
acknowledge_mandate_lapses | after signing | See below. Ignored when no machtiging is active. |
The EAN and the address are re-validated together on every change, on the same
rules as create: mismatched is refused, and the EAN must still be free
(409 ean_in_use).
After the customer signed
The machtiging names the EAN and the address, and its own clause text says it lapses when either changes. So a correction on a signed connection is not a field edit: it ends that authorization.
Send acknowledge_mandate_lapses: true to confirm. We then revoke the
machtiging, put the connection back to concept and clear the account's
eligibility, and you issue a new signing link with
POST /v1/mandate-links. The customer signs the corrected
EAN, and the account becomes eligible again. Without the flag you get
409 mandate_signed and nothing changes.
A month you delivered that is already registered as ERE fixes the connection in
place: that energy is booked against the old EAN, so the endpoint answers
409 ere_booked and the correction comes to us. This check runs before the
comparison with the stored values, so an unchanged body gets the same 409.
Errors
| HTTP | error |
|---|---|
| 400 | target_required, invalid_ean, address_required |
| 404 | connection_not_found |
| 409 | ambiguous_connection, connection_not_active, ean_in_use, mandate_signed, ere_booked |
| 422 | ean_address_mismatch, ean_not_dutch |
List accounts
GET /v1/accounts — scope cpo:accounts:read
Paginated with ?limit (default 50, max 200) and ?offset. Returns each
account with its signatory and per-connection registration + mandate state — so
you can track a customer from concept to signed to registered.
To fetch one specific account, filter on your own customer number:
GET /v1/accounts?cpo_external_ref=your-crm-id-001. That is the same
cpo_external_ref you passed on create (the idempotency key), so a status
poll never has to page through the full list.
{
"accounts": [
{
"id": "60092124-…",
"type": "particulier",
"display_name": "Jan de Vries",
"payout_handler": "joulo_pays_customer",
"cpo_external_ref": "CUST-100482",
"access_channel": "cpo_api",
"created_at": "2026-07-20T12:00:00Z",
"signatory": { "full_name": "Jan de Vries", "email": "jan@example.nl" },
"connections": [
{
"connection_id": "2eca20d8-…",
"ean_code": "871685920000123456",
"status": "active",
"registration_status": "concept",
"ean_validation_status": "matched",
"mandate_status": "pending",
"mandate_valid_from": null
}
]
}
],
"limit": 50,
"offset": 0,
"count": 1
}Each connection carries status: active or archived. An archived connection
no longer counts, and POST /v1/connections:update cannot correct it.
After the customer signs, we re-check an unverified connection against the
EAN register about once a week. It becomes matched once the register confirms
the pair, or mismatched when the register lists another EAN at the address.
mandate_status is pending until the customer signs, then active.
registration_status moves concept → ingediend once signed and submitted
for review.
access_channel is always cpo_api: the list holds only the accounts you
created through this API.
connections holds every connection on the account, archived ones included.
Only an active connection counts toward ERE, and the write endpoints act on
active connections only.