This story is about the world of EV charging. Now, I don't work in EV charging (although I do have a Nissan Leaf named Bedivere, as in 'outta here like Bedivere'), but the fun part of building Validibot is looking at different industries and imagining how a data validation helper might provide some real 'gosh thanks for checking that' moments.
I'll use this example to show a couple of very basic features in Validibot that are easy to overlook:
- cross-field rules: small checks that compare one field to another, or to a value you expect.
- extra-data: a way for the submitter to attach a bit of extra information alongside the original data.
The setup, in plain terms
When you charge an electric car, you might be using an app from one company but plugging into a charger owned by another. This situation is similar to phone roaming: the two different companies settle up behind the scenes. The shared language they use to pass data back and forth is called OCPI.1 1 OCPI (Open Charge Point Interface) is an open protocol maintained by the EVRoaming Foundation. The two main roles: the CPO (owns the chargers) and the eMSP (has the driver as a customer).
After each session, the charger company produces a small itemised receipt and sends it over to the app company so the driver can be billed. The official name for that receipt is a CDR, or Charge Detail Record. The OCPI spec calls the CDR "the only billing-relevant object" and once a CDR is sent, it can't be edited. If it's wrong, the sender doesn't fix it; they issue a separate "credit" record to claw the mistake back.2 2 OCPI 2.2.1, CDR module: a CDR is "the only billing-relevant object" and "cannot be changed or replaced once sent." Corrections are a new Credit CDR, not an edit.
So OCPI is the pipe, not the referee. It moves the receipt from one company to another. Deciding if the numbers inside the receipt are right is the job of the company that sends it and the company that receives it. That part was never meant to be OCPI's job.
Validating that data before it's sent is worth doing, as a wrong-but-well-formed receipt could be expensive. A 2025 US Department of Energy report on OCPI looked at this and recommended the industry "develop semantic validation tools" — tools that check whether a receipt is sensible, not just well-formed.3 3 ChargeX OCPI Recommendations, DOE / Idaho National Laboratory, June 2025, report PDF (§3.1.2 and §3.4.2).
Perhaps Validibot can help! Let's look at an example piece of data and then see how we can set up a Validibot workflow to validate it.
What "valid but wrong" looks like
A CDR is a JSON document. Here's a trimmed one, with enough parts to work for this story:
A charge detail record (simplified)
{
"id": "CDR-2026-0042",
"start_date_time": "2026-06-14T09:50:00Z",
"end_date_time": "2026-06-14T10:50:00Z",
"currency": "EUR",
"total_energy": 30.0, // kWh delivered
"total_energy_cost": { "excl_vat": 13.50 }, // <-- implies 0.45 / kWh
"total_cost": { "excl_vat": 13.50, "incl_vat": 14.85 },
"charging_periods": [
{ "tariff_id": "TARIFF_DAY_2026Q1", // <-- last quarter's tariff
"dimensions": [ { "type": "ENERGY", "volume": 30.0 } ] }
]
}
Every field in the above example is the right type, so this data passes a format check. And yet, if the deal with this partner was 0.40 per kWh on Q2's tariff, two things here are quietly wrong. Let's build a workflow in Validibot that uncovers the issue.
The Validibot workflow, in layers
Validibot is a no-code environment that helps you set up validation 'workflows'. A workflow is an ordered set of steps a data submission passes through. Most workflow steps will check data, but some might do other things like award a verifiable credential (VC) for data that passed all checks.4 4 New to verifiable credentials? We wrote a whole post on what they are and why a tamper-evident "this data was checked" stamp is worth having: Is that data valid? Prove it.
Here's how we can set up a workflow to check our charging data...
Step 1: Is it a well-formed CDR?
The first step is a JSON Schema validator. It does an
unglamorous but vital check: are the required fields present, are the
types right, is the currency a three-letter code, are the timestamps real
timestamps? You need to provide a schema to use this validator, so here's a
trimmed schema for our demo receipt:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "OCPI 2.2.1 CDR (demo subset)",
"required": ["id", "start_date_time", "end_date_time", "currency", "total_energy", "total_cost", "charging_periods"],
"properties": {
"id": { "type": "string", "maxLength": 39 },
"start_date_time": { "type": "string", "format": "date-time" },
"end_date_time": { "type": "string", "format": "date-time" },
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
"total_energy": { "type": "number" },
"total_energy_cost": {
"type": "object",
"required": ["excl_vat"],
"properties": { "excl_vat": { "type": "number" }, "incl_vat": { "type": "number" } }
},
"total_cost": {
"type": "object",
"required": ["excl_vat"],
"properties": { "excl_vat": { "type": "number" }, "incl_vat": { "type": "number" } }
},
"charging_periods": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["dimensions"],
"properties": {
"tariff_id": { "type": "string", "maxLength": 36 },
"dimensions": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["type", "volume"],
"properties": {
"type": { "type": "string", "enum": ["ENERGY", "TIME", "PARKING_TIME"] },
"volume": { "type": "number" }
}
}
}
}
}
}
}
}
This schema is for the demo only. OCPI doesn't publish an official JSON Schema, so I created this from the 2.2.1 spec, covering just the fields in our example. If you want a fuller, real-world starting point, there are community-maintained schemas like solidstudiosh/ocpi-schema.
The workflow now has a JSON schema step, which in itself is really useful, and can catch lots of issues with incoming data.
However, our example for this blog post passes the JSON Schema check without issue. Which is exactly the point: the problem is deeper than shape.
Step 2: Do the numbers agree with each other?
Next, we'll check the receipt against itself with
CEL assertions — small
true/false rules that read the submitted file through the payload.
("payload") namespace and need no outside information:
Self-consistency rules
payload.end_date_time > payload.start_date_time // can't end before it starts
payload.total_cost.incl_vat >= payload.total_cost.excl_vat // tax can't make it cheaper
We add a second "Basic" validator step, and then double-click into that step to add a couple of CEL assertions to represent our rules.
These CEL assertions catch some data issues that our JSON Schema can't. That's super useful, but still doesn't catch the fundamental problems with price and tariff that we noted above. The bill doesn't match reality. We need some information from outside the core data.
Step 3a: Does it match the deal? (the agreed values)
Some facts in our scenario are stable, like the agreed price per kWh with this partner, or the
currency the deal is in. They don't change from one receipt to the next, so
for now I just write them straight into the rule as the values to check
against.5
5 I'm currently building a "Constants" feature
where static values can be defined by the author and then
used in CELs throughout the workflow, such as
constants.PRICE_PER_KWH instead of hard-coding 0.40
in the CEL expression.
Compare the receipt to the agreed deal
payload.currency == "EUR"
abs(payload.total_energy_cost.excl_vat - payload.total_energy * 0.40) <= 0.01 // a cent of slack for rounding
That second rule fails, and notice it isn't a naive exact match. The
abs(...) <= 0.01 gives a cent of slack so honest rounding
doesn't trip a false alarm.6
6 Real billing rounds energy and time up in fixed steps.
At the agreed 0.40, 30 kWh should cost 12.00 before tax; the receipt says 13.50 — really 0.45 per kWh. That's 1.50 off, miles beyond any rounding wiggle. So that's kind of like a "dinner price on a lunch order" problem, caught by our workflow. The receipt was format-perfect, but the rate check found the issue.
Step 3b: Does it match this session? (extra data)
Other facts aren't stable. They're specific to this one session, and only the submitter knows them. For example, the price the driver was actually shown on the screen, or which tariff was in effect at that moment. Neither is reliably inside the CDR.
For per-file facts like these, Validibot lets
whoever submits the file attach a small bundle of extra data
alongside it, read through the submission. namespace.7
7 This is the submission's metadata: a small
JSON object attached at submission time, referenced in a rule as
submission.metadata.<key>.
Extra data attached to this submission
{
"displayed_total": 12.00, // what the driver saw on screen
"active_tariff_id": "TARIFF_DAY_2026Q2" // the tariff actually in effect
}
And the rules:
payload.total_cost.incl_vat == submission.metadata.displayed_total
payload.charging_periods.all(p, p.tariff_id == submission.metadata.active_tariff_id)
Both assertions fail. The driver was shown 12.00 but the bill came to 14.85; and the charging period quotes last quarter's tariff when this quarter's was in force.
The 3a scenario shows how an agreed value that never changes can live right in the rule, while the 3b scenario shows how extra data is for the per-file facts the submitter hands in each time.
Both demonstrate the same idea: comparing the submitted data file to something true that isn't inside it...the only question is whether that truth is fixed or arrives with each submission.
And then: a signed result
When every layer passes, Validibot can issue a signed VC that says this exact receipt was checked against these exact rules at this time, and passed. The useful part for billing is that the stamp carries only fingerprints of the data, not the data itself, so the charger company can hand it to a partner as proof without exposing its pricing or its customers.
Could you build this today?
Mostly yes. The JSON Schema step, the CEL rules, and the extra-data
mechanism all exist now in the community edition; signed credentials are in
Pro. Two honest caveats. A few checks want to add up a list (say,
the energy across charging periods summing to the total) — comparing values
and "every item must…" checks are easy, but cleanly summing a list is a
small helper I'd add rather than pretend is a one-liner. And you'd point the
workflow at the right OCPI version, since the price shape changed between
2.2.1 and 2.3.0.8
8 In 2.2.1 a price is excl_vat/incl_vat
(used above); in
2.3.0
it became before_taxes plus an itemised taxes[]
list. A validator has to know which it's reading.
Why I think it's a nice fit
Pricing and customer billing data can be sensitive. Nobody wants to ship that to a third-party cloud to get it checked. Validibot runs on your own servers, so the receipts never leave the building; the only thing you'd ever share is that fingerprint-only signed verifiable credential.
We've used EV charging as an example, but really "check a file against a stable agreement (values baked into the rule) and against facts supplied with it (extra data), then sign the result" is the same shape whether the file is a charging receipt, an insurance message, or a lab result. Charging just happens to be a place where the gap between valid and correct is measured directly in money.
But again, I'm an outsider having a curious poke at this, so if you work in EV roaming and I've got something wrong, I'd honestly love to hear it. Get in touch.
Until then: don't trust the bill just because the columns add up. They charged you the dinner price. Check it against what you actually ordered.