Skip to main content

Public Verification

Three calls let someone confirm that a document GoRoute issued is real, and read back what it says, without an API key. They exist so that a person holding a printed receipt or a scanned QR code can check it for themselves โ€” a customer, an auditor, an inspector โ€” none of whom has an account with you.

All paths below are relative to https://app.goroute.ai/peppol-api. None of the three takes an Authorization header, an API key or a session. There is no organisation filter on any of them.

CallWhat it answers
GET /api/v1/public/verify/{transaction_id}Does this transaction exist, and what does our record say about it?
GET /api/v1/public/verify/{transaction_id}/downloadGive me the original invoice XML.
GET /api/v1/public/verify-receiptDoes a receipt with this reference and this total exist, and what was on it?
A transaction ID is a credential. Treat it like a password

Anyone who has a transaction ID can read that transaction's record and download the original invoice XML, with no key from GoRoute and no permission check of any kind. That is deliberate: it is what makes a QR code on a piece of paper checkable by the person holding the paper. The ID is a random UUID, so it cannot be guessed and the range cannot be walked.

But it means the ID carries the weight of a password for that one document. Do not put a transaction ID anywhere a password would not go:

  • not in a URL you publish, print outside the receipt, or share in a message,
  • not in a support ticket or a bug report,
  • not in application logs you ship to a third party,
  • not in a referrer-leaking link, an analytics event or an error tracker.

Losing one exposes one invoice โ€” the parties, the amount and the full XML โ€” to whoever picked it up. It does not expose anything else in your account.

The receipt check, verify-receipt, works differently and is not covered by that warning: it needs two things off the same receipt and discloses nothing to someone holding neither.

Check a transactionโ€‹

GET /api/v1/public/verify/{transaction_id}

transaction_id is the UUID GoRoute returned when the document was sent. A value that is not a UUID is rejected with 422 before anything is looked up.

Returns the platform's record of the transaction, plus what the document itself says.

FieldWhat it is
idThe transaction ID you asked about.
verifiedAlways true in a 200 response โ€” a transaction that does not exist is a 404, not a false.
document_typeThe kind of document, for example an invoice or a credit note.
directionWhether GoRoute sent or received it.
statusWhere the transaction got to.
sender_peppol_id, receiver_peppol_idThe two Peppol addresses.
message_idThe network message identifier.
created_at, delivered_atWhen the transaction was recorded, and when it was delivered. Either can be absent.
document_numberThe document's own number, as written on it.
issue_dateThe document's own issue date.
currencyThe document's currency.
total_amountThe document's payable total, as a string.
seller_name, buyer_nameThe two party names off the document.
document_storage_keyInternal. See the note below.

Every field from document_type downwards can be absent or null. The document's own figures are read out of the stored record rather than out of the XML, so a transaction whose record never captured them returns nothing for them rather than failing.

Ignore document_storage_key

It is GoRoute's internal reference to where the document sits in storage, and it should not be in an unauthenticated response at all. It is not an address, it is not stable, and nothing you can call accepts it. Do not read it, store it or display it. That it is exposed here is filed with engineering as issue #768.

When it refuses

Statuserror_codeMeaning
404TRANSACTION_NOT_FOUNDNo transaction with that ID. The message reads "Transaction not found or has expired".
422โ€”transaction_id is not a well-formed UUID. A standard validation body, not an error_code.

Download the original documentโ€‹

GET /api/v1/public/verify/{transaction_id}/download
GET /api/v1/public/verify/{transaction_id}/download?inline=true

Gets the original XML for a transaction. No authentication, exactly as above โ€” so everything in the warning at the top of this page applies here with more force, because this call hands over the document itself rather than a summary of it.

Two modes:

  • Default. Answers a JSON object with download_url, a temporary link to the document in object storage; expires_in, which is 300 (five minutes); and content_type, which is application/xml. Fetch it promptly โ€” after five minutes the link stops working and you must call this endpoint again.
  • inline=true. Answers the XML itself, as application/xml, with Cache-Control: no-store. Use this from a browser page. A temporary storage link cannot always be fetched from a web page in another origin, and serving the bytes from the API avoids that problem entirely.

When it refuses

Statuserror_codeMeaning
404TRANSACTION_NOT_FOUNDNo transaction with that ID.
404DOCUMENT_NOT_STOREDThe transaction exists, but no document content was kept for it.
503DOCUMENT_STORAGE_NOT_CONFIGUREDDocument downloads are not available on that deployment at all. Not a fault in your request.
500DOWNLOAD_FAILEDThe temporary link could not be generated.
422โ€”transaction_id is not a well-formed UUID.

Check a printed receiptโ€‹

GET /api/v1/public/verify-receipt?reference={invoice reference}&total={gross total}

Checks a point-of-sale receipt against the two values printed on it. Both query parameters are required; omitting either is a 422 naming the one you left out.

ParameterWhat to pass
referenceThe invoice reference on the receipt. Case and surrounding spaces do not matter.
totalThe gross total charged, including VAT, as a decimal โ€” compared to three decimal places.
Why the total is required as well, and why we will not relax it

An invoice reference is sequential: the one above yours and the one below yours both exist. If a bare reference were enough, anyone could count through the range and read other customers' receipts โ€” names, amounts and all. Asking for the total as well costs the person holding the receipt nothing, because it is printed right there, and makes counting through the range useless.

For the same reason there is one refusal for both "no such reference" and "right reference, wrong total". Telling them apart would confirm that a reference exists, which is the thing being prevented.

A match must also be a receipt a point-of-sale endpoint actually wrote, and its seller must genuinely belong to the organisation whose record it is. A document posted by some other tenant under a seller name and VAT number of its choosing cannot answer for your reference and total, and cannot shadow the real receipt either.

What comes back

FieldWhat it is
verifiedtrue. A receipt that does not match is a 404.
referenceThe reference, normalised to upper case.
totalThe gross total, to three decimals.
currencyThe receipt's currency; OMR where the record does not say.
net_amount, vat_amountThe receipt's net and VAT figures.
sellerThe seller's trading name as printed.
seller_vatThe seller's VAT number.
branch, branch_idThe station or branch that made the sale.
tellerThe pump or till the sale came from.
serviceWhat was sold.
customerThe customer name, where the sale recorded one.
issued_atWhen the sale was recorded.
statusThe transaction's status.
invoice_uuidThe Invoice UUID โ€” BTOM-002 in the Oman architecture, identifying the business document.
seller_uuidThe Seller UUID โ€” BTOM-004, identifying the receipt. Read back out of the QR payload rather than from a separate field, so it is the same value the paper shows.
qr_tlvThe tax QR payload stored for the sale.
qr_imageThat payload rendered as a QR image, as a data: URI. Absent if it could not be rendered.
reporting, reporting_noteWhere the sale stands with tax reporting, in words.

Every field other than verified, reference and total can be absent or null, depending on what the sale recorded.

When it refuses

Statuserror_codeMeaning
404RECEIPT_NOT_VERIFIED"No receipt matches that reference and total." Covers both a reference we do not have and a total that does not match.
400INVALID_TOTALtotal is not a decimal amount. The message gives the expected shape.
422โ€”reference or total was not supplied.

The QR code on an Oman receiptโ€‹

An Oman point-of-sale sale comes back with two QR codes, described under The two QR codes. The second of them, qr.verify_image_data_uri, encodes qr.verify_url โ€” a link to a GoRoute web page with the receipt's values in the part of the URL after the #.

Among those values are the invoice reference and the gross total: the same two things verify-receipt matches on. What that web page does with them is a matter for the page, not for this documentation โ€” this page describes the API, and does not claim that scanning the printed QR code calls any of the three endpoints above.

If you are building your own "check this receipt" feature, use verify-receipt directly with the reference and the total. You do not need the link or the QR image to do it.