Skip to main content

Oman Retail and E-commerce Sales

When an Omani online store or shop counter takes a payment from a consumer, it issues a simplified tax invoice carrying a Fawtara QR code. Two endpoints do that: one for the sale, one for the refund. Both return the QR code and the invoice reference in the same response, so a store can show them on the order-confirmation page while the customer is still looking at it.

This is a consumer (B2C) path. A sale made through it is never transmitted over the Peppol network โ€” see What happens to the invoice afterwards. To invoice another business, use Send Invoice instead.

Where these endpoints are confirmed to exist

Both endpoints are present in the GoRoute test API and are documented here from the product source code. Availability in production is not stated on this page, because it could not be verified at the time of writing.

The endpointsโ€‹

MethodPathPurpose
POST/api/v1/pos/saleA customer has paid. Issue the simplified tax invoice and return its QR code.
POST/api/v1/pos/refundA customer has been refunded. Issue a credit note against the original sale.

Both require the invoices:create permission and the create_invoice feature. If the feature is switched off for your organisation the call is refused with HTTP 403 and the message Feature 'create_invoice' is not enabled for this organization.

Authenticate with your API key in the X-API-Key header, as everywhere else on the API.

Issue a saleโ€‹

curl -X POST "https://app.goroute.ai/peppol-api/api/v1/pos/sale" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"seller_name": "Muscat Coffee Traders LLC",
"seller_vat": "OM1100017479",
"order_reference": "WC-10432",
"issued_at": "2026-08-20T13:41:22+04:00",
"currency": "OMR",
"lines": [
{
"description": "Espresso beans 1kg",
"quantity": 2,
"unit_price": 12.500,
"tax_rate": 5,
"sku": "ESP-1KG"
}
],
"gross_amount": 26.250
}'

Required fieldsโ€‹

FieldMeaning
seller_nameYour registered business name.
seller_vatYour Omani VAT identification number, for example OM1100017479.
order_referenceYour own order number, for example WC-10432. Maximum 50 characters.
issued_atThe moment the customer paid, as a date and time.
linesThe cart. At least one line, at most 200.
gross_amountThe total including VAT that the customer actually paid. Read the totals rule before you fill this in.

Optional fields, and what they default toโ€‹

FieldDefaultNotes
currencyOMRThree-letter currency code.
channelwebWhere the sale happened: web, pos or app.
seller_scheme0248The identifier scheme for your VAT number. Four digits.
seller_cityMuscat
seller_streetempty
seller_countryOMTwo-letter country code.
customeromittedA name and an email, kept for your own records. If you supply a name it appears as the buyer name on the document; if you do not, the document is issued to Cash Customer. No Peppol address is needed either way.

Each cart lineโ€‹

FieldRequiredDefaultNotes
descriptionyesUp to 500 characters.
quantityyesMust be greater than zero.
unit_priceyesThe net price per unit, excluding VAT.
unitnoEAUnit of measure.
tax_rateno5Percentage, between 0 and 100.
skunononeYour stock code.

The totals rule (the one that causes support tickets)โ€‹

Send net, VAT-exclusive unit prices. GoRoute computes the document totals from the lines themselves:

  • each line's net amount is quantity ร— unit_price,
  • VAT is that net amount times the line's tax_rate,
  • the gross total is the two added together.

All three are rounded to three decimal places, because the rial divides into a thousand baisa.

The gross_amount you send is not used to build the document. It is a cross-check. If it differs from the computed total by more than one baisa per cart line, the call is refused with HTTP 422 and the response explains that net unit prices are expected. Nothing is issued.

This is deliberate: a tax document whose totals contradict the customer's basket is worse than a refused API call. A store that sends VAT-inclusive prices will hit this on its very first call โ€” the computed gross will come out roughly 5% above the figure sent.

In the example above, two units at a net 12.500 give 25.000 net, 1.250 VAT and 26.250 gross, which is what gross_amount says. It reconciles, so the sale is accepted.

What comes backโ€‹

A new sale returns HTTP 201 and a body like this:

{
"invoice_reference": "INV-WC-10432",
"invoice_uuid": "03a1e43a-3a94-521c-ae4e-6bf2f1f975c1",
"invoice_type": "simplified",
"status": "validated",
"transaction_id": "โ€ฆ",
"transaction_status": "delivered",
"seller_vat": "OM1100017479",
"issued_at": "2026-08-20T13:41:22+04:00",
"qr": {
"tlv_base64": "โ€ฆ",
"image_data_uri": "data:image/png;base64,โ€ฆ",
"tags": { "โ€ฆ": "โ€ฆ" },
"verify_url": "https://app.goroute.ai/app/verify.html#โ€ฆ",
"verify_image_data_uri": "data:image/png;base64,โ€ฆ"
},
"totals": { "net": "25.000", "vat": "1.250", "gross": "26.250", "currency": "OMR" },
"tdd": { "status": "cleared", "channel": "ota_fawtara_tdd", "deadline": "โ€ฆ", "note": "โ€ฆ" },
"validation_score": 100,
"validation_errors": []
}
FieldWhat it is
invoice_referenceThe invoice number. For a sale it is INV- followed by your order_reference; for a refund, CN- followed by it.
invoice_uuidThe invoice's unique identifier. Keep it โ€” a refund needs it.
invoice_typesimplified for these endpoints.
statusvalidated, or validation_failed if the document broke a business rule.
transaction_idThe record on your Transactions page. Null only if saving it failed.
transaction_statusdelivered, queued or failed.
qrTwo QR codes. See The two QR codes.
totalsThe computed net, VAT and gross, as strings with three decimals.
tddThe tax-reporting position. See What happens to the invoice afterwards.
validation_scoreA score out of 100 from the validation stack.
validation_errorsThe business rules that failed, each with a code and a message. Empty when status is validated.
A 201 does not by itself mean the invoice is valid

If the document fails business validation, the endpoint still answers 201, but status reads validation_failed, validation_errors lists what went wrong and transaction_status reads failed. Check the status field, not only the HTTP code.

The one exception to that ordering: a request that fails the totals cross-check, or that is missing a required field, never gets that far. It is rejected with 422 before anything is issued.

Retries are safe: 201 for a new sale, 200 for a repeatโ€‹

The invoice's identifier is derived deterministically. It is computed from your seller scheme and VAT number, the document type, the invoice number, the issue date, the VAT amount and the gross amount. Send the same order twice and the same identifier comes out both times, so the second call returns the record already stored instead of issuing a second invoice.

The two cases are told apart by the HTTP status:

  • 201 โ€” this sale is newly recorded.
  • 200 โ€” GoRoute already had it, and you are being handed the original.

A double-clicked checkout button, or a payment gateway retrying after a timeout, is therefore safe. It is also the reason a repeat cannot inflate what you are billed.

Two cautions worth knowing before you rely on it:

  • Because the totals and the issue date are part of the identifier, resending the same order reference with different amounts or a different date is a different document, and it will be issued as one.
  • The 200 case is not listed in the machine-readable API description, which documents 201 and 422 only. It is the behaviour of the code; a client that treats any 2xx as success is unaffected either way.

Refund a saleโ€‹

A refund is the same shape as the sale, plus a reference to the sale it reverses. Both of the fields below come from the original sale's response: preceding_invoice_number is its invoice_reference and preceding_invoice_uuid is its invoice_uuid.

curl -X POST "https://app.goroute.ai/peppol-api/api/v1/pos/refund" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"seller_name": "Muscat Coffee Traders LLC",
"seller_vat": "OM1100017479",
"order_reference": "WC-10432-R1",
"issued_at": "2026-08-21T09:12:00+04:00",
"currency": "OMR",
"lines": [
{
"description": "Espresso beans 1kg (returned)",
"quantity": 1,
"unit_price": 12.500,
"tax_rate": 5
}
],
"gross_amount": 13.125,
"preceding_invoice_number": "INV-WC-10432",
"preceding_invoice_uuid": "03a1e43a-3a94-521c-ae4e-6bf2f1f975c1"
}'

Three things differ from a sale:

  • Amounts stay positive. A credit note credits an amount; it does not carry negative lines.
  • A partial refund sends only the lines being refunded, with the quantities actually returned.
  • A reason may be given. credit_reason_code defaults to RET and credit_reason_text to Customer refund.

The response has exactly the same shape as a sale. The credit note gets its own invoice reference, its own identifier and its own QR code, and it is reported in its own right.

The two QR codesโ€‹

Both endpoints return two images, each as a data: URI you can put straight into an image tag on a receipt or a confirmation page.

FieldWhat it is
qr.image_data_uriThe strict Oman tax QR code. It encodes the nine-tag Fawtara payload โ€” QR version, invoice type, seller name, VAT number, date, time, total including VAT, VAT amount and the seller identifier. This is the one the tax rules are about.
qr.verify_image_data_uriA convenience QR code. It encodes a link to a GoRoute page that displays the invoice details, so a phone camera opens something a person can read.

qr.tlv_base64 is the exact value encoded in the tax QR, if you would rather render the image yourself. qr.tags is that same payload decoded into readable fields.

What happens to the invoice afterwardsโ€‹

This is the part to read carefully, because it is a compliance statement.

A consumer sale is never sent over the Peppol network. There is no buyer Peppol address involved and none is required. The compliance record is the QR receipt plus a Tax Data Document, the artefact reported to the Oman Tax Authority.

The tdd object in the response describes that position:

FieldMeaning
channelota_fawtara_tdd โ€” reported to the tax authority, not transmitted over Peppol.
deadline24 hours after the sale. That is the Oman reporting window for a consumer sale.
statuscleared once the sale has passed validation and been cleared through GoRoute.
noteA sentence describing the current position in words.
Reports are currently generated and held, not sent

GoRoute builds and stores each Tax Data Document, and it is auditable. It is not being transmitted to the Oman Tax Authority today, because the setting that enables transmission is switched off by default while the authority's Fawtara portal is not yet accepting submissions.

Held reports are released later automatically, and each one keeps the original issue time of its invoice, so the 24-hour obligation stays evidenced per invoice. Do not tell a merchant their sale has already been reported to the authority.

When something is wrongโ€‹

StatusWhat happened
422The request was rejected before anything was issued: a required field is missing, or gross_amount does not reconcile with the lines. The message names the problem.
403Either your key lacks the invoices:create permission, or the create_invoice feature is off for your organisation.
201 with status: validation_failedThe document was issued as a record but broke a business rule. validation_errors lists each one.

Rules that only apply to sending a document over the Peppol network โ€” such as the requirement for a buyer Peppol address โ€” are deliberately not applied to a consumer sale, so a valid cash receipt is not marked as failed for lacking something it can never have.

Next Stepsโ€‹