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.
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 encodes | What to do | Reprint? |
|---|---|---|
A hostname you control, such as https://pay.example.com/c/1042 | Point that hostname at Ivora (below). Every existing path keeps working | No |
A path on your own website, such as https://www.example.com/charge/1042 | Keep your website and redirect that path prefix (302) to Ivora's scan URL with the same code, or move a dedicated hostname | No |
Your previous vendor's own domain, such as https://app.oldvendor.com/s/1042, or its short-link service | You 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 Ivora | Yes |
| Only the charger ID as text | Drivers type it on the find page of your QR hostname or Ivora's scan URL | Optional |
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:
POST /v1/tenants/{tenant_id}/qr-domainswith{"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.- Add both at your DNS provider. Keep the address record DNS-only (not proxied by a CDN): Ivora issues the hostname's certificate itself.
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 becomesactiveandready.
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"
}
| Field | Meaning |
|---|---|
previous_platform_origins | Up 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_template | The 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_route | Where 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,,,
| Column | Meaning |
|---|---|
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_number | Optional: the connector the sticker is on. Without it, a charger with one EVSE goes straight to it; several show a choice |
previous_url | Optional: this sticker's own page on the previous platform (on previous_platform_origins) instead of the template |
label | Optional 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
urlcolumn. - EVSE-ID stickers: use
evse_idonce 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": []}
| Field | Meaning |
|---|---|
moved | true: send the driver to url. false: stay on your page |
url | The Ivora checkout page of the sticker's charger and connector; null when not moved, or when the driver picks a connector |
choices | When 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 (XYZ123above) in thecodecolumn. A charger ID or an EVSE ID that is not imported as a sticker answersmoved: 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) towebsite_originsin your QR settings; Ivora checks that you run that website first. You can remove an origin yourself withPUT /v1/tenants/{tenant_id}/qr/settings; leavingwebsite_originsout keeps the stored list. Each change is in your project's audit log asqr.website_origins.GETonly, never with cookies or credentials. - Limits: 30 lookups a minute from one address and 3,000 a minute per
project (
429 rate_limitedwithRetry-Afterbeyond that). Answers carryCache-Control: private, max-age=10, so a move shows within seconds. - Each lookup counts in scan counts as
website_lookup_movedorwebsite_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 thequery.get(PARAM)line withvar 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 aContent-Security-Policy, allow Ivora's API host inconnect-srcand 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_domainwhile it is ready, else Ivora's payment host) or one of yourprevious_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.
HEADrequests (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:
| Outcome | Meaning |
|---|---|
ivora | Sent to Ivora checkout (or the connector choice) |
previous_platform | Sent to the previous platform |
unknown | Code not recognized: the find-your-charger page |
unavailable | Charger known, but no allowed destination (no previous platform URL configured, or a destination no longer allowed) |
rate_limited | Refused by the scan or website lookup limits |
website_lookup_moved | Your website's lookup: the charger moved, the page sends the driver to Ivora checkout |
website_lookup_stay | Your 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.



