Quickstart
Integrate with PetroMon
Where to start
API guide
The rules a schema cannot state — who drives which stage, and why volumes must agree.
Reference
Every endpoint and field, generated from the code so it cannot drift.
OpenAPI 3
Import into Postman, or generate a client in your own language.
If you are here to build, keep reading. The steps below take about twenty minutes and end with events arriving at your own endpoint.
1Get an account
Registration takes your PetroMon account reference — 00000009, CMP-08468222 — and nothing else. Not a company code: the account is what owns companies, and one registration onboards every company trading under it.
Go to /register. It shows you the masked contact address PetroMon holds, you type it back in full, and the activation link goes to that address. Open it to set your password.
You never choose where the link goes
2Create a key
Sign in and create a key under API keys. Its permissions are your PetroMon licence types — OMC, BDC, DEPOT — and every licence your account holds is ticked by default, because one key covers every company under the account. Which company acts is read off the order, so you never send it.
Choose an expiry while you are there: 1, 15 or 30 days, a date of your own, or never.
The secret is shown once
3Your first call
Every request carries the key in an X-Api-Key header. List the orders your company is a party to:
curl "/api/v1/orders?createdFrom=2026-09-01&createdTo=2026-09-30&pageSize=10" \
-H "X-Api-Key: ptm_live_..."createdFrom and createdTo are required, and span at most 366 days — the list reaches back through every order you have ever raised, so it asks which window you mean rather than guessing. Narrow further with status, type and search. Everything is paged.
Building an order? One call gives you the whole thing — the vehicle's capacity, its tanks, and every product code valid on it:
GET/api/v1/reference/brvs/{vehicleNumber}4Follow an order
Statuses are always names. The lifecycle runs in this order:
ordered → bdc_approved → good_standing → depot_approved
→ brv_checked → loaded → marked → releasedTerminal states sit outside that sequence: cancelled, bdc_declined, bdc_cancelled, depot_cancelled, ppms_cancelled, unserved.
Never map statuses back to numbers
released. Code like if (status >= 8) => delivered eventually reads a cancelled order as delivered fuel. Compare names.The timeline is the full history, including transitions nobody performed: GET/api/v1/orders/{orderNumber}/events
[
{ "status": "ordered", "at": "2026-07-23T10:13:20Z", "trigger": "user" },
{ "status": "bdc_approved", "at": "2026-07-23T10:13:56Z", "trigger": "user" },
{ "status": "good_standing", "at": "2026-07-23T10:13:59Z", "trigger": "auto" }
]Some stages are skipped, not delayed
"trigger": "auto". Code that waits for a status PetroMon skips waits forever — drive off the timeline, not off an expected next status.5Receive events
Register an HTTPS endpoint under Webhooks in the portal. We push every transition rather than you polling for it, and show you every delivery attempt.
| Header | Purpose |
|---|---|
X-Petromon-Signature | t=<unix>,v1=<hex> — HMAC-SHA256 |
X-Petromon-Timestamp | When it was signed |
X-Petromon-Event-Id | Stable id — use it to de-duplicate |
Verify the signature before trusting the body
We sign <timestamp>.<raw body> with your endpoint secret using HMAC-SHA256, and send it as t=<unix>,v1=<hex>. Two details catch people out: the timestamp is part of what is signed, and it has to be the raw body — parsing the JSON and re-serialising it changes the bytes, and the signature will never match. Copy this:
import crypto from "node:crypto";
// Mount the RAW body. express.json() re-serialises it, and the bytes
// would no longer be the ones that were signed.
app.post("/petromon/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Petromon-Signature") ?? ""; // t=1760000000,v1=9f86d0…
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const body = req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", process.env.PETROMON_WEBHOOK_SECRET) // whsec_…
.update(`${parts.t}.${body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300; // five minutes
const signed =
parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!fresh || !signed) return res.sendStatus(400);
res.sendStatus(200); // acknowledge first — we retry on anything else
handle(JSON.parse(body)); // then do the work
});Compare in constant time, reject anything older than five minutes, and answer 2xx before doing the work — we retry on anything else, and a slow handler looks the same to us as a failed one.
That is the whole loop. Every rejection along the way carries a stable errorCode to branch on and a requestId to quote — both listed in the API guide, along with the rules that decide who may drive each stage.