JouloDocs

Replacement offer

Joulo's offer for replacing a laadstation without a MID meter, shown, ordered and followed inside your app

Only a laadstation with a MID meter earns ERE. A customer with mid_status no_mid has linked one without, so their position stays at zero until it is replaced. Joulo replaces it for one fixed price: a laadstation with a MID meter from the factory, placed on the same spot by an installer from our network, with the old one traded in. Customers find that offer on joulo.nl/vervangen.

GET /partner/replacement-offers gives you the same offer, so your app can show it to that customer. Same products, same prices, same photo.

Joulo stays the seller. Your app places the order with POST /partner/replacement-orders and opens the Joulo checkout page it gets back, like the onboarding inside your app. The customer pays there and comes back to your app. See Ordering. Sessions count from the moment the MID meter is there. Nothing from before counts later.

This differs from replacement_offer in GET /partner/customers and GET /partner/ere-positions. That field names a laadstation from your own store. This endpoint names Joulo's.

The offer for one customer

curl "https://api.joulo.nl/functions/v1/api/partner/replacement-offers?customer_ref=3f9a1c2e" \
  -H "Authorization: Bearer jpk_live_..."

customer_ref is the pseudonymous reference from GET /partner/customers.

{
  "customer_ref": "3f9a1c2e",
  "mid_status": "no_mid",
  "current_brand": "zaptec",
  "available": true,
  "reason": null,
  "offer": {
    "product_slug": "vervanging-zaptec-go-2",
    "name": "Zaptec Go 2, geplaatst",
    "brand": "zaptec",
    "brand_label": "Zaptec",
    "price_cents": 119900,
    "trade_in_cents": 10000,
    "installation_included": true,
    "hardware_via_joulo": false,
    "lead_time_days": null,
    "valid_until": "2027-01-01",
    "same_brand": true,
    "regional": false,
    "covered": true,
    "replaces_brands": ["zaptec"],
    "image_url": "https://joulo.nl/images/partner-stores/zaptec-go-2.webp",
    "options": [
      {
        "product_slug": "vervanging-zaptec-go-2-p1",
        "name": "Zaptec Go 2 met P1-dongle, geplaatst",
        "price_cents": 134900
      }
    ]
  },
  "alternatives": [
    {
      "product_slug": "vervanging-bluebird-bluesky",
      "name": "BlueBird Power BlueSky, geplaatst",
      "brand": "bluesky",
      "brand_label": "BlueSky",
      "price_cents": 89900,
      "trade_in_cents": 0,
      "installation_included": true,
      "hardware_via_joulo": false,
      "lead_time_days": null,
      "valid_until": "2027-01-01",
      "same_brand": false,
      "regional": true,
      "covered": true,
      "replaces_brands": ["easee", "zaptec", "wallbox", "tesla"],
      "image_url": "https://joulo.nl/images/partner-stores/bluebird-bluesky.webp",
      "options": []
    }
  ]
}
FieldMeaning
mid_statusThe same value as in GET /partner/customers. Only no_mid gets an offer.
current_brandBrand slug of the customer's laadstation without a MID meter. null unless mid_status is no_mid.
availabletrue when there is at least one laadstation to offer.
reasonWhy available is false. See below.
offerThe laadstation of the same brand as the customer's current one. null when we have none for that brand; look at alternatives then.
alternativesOther laadstations that replace the customer's brand, by price. A regional one is listed only when the customer's postal code lies in its service area. Show offer first.

Fields of a product:

FieldMeaning
product_slugIdentifies the product. You send it back when ordering.
price_centsWhat the customer pays, in euro cents, including VAT and installation. The trade-in of the old laadstation is already deducted. Show one figure, never a range.
trade_in_centsWhat the old laadstation was worth in this price. Already deducted; context only.
installation_includedtrue: an installer from our network places it on the same spot and takes the old laadstation with them.
hardware_via_joulotrue when Joulo ships the laadstation itself and the installer only places it. Then lead_time_days applies.
lead_time_daysLead time in days for a laadstation Joulo ships itself. null when the installer brings it: the installer then plans the date with the customer.
valid_untilThe price holds until this date.
same_brandtrue on offer. false on an alternative and in the catalogue.
regionaltrue when one installer places this laadstation in a limited area.
coveredWhether this product can be placed at the customer's address. Always true on what the per-customer answer lists.
replaces_brandsBrand slugs of the laadstations this product replaces.
image_urlThe product photo from joulo.nl/vervangen.
optionsVariants at another price, such as a P1 dongle that lets the laadstation follow the capacity of the connection. Each is a product of its own.

When there is no offer

available is false and reason says why:

reasonMeaningWhat to show
midThe customer already has a laadstation with a MID meter.Nothing. Their sessions count.
noneNo laadstation linked yet.Ask the customer to connect one first, through the onboarding inside your app. We decide on the meter once we see the laadstation.
unknownA laadstation is linked, but Joulo has not determined its meter yet.Nothing yet. Poll again later.
no_offerWe replace no laadstation of this brand yet.Nothing. The customer can ask us for a quote on joulo.nl/offerte.

offer is null and alternatives is [] in all four cases.

The catalogue

Without customer_ref you get every product you may show, for a page that explains the offer before a customer is linked.

curl "https://api.joulo.nl/functions/v1/api/partner/replacement-offers" \
  -H "Authorization: Bearer jpk_live_..."
{
  "offers": [
    { "product_slug": "vervanging-tesla-wall-connector-3-mid", "...": "..." },
    { "product_slug": "vervanging-easee-charge-max", "...": "..." }
  ]
}

Laadstations for the whole country come first, then regional ones, each by price. The catalogue runs no service-area check: covered is always true there. Use the per-customer form to know what fits one address.

Errors

StatuserrorMeaning
400customer_ref must be the 8-character reference from GET /partner/customersThe reference has another shape.
403Not available for your partner typeYour partner is in the ambassadeur work model and never sees the brand of a customer's laadstation.
404Customer not foundNo customer of yours has this customer_ref.

Ordering

POST /partner/replacement-orders places the order for one customer. Send a product_slug from the offer for that customer: offer, one of the alternatives, or one of their options.

curl -X POST "https://api.joulo.nl/functions/v1/api/partner/replacement-orders" \
  -H "Authorization: Bearer jpk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "customer_ref": "3f9a1c2e",
    "product_slug": "vervanging-zaptec-go-2",
    "external_ref": "hw-order-8812",
    "phone": "+31612345678",
    "notes": "Laadstation hangt in de carport",
    "return_url": "homewise://auth/joulo-result?screen=vervanging"
  }'
{
  "ok": true,
  "order_id": "a1b2c3d4-0000-4000-8000-000000000001",
  "status": "awaiting_payment",
  "checkout_url": "https://joulo.nl/vervangen/afrekenen/9f1c…?embed=1",
  "checkout_expires_in": 1800,
  "amount_cents": 119900,
  "replayed": false
}
FieldMeaning
customer_refThe customer, from GET /partner/customers. Required.
product_slugWhat to order. Required, and it must be in the offer for this customer.
external_refYour own reference. The same value returns the same order, whatever its status, with replayed: true. At most 120 characters.
phoneThe number the installer calls to plan the visit. Defaults to the number on the account.
cabletethered or socket, for a laadstation that offers that choice (BlueSky). Ignored elsewhere.
notesA note for the installer. At most 500 characters.
return_urlWhere our page sends the customer after paying. Must extend the return URL you registered in the partner portal. Leave it out to use that URL.

The checkout page

Open checkout_url in your app the way you open the sign-in link of the onboarding flow. The customer buys from Joulo, so the price, the address and our terms come from our page. The page shows the laadstation, the trade-in, the total, the address and phone number we have (the customer can change them), the cable choice where it applies, and our terms. The customer pays through Mollie.

The link works for 30 minutes and opens no session on the customer's account: it reads and pays this one order. After 30 minutes the page says the link expired, and an order that never reached Mollie closes as expired. Place the order again for a fresh link. A customer who reached Mollie in time keeps that payment, and Mollie decides how long it stays open.

When the payment is done, our page sends the customer to return_url with two parameters:

statusMeaning
paidThe customer paid. order carries the order_id.
cancelledNo payment: the customer went back, cancelled at the bank, or the link expired. reason says which: closed, expired or none.
failedThe payment failed.

Without a registered return URL the customer stays on our page, with a link to the Joulo dashboard.

What happens after payment

The same route as joulo.nl/vervangen: we hand the job to an installer from our network, who plans the visit with the customer, places the laadstation and takes the old one along. The customer gets the installer's details by e-mail. Your app follows the order with the status endpoint. Once the new laadstation is linked, GET /partner/customers shows mid_status as mid.

Errors

StatuserrorMeaning
400one of the validation messages, return_url_not_registered, return_url_not_allowedSee the API reference.
403Not available for your partner typeYour partner is in the ambassadeur work model.
404customer_not_foundNo customer of yours has this customer_ref.
409not_eligibleNo laadstation without a MID meter. reason is mid, none or unknown.
409offer_unavailableThis product_slug is not in the offer for this customer. reason is not_in_offer or no_offer.
409order_openThe customer already has an open order. The answer carries its order_id and status, and while it awaits payment a fresh checkout_url.

Status

GET /partner/replacement-orders?order_id=… gives one order. Without order_id it gives a page of your orders, newest first; filter with customer_ref and status, page with limit and offset. Poll it; there are no webhooks.

curl "https://api.joulo.nl/functions/v1/api/partner/replacement-orders?order_id=a1b2c3d4-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer jpk_live_..."
{
  "order_id": "a1b2c3d4-0000-4000-8000-000000000001",
  "customer_ref": "3f9a1c2e",
  "external_ref": "hw-order-8812",
  "product_slug": "vervanging-zaptec-go-2",
  "amount_cents": 119900,
  "status": "scheduled",
  "paid_at": "2026-10-22T09:12:00Z",
  "installer": { "company_name": "Laadpunt Installaties B.V.", "phone": "+31201234567" },
  "installed_at": null,
  "completed_at": null,
  "expires_at": null,
  "created_at": "2026-10-22T09:05:00Z",
  "updated_at": "2026-10-22T10:01:00Z"
}

One ladder, in order:

statusMeaningWhat to show
awaiting_paymentPlaced, not paid. expires_at says when it closes unpaid."Rond je betaling af", with the checkout link.
paidPaid. We are finding an installer."Betaald. Een installateur neemt binnen twee weken contact op."
scheduledAn installer holds the job. installer carries their name and phone.The installer's name and number. The customer has them by mail too.
installedThe installer placed the laadstation. installed_at says when."Geplaatst. Koppel je nieuwe laadstation" if it is not linked yet.
completedJoulo closed the order.Nothing more.
expiredNot paid in time.Let the customer order again.
cancelledCancelled by you, by the customer at the bank, or by support.Let the customer order again.
failedThe payment failed.Let the customer order again.

A job on hold (no installer found yet) stays paid until support assigns one. installer gives only a name and a phone number, the same details the customer received.

Cancel

POST /partner/replacement-orders/cancel with { "order_id": "…" } closes an order that still awaits payment. The checkout link stops working and the customer can order again. After payment an installer holds the job, and only support cancels then: the answer is 409 not_cancellable with the current status.

On the sandbox

The sandbox has the same products. Give a fixture customer a laadstation without a MID meter with POST /sandbox/v1/customers/{id}/chargers and mid: false, then call this endpoint with their customer_ref. Ordering works there too, with Mollie in test mode: nothing is charged. To walk the whole status ladder without Mollie, use POST /sandbox/v1/replacement-orders/{id}/advance.