Log inTalk to us
Billing and settlement

Keep your QR stickers working when you move to Ivora

Your chargers already carry QR stickers from your previous platform. Ivora can answer those scans and send each driver to the right checkout for that charger at that moment:

  • Not yet moved: the driver goes to your previous platform's page for that sticker (passthrough), exactly as today.
  • Moved to Ivora: the driver goes to the Ivora checkout page for that charger.
  • Moved back (a rollback): the sticker goes back to the previous platform as well.

Scans follow the same per-charger move that switches your chargers' connection (the migration proxy's flip), so a sticker never points at a platform the charger is not connected to for longer than the move itself. Codes Ivora does not know land on a page in your brand where drivers enter the charger ID printed on the charger. They never reach a dead link.

flowchart diagram; its source follows
Diagram source
flowchart LR
  Driver[Driver scans sticker] --> Host[Your QR hostname or Ivora's scan URL]
  Host --> Map{Sticker code known?}
  Map -->|No| Find[Find your charger page]
  Map -->|Yes| Side{Charger moved?}
  Side -->|Not yet / rolled back| Old[Previous platform URL]
  Side -->|Yes| Checkout[Ivora checkout for that charger]

Important

QR sticker redirects are not available in production yet: the qr and qr-domains routes answer 404, and every scan shows a neutral "not active" page. This guide describes them as they work in preproduction; Ivora turns them on in production in a later release.

1. Decide where scans arrive

What your stickers encode decides whether you can keep them:

Your sticker encodesWhat to doReprint?
A hostname you control, such as https://pay.example.com/c/1042Point that hostname at Ivora (below). Every existing path keeps workingNo
A path on your own website, such as https://www.example.com/charge/1042Keep your website and redirect that path prefix (302) to Ivora's scan URL with the same code, or move a dedicated hostnameNo
Your previous vendor's own domain, such as https://app.oldvendor.com/s/1042, or its short-link serviceYou cannot point a domain you do not own. Reprint with your own hostname or Ivora's scan URL, unless the vendor agrees to redirect those paths to IvoraYes
Only the charger ID as textDrivers type it on the find page of your QR hostname or Ivora's scan URLOptional

Reprint once, on a hostname you control: then no later platform change needs a reprint again. A sticker for a charger with several connectors may name the connector (evse_number), or Ivora shows the driver a choice.

Your own hostname (QR domain)

A QR domain uses the same DNS verification and certificate pipeline as custom OCPP domains:

  1. POST /v1/tenants/{tenant_id}/qr-domains with {"hostname": "pay.example.com"} (stations:write). The response lists two DNS records: the address record and the ownership TXT record _ivora-qr.pay.example.com.
  2. Add both at your DNS provider. Keep the address record DNS-only (not proxied by a CDN): Ivora issues the hostname's certificate itself.
  3. POST /v1/tenants/{tenant_id}/qr-domains/{domain_id}/verify. Once the records check out (dns_verified), Ivora's edge issues the certificate and the domain becomes active and ready.

Every sticker path on a ready QR domain is a scan: https://pay.example.com/<code>. A hostname serves one purpose: OCPP, payment, QR or return. DELETE disables it at once; scans then stop. The step-by-step guide, with the exact records and troubleshooting, is Set up a custom QR domain.

Ivora's scan URL

Without a hostname of your own, stickers can encode Ivora's scan URL for your project: GET /v1/tenants/{tenant_id}/qr/settings returns it as scan_url_template.

For example https://api.ivoracharge.com/v1/qr/7/{code}.

2. Configure your previous platform

PUT /v1/tenants/{tenant_id}/qr/settings (stations:write):

{
  "previous_platform_origins": ["https://app.oldvendor.com"],
  "previous_url_template": "https://app.oldvendor.com/start/{code}",
  "default_route": "previous_platform"
}
FieldMeaning
previous_platform_originsUp to ten https:// origins of the previous platform: a public hostname only, no path, port, IP address or user information. Internationalized hostnames are stored in their ASCII (xn--) form
previous_url_templateThe previous platform's page for a sticker without its own previous_url. Placeholders {code}, {station_name} and {charger_id} go in the path or query only, and their values are percent-encoded
default_routeWhere a charger with no recorded move sends drivers: previous_platform while you migrate, ivora for chargers that were never anywhere else

Saving these settings turns scans on for your project. Until then, Ivora's scan URL shows the same neutral "not active" page for every code, without looking up a charger or counting a scan. A ready QR domain answers scans on its own.

Every destination is checked when you save it (422 invalid_destination) and again on every scan. A change that would strand imported stickers whose own previous_url uses a removed origin is refused with 409 destination_in_use.

3. Import your stickers

POST /v1/tenants/{tenant_id}/qr/stickers/import (stations:write) with {"format": "csv", "content": "<the file>", "replace": false, "dry_run": true}. Run it with dry_run first: the import is all or nothing, and a refused file answers 422 invalid_import with every refused line and why.

code,station_id,station_name,charger_id,evse_id,evse_number,previous_url,label
C-1042,,,CP1042,,,,Garage bay 4
https://pay.example.com/c/1043,,LOT-B-01,,,2,,
s/77?cp=9,1201,,,,,https://app.oldvendor.com/legacy/77,
X9,,,,US*ABC*E1044*1,,,
ColumnMeaning
code, url or path (one per line)What the sticker encodes. A full URL keeps its path and query; leading and trailing slashes do not matter. Codes match regardless of letter case, because QR codes often encode a URL in capitals
station_id, station_name, charger_id or evse_id (one per line)The charger: its station ID, station name, per-project charger ID, or the EVSE ID imported from your previous platform (OCPI, such as US*ABC*E1044*1)
evse_numberOptional: the connector the sticker is on. Without it, a charger with one EVSE goes straight to it; several show a choice
previous_urlOptional: this sticker's own page on the previous platform (on previous_platform_origins) instead of the template
labelOptional note, up to 120 characters

An import updates stickers it already holds (by code) and adds new ones; replace: true also removes the ones the file no longer names. At most 5,000 lines per file. GET /v1/tenants/{tenant_id}/qr/stickers lists them and DELETE .../qr/stickers/{sticker_id} removes one.

Formats known so far:

  • CSV as above, the baseline: export your previous platform's sticker or station list and rename its columns.
  • Sticker URL lists (the print job): put each URL in the url column.
  • EVSE-ID stickers: use evse_id once your EVSE IDs are imported.
  • Charger-ID stickers: no import needed. A code that is a station name or a per-project charger ID resolves directly, as does an imported EVSE ID.

If your previous platform exports another format, send Ivora a sample with the identifiers removed and map it to this CSV in the meantime.

4. Moving and rolling back chargers

You do not call anything when a charger moves. Ivora's migration team moves your chargers with the migration proxy, which records each move with Ivora at once: a whole site (the same move that sends you site.flipped and site.flip_rolled_back, see Moving your payment service to Ivora) or one charger at a time. Each charger's latest recorded move is what scans follow; an older report that arrives late never replaces a newer one.

GET /v1/tenants/{tenant_id}/qr/charger-routes shows where each charger connects now (ivora or previous_platform, with when and how it was recorded); a charger not listed follows default_route. To check a sticker without counting a scan, call GET /v1/tenants/{tenant_id}/qr/resolve?code=C-1042: it returns the outcome and the destination a scan would get now.

A scan redirects with 302 and Cache-Control: no-store, never a permanent redirect, so phones and browsers never keep an old destination after a move.

5. Keep your own sticker page (website lookup)

If your stickers open a page on your own website, such as https://www.example.com/charge?stationId=XYZ123, you can keep that page and let it ask Ivora whether the charger moved, instead of redirecting every scan to Ivora:

  • Moved: the page sends the driver on to the Ivora checkout for that charger.
  • Not moved, or a code Ivora does not know: the page stays exactly as it is today. Chargers that have not moved never depend on Ivora, and there is no redirect loop.

GET /v1/qr-lookup/{tenant_id}?code=XYZ123 is public and read-only (no key). GET /v1/tenants/{tenant_id}/qr/settings returns it as lookup_url. It answers only this:

{"moved": true, "url": "https://payment.example.com/checkout/CP1042-1", "choices": []}
FieldMeaning
movedtrue: send the driver to url. false: stay on your page
urlThe Ivora checkout page of the sticker's charger and connector; null when not moved, or when the driver picks a connector
choicesWhen the charger moved, has several connectors and the sticker names none: each connector's evse_number and checkout url. Pass &evse=2 to choose one directly
  • Only codes in your imported sticker list answer moved: true: import each sticker with the code your page reads (XYZ123 above) in the code column. A charger ID or an EVSE ID that is not imported as a sticker answers moved: false, as does any unknown code, in the same time, so the lookup cannot be used to find codes. Nothing else about the charger or your project is returned.
  • CORS: browsers let only your own website read the answer. Ask Ivora to add your website's origin (such as https://www.example.com, hostname only) to website_origins in your QR settings; Ivora checks that you run that website first. You can remove an origin yourself with PUT /v1/tenants/{tenant_id}/qr/settings; leaving website_origins out keeps the stored list. Each change is in your project's audit log as qr.website_origins. GET only, never with cookies or credentials.
  • Limits: 30 lookups a minute from one address and 3,000 a minute per project (429 rate_limited with Retry-After beyond that). Answers carry Cache-Control: private, max-age=10, so a move shows within seconds.
  • Each lookup counts in scan counts as website_lookup_moved or website_lookup_stay.

The page script

Paste this on the page your stickers open, and set the three values at the top: replace {lookup_url} and {scan_url_template} with those two values from GET /v1/tenants/{tenant_id}/qr/settings, and set PARAM to the query parameter your stickers carry the code in. It is plain JavaScript with no dependencies.

<script>
(function () {
  var LOOKUP = "{lookup_url}";      // lookup_url from your QR settings
  var SCAN = "{scan_url_template}"; // scan_url_template from your QR settings, with its {code}
  var PARAM = "stationId";  // the query parameter that carries the sticker's code
  var TEXT = "Paying at a charger that moved to our new system? Tap here";
  var query = new URLSearchParams(window.location.search);
  var code = (query.get(PARAM) || "").trim();
  var evse = query.get("evse") || "";
  var settled = false;
  if (!code) return;

  function show(links) {
    function add() {
      var box = document.createElement("div");
      box.style.cssText = "margin:16px auto;max-width:480px;padding:12px;font:16px/1.4 sans-serif;text-align:center";
      links.forEach(function (link) {
        var a = document.createElement("a");
        a.href = link[0];
        a.textContent = link[1];
        a.style.cssText = "display:block;padding:8px";
        box.appendChild(a);
      });
      document.body.insertBefore(box, document.body.firstChild);
    }
    if (document.body) add(); else document.addEventListener("DOMContentLoaded", add);
  }

  // Stay on this page. After an error or a timeout, also offer Ivora's scan page for this code.
  function stay(failed) {
    if (settled) return;
    settled = true;
    if (failed) show([[SCAN.replace("{code}", encodeURIComponent(code)), TEXT]]);
  }

  if (!window.fetch) return stay(true);
  var abort = window.AbortController ? new AbortController() : null;
  var timer = setTimeout(function () { if (abort) abort.abort(); stay(true); }, 1500);
  var url = LOOKUP + "?code=" + encodeURIComponent(code) + (/^[0-9]{1,6}$/.test(evse) ? "&evse=" + evse : "");
  fetch(url, { credentials: "omit", signal: abort ? abort.signal : undefined })
    .then(function (response) {
      if (!response.ok) throw new Error("lookup " + response.status);
      return response.json();
    })
    .then(function (answer) {
      clearTimeout(timer);
      if (settled) return;
      if (answer.moved && /^https:\/\//.test(answer.url || "")) {
        settled = true;
        window.location.replace(answer.url);
      } else if (answer.moved && answer.choices && answer.choices.length) {
        settled = true;
        show(answer.choices.map(function (c) { return [c.url, "Connector " + c.evse_number]; }));
      } else {
        stay(false);
      }
    })
    .catch(function () { clearTimeout(timer); stay(true); });
})();
</script>

It fails open: when Ivora does not answer within 1.5 seconds, or answers with an error, your page stays as it is and shows one small link, "Paying at a charger that moved to our new system? Tap here", to Ivora's scan page for that code, which sends the driver to the right checkout or the find-your-charger page.

  • Framer: open the page your stickers link to, then Page settings → Custom Code → End of <body> tag, and paste the script. For a page with a path variable instead of a query parameter (such as /charge/XYZ123), replace the query.get(PARAM) line with var code = decodeURIComponent(window.location.pathname.split("/").pop());. Publish, then test with a moved and a not-moved sticker.
  • Plain HTML: paste the script just before </body>. If your site sends a Content-Security-Policy, allow Ivora's API host in connect-src and the inline script (or move it into a file on your own host).

To test before a move, call the lookup for a sticker you imported (moved: false), then preview the scan with GET /v1/tenants/{tenant_id}/qr/resolve.

Safety and limits

  • No open redirects: a scan only ever goes to the Ivora checkout origin (your checkout_domain while it is ready, else Ivora's payment host) or one of your previous_platform_origins. Nothing in the scanned URL can name a destination. Backslashes, //, user information (@), ports, fragments, control characters and lookalike internationalized hostnames are refused, and hostnames are compared in lower-case ASCII form.
  • Rate limits: 30 scans a minute from one address and 3,000 a minute per project. Beyond that the driver sees a "too many scans" page with Retry-After.
  • Privacy: scans are counted per project, charger, outcome and day. No address, device, cookie or scanned text of an unknown code is stored or logged. HEAD requests (link previews and checkers) get the same answer but are not counted.

Scan counts

GET /v1/tenants/{tenant_id}/qr/scans?from=2026-10-01&to=2026-10-21&station_id=1201 (stations:read; UTC days, at most 92) returns totals and daily counts per charger and outcome:

OutcomeMeaning
ivoraSent to Ivora checkout (or the connector choice)
previous_platformSent to the previous platform
unknownCode not recognized: the find-your-charger page
unavailableCharger known, but no allowed destination (no previous platform URL configured, or a destination no longer allowed)
rate_limitedRefused by the scan or website lookup limits
website_lookup_movedYour website's lookup: the charger moved, the page sends the driver to Ivora checkout
website_lookup_stayYour website's lookup: not moved or an unknown code, the page stays

A rising unknown count after a launch usually means a sticker format the import does not cover yet; unavailable means a setting to fix.