Guide
API guide
Start here
| If you want to | Go to |
|---|---|
| Read every endpoint and field | the generated reference |
| Generate a client, or import into Postman | /api/openapi/v1.json |
| See a first call end to end | the quickstart |
The OpenAPI document is generated from the code rather than maintained separately, so it cannot drift from what the API actually does. Postman imports it directly — Import → Link — and gives you every request with its parameters already filled in.
No try-it console, deliberately
Authentication
Two credentials, two audiences. They are never interchangeable.
| Credential | Header | Used by |
|---|---|---|
ptm_live_… / ptm_test_… | X-Api-Key | Partner systems |
| Portal JWT | Authorization: Bearer … | The web portal — read-only, and manages credentials |
One key covers your whole account. If you operate an OMC and two depots, that is one credential, not three — and you never tell us which company is acting. The order already records its OMC, its BDC and its depot, so we read the actor off the work.
A key's permissions are your PetroMon licence types, and holding one lets the key do everything that licence may do:
OMC raise orders, read yours
BDC approve, decline or cancel orders, custody transfers
DEPOT depot approval, vehicle checks, loading, marking, cancelling, custody transfersThe portal offers only the licences your account actually holds. Asking for one it does not hold is refused when the key is issued, rather than granted and quietly ignored.
Holding the licence is not enough on its own
Raising an order is the one call that can be ambiguous
omcCode; omitting it is 422 ambiguous_actor, which lists yours, rather than a guess that raises the order in the wrong company's name.Why the portal can't drive stages
Keys issued before the rename still work
erd_test_ / erd_live_ rather than ptm_…. They keep working indefinitely — there is nothing to rotate and no deadline.Rate limits
Limited per credential, not per connection — spreading calls across hosts doesn't buy headroom.
| Caller | Limit | Window |
|---|---|---|
| API key | 240 requests | 1 minute |
| Portal session (per user) | 600 requests | 1 minute |
| Unauthenticated (login, registration) | 30 requests | 1 minute |
Honour Retry-After
429 with a Retry-After header. The gateway shares a database with a live regulatory system, and the limit exists so one partner's retry loop doesn't become everyone's outage — not to price access. Back off on the header rather than blindly.Rules the schema can't state
Four things the reference will not tell you, each of which produces a working-looking integration that is quietly wrong.
1. Codes, never ids
PetroMon's internal ids are never accepted and never returned. Everything is named by a code resolved from /api/v1/reference — bdcCode, depotCode, productCode, outletCode, brvNumber. Nothing you store can be invalidated by a renumbering inside PetroMon. A code that matches nothing is 422 unknown_code, naming the field.
Orders included: a stage call names the order in the path and carries no id in its body. A decline or cancellation names its reason the same way, by reasonCode from /api/v1/reference/reasons.
Reference lists carry only what you can use: deactivated companies and inactive products are absent rather than flagged, so anything returned can be ordered.
2. Compare statuses as names, never numbers
Statuses arrive as names — ordered, bdc_approved, good_standing, depot_approved, brv_checked, loaded, marked, released.
The underlying integers are not ordered by progress
released. Any ordering or range comparison on them eventually reads a cancelled order as delivered fuel. Compare the status name instead.3. Each stage belongs to one party
The API refuses a stage your partner type does not drive, with actor_not_permitted. One integration cannot walk an order end to end.
| Stage | Driven by | status |
|---|---|---|
| Create the order | OMC | — |
| Cancel the order, before BDC approval | OMC | ppmc_cancelled |
| BDC approval | BDC | bdc_approved · bdc_declined · bdc_cancelled |
| Depot approval | Depot | depot_approved · depot_cancelled |
| Vehicle check | Depot | brv_checked · depot_cancelled |
| Loading | Depot | loaded |
| Tagging | Marking company, or the depot | tagged (the order stays at brv_checked) |
| Marking | Marking company, or the depot | — |
| Custody transfers | Depot only | — |
Stage requests carry a status name — the status the order should end up in. The gateway turns it into PetroMon's number, so you never handle one. A decline or cancellation needs a comment and a reasonCode from /api/v1/reference/reasons, ends the order for good, and gives back any stock the BDC's approval reserved. Any other name is invalid_status, refused before the order is read.
Every name the stage endpoints accept, and nothing else. Case does not matter:
| status | Send it to | What it does |
|---|---|---|
bdc_approved | approve | BDC approves; the volume is reserved |
bdc_declined | approve | BDC declines |
bdc_cancelled | approve | BDC cancels |
depot_approved | depot-approve | Depot manager approves |
depot_cancelled | depot-approve, check-brv | Depot cancels |
brv_checked | check-brv | Vehicle checked |
loaded | load | Loading recorded |
tagged | tag | Tag recorded — the order does not move; it stays brv_checked |
ppmc_cancelled | cancel | OMC cancels its own order, before the BDC approves |
tagged is the one name that is not a status: tagging leaves the order where it is. PetroMon's PPMS cancellation, after loading, is deliberately not accepted — it would give back stock that has already left the depot.
Names in, names out
invalid_stage_transition when approving, cancellation_not_allowed when declining — because the name was fine and the order was not where it needed to be.Reason codes arrive with a PetroMon release
Everything above is live. Release is not Not yet live — there is no route for it, so it answers 404 and is absent from the OpenAPI document rather than published and broken. It computes UPPF levies against a price window; it is not a status change on its own.
4. The volumes have to agree
On a road movement the vehicle's capacity replaces the volume you send — it is not a suggestion. Two sums are then checked, and both must be exact:
delivery lines = the vehicle's capacity
tanks assigned to an outlet = that outlet's lineEither mismatch is invalid_volume, and the message names the outlet and both figures. Tank numbers come from compartments on /api/v1/reference/brvs. A worked example, with codes that resolve today, is in docs/API-EXAMPLES.md.
Compartments decide what can be marked
Error codes
Branch on errorCode; the message is for logs. Every error carries a requestId that traces the call end to end.
| Code | Meaning | HTTP |
|---|---|---|
unauthorized | Missing or invalid credential | 401 |
ambiguous_actor | You hold several OMCs; name one with omcCode | 422 |
actor_not_permitted | You hold no licence that drives this stage, or none of your companies is this order's party | 403 |
not_a_party | Your company is not on this order | 403 |
order_not_found | No such order, or outside your scope | 404 |
unknown_code | A code matched nothing; the field is named | 422 |
validation_failed | Malformed request | 400 |
duplicate_order_number | Already used by this OMC | 409 |
invalid_stage_transition | Order is not at a valid prior stage | 409 |
invalid_status | status is not one this stage endpoint accepts | 422 |
cancellation_not_allowed | That decline or cancel code is not allowed from the order's status | 422 |
comment_required | A decline or cancellation needs a comment | 422 |
reason_required | A decline or cancellation needs a reasonCode | 422 |
product_not_found | The order's product is no longer in PetroMon's product list, so it cannot move | 422 |
reason_not_found | reasonCode is not in /reference/reasons, or this environment has no reason codes yet | 422 |
concurrent_modification | Someone else changed it first | 409 |
stock_lockdown_active | Stock-taking in progress | 423 |
product_not_permitted | OMC not authorised for this product | 403 |
invalid_volume | Lines or tanks do not add up | 422 |
brv_in_transaction | Vehicle is already on a live order | 422 |
insufficient_marker | The can does not hold what was asked of it | 422 |
rate_limit_exceeded | Too many requests | 429 |
404 rather than 403
404, not 403 — otherwise the response would confirm that someone else's order exists.Webhooks
One endpoint per environment, HTTPS only. Posting again re-points the existing one and leaves the secret alone — rotate to change it. Deleting disables rather than removes, so the delivery history survives.
Failed deliveries retry with backoff and dead-letter after eight attempts. A replay creates a new delivery rather than resetting the old one, so the record of what you were and weren't told stays intact.
Verify the signature before trusting a payload
X-Petromon-Signature — HMAC-SHA256 of the raw body under your endpoint secret — and X-Petromon-Timestamp. Compare in constant time, and reject anything older than five minutes. An unverified webhook endpoint is a way for anyone who learns the URL to tell you fuel moved when it did not.