JouloDocs

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

FieldRequiredDescription
typenoparticulier (default), zakelijk, or vve.
display_namenoHuman-readable name for the account.
cpo_external_refrecommendedYour 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_handlernojoulo_pays_customer (default) — Joulo pays the customer directly. via_cpo_invoicing — the payout settles through you, and Joulo does not pay the customer directly.
ibanfor eligibilityThe 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_nameyesThe natural person who will sign the machtiging.
person.emailnoSignatory email.
person.phonenoSignatory phone.
connection.ean_codeyes18-digit EAN of the connection.
connection.street house_number postal_code cityyesAddress, validated against the EAN.
connection.house_number_additionnoAddition (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

HTTPerror
400invalid_type, invalid_payout_handler, invalid_iban, person_required, invalid_ean, address_required
409ean_in_use
422ean_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

FieldRequiredDescription
connection_idone of these threeThe connection to correct.
account_idone of these threeThe account, when it has one active connection.
cpo_external_refone of these threeYour customer number, same key as on create.
connection.ean_codenoThe corrected 18-digit EAN.
connection.street house_number postal_code citynoCorrected address fields.
connection.house_number_additionnoSend null to clear it.
acknowledge_mandate_lapsesafter signingSee 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

HTTPerror
400target_required, invalid_ean, address_required
404connection_not_found
409ambiguous_connection, connection_not_active, ean_in_use, mandate_signed, ere_booked
422ean_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.