Quickstart

Integrate with PetroMon

Five steps from nothing to receiving live lifecycle events. Everything runs against your own company's data, scoped automatically — you only ever see orders you are a party to.

Where to start

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

An account reference appears on paperwork, so it is not a secret. Sending the link only to the contact PetroMon already holds is what makes registration prove you can read that mailbox — and it is why knowing a reference is not enough to claim somebody else's companies.

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

Only a hash is stored, so a key cannot be recovered — if you lose one, revoke it and create another. A forgotten portal password is different: reset it at /forgot-password and your keys keep working, because they authenticate systems rather than people.

3Your first call

Every request carries the key in an X-Api-Key header. List the orders your company is a party to:

bash
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:

text
ordered → bdc_approved → good_standing → depot_approved
        → brv_checked → loaded → marked → released

Terminal states sit outside that sequence: cancelled, bdc_declined, bdc_cancelled, depot_cancelled, ppms_cancelled, unserved.

Never map statuses back to numbers

PetroMon stores these as integers, and those integers are not ordered by progress — cancellations sit numerically above 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

json
[
  { "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

PetroMon advances pipeline orders straight past the BRV check, and non-markable products straight past marking. Those arrive with "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.

HeaderPurpose
X-Petromon-Signaturet=<unix>,v1=<hex> — HMAC-SHA256
X-Petromon-TimestampWhen it was signed
X-Petromon-Event-IdStable id — use it to de-duplicate

Verify the signature before trusting the body

An unverified endpoint lets anyone who learns the URL tell you fuel moved when it did not. Rotating a secret keeps the previous one valid for 24 hours, so you can deploy the new one without dropping anything.

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.