JouloDocs

Upload customers

POST /partner/customers • push customers as claimable drafts, with or without Joulo sending the invite

POST /partner/customers turns each entry into a customer_draft through the same validation, dedupe, cap and attribution path as the portal's CSV import. Name and email are enough; the customer completes the rest (address, charger, authorization) in the claim flow.

Request

curl -X POST https://api.joulo.nl/functions/v1/api/partner/customers \
  -H "Authorization: Bearer jpk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "consent": true,
    "send_invite": true,
    "customers": [
      { "name": "Jan de Vries", "email": "[email protected]" },
      { "name": "Bedrijf B.V.", "email": "[email protected]", "type": "zakelijk", "kvk": "12345678" }
    ]
  }'
FieldRequiredNotes
consentyesMust be true: you confirm you may supply these customers and pre-register them with Joulo. Otherwise the request fails with 400 consent_required.
customers[]yesNon-empty array of { name, email }. Optional per row: type (particulier • default, zakelijk, vve), ean, kvk, btw.
send_invitenoDefault true: Joulo emails each customer their claim link. Set to false to deliver the links yourself • they are returned in the response.

Limits: at most 1.000 rows per request and a rolling cap of 5.000 rows per 30 days per partner. Rows over either limit are reported in skipped, never silently dropped.

Response

{
  "ok": true,
  "added": 2,
  "batch_id": "…",
  "claim_urls": [
    { "email": "[email protected]", "claim_url": "https://joulo.nl/claim/…" }
  ],
  "skipped": {
    "invalid": 0,
    "missing_kvk": 0,
    "already_drafted": 0,
    "existing_user": 0,
    "suppressed": 0,
    "conflict": 0,
    "over_request_limit": 0,
    "over_cap": 0
  },
  "cap": { "rolling_days": 30, "rolling_cap": 5000, "remaining_before": 4998 }
}
  • claim_urls is only present when send_invite is false.
  • skipped explains every row that did not become a draft: invalid email, business rows without KvK, already drafted, already a Joulo user, and rows over the per-request or rolling cap.
  • cap shows the rolling upload window so your integration can pace batches.

The claim flow

Each draft resolves to a personal link on joulo.nl/claim/…. The customer confirms who they are, connects their charger and signs the ERE authorization themselves.

Only the holder of the connection (EAN) can sign the authorization • an upload never creates an authorization by itself. Drafts that are never claimed expire without side effects.

The claim link identifies the draft, not the user: opening it still requires a sign-in. Inside an embedded webview that is a problem — Google blocks OAuth in webviews, and an emailed magic link forces the user out to their mail client.

For that case, mint a short-lived auth link just-in-time, the moment the user opens the screen:

POST /partner/drafts/auth-link
{ "draft_id": "…" }        // or { "email": "[email protected]" }
{ "ok": true, "auth_url": "https://joulo.nl/claim-auth/…", "expires_in": 300 }

Load auth_url straight into the webview. Joulo signs the customer in server-side and lands them on the claim page — no OAuth, no email round-trip.

  • The link is valid for 5 minutes and is a login credential: never store it, never email it, request a fresh one per screen-open. Re-minting invalidates the previous link.
  • If the customer is new to Joulo, an account is created on the uploaded email address (confirmed, passwordless).
  • If the email — or the uploaded EAN — already belongs to an existing Joulo account, the auth link deliberately does not sign in. The customer lands on the normal claim page and signs in with their own account. A partner-delivered link can never grant access to an existing account.
  • Works only for drafts in invited or claimed status; activated, expired or conflicted drafts return an error.

Reading back status

  • GET /partner/drafts • uploaded rows and their claim status.
  • GET /partner/customers • claimed, attributed customers, including the effective fee and your partner share per customer. Self-signup customers are pseudonymised (city + month granularity); customers you uploaded yourself include contact details for follow-up.