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.
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โ
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/pos/sale | A customer has paid. Issue the simplified tax invoice and return its QR code. |
POST | /api/v1/pos/refund | A 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โ
| Field | Meaning |
|---|---|
seller_name | Your registered business name. |
seller_vat | Your Omani VAT identification number, for example OM1100017479. |
order_reference | Your own order number, for example WC-10432. Maximum 50 characters. |
issued_at | The moment the customer paid, as a date and time. |
lines | The cart. At least one line, at most 200. |
gross_amount | The total including VAT that the customer actually paid. Read the totals rule before you fill this in. |
Optional fields, and what they default toโ
| Field | Default | Notes |
|---|---|---|
currency | OMR | Three-letter currency code. |
channel | web | Where the sale happened: web, pos or app. |
seller_scheme | 0248 | The identifier scheme for your VAT number. Four digits. |
seller_city | Muscat | |
seller_street | empty | |
seller_country | OM | Two-letter country code. |
customer | omitted | A 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โ
| Field | Required | Default | Notes |
|---|---|---|---|
description | yes | Up to 500 characters. | |
quantity | yes | Must be greater than zero. | |
unit_price | yes | The net price per unit, excluding VAT. | |
unit | no | EA | Unit of measure. |
tax_rate | no | 5 | Percentage, between 0 and 100. |
sku | no | none | Your 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": []
}
| Field | What it is |
|---|---|
invoice_reference | The invoice number. For a sale it is INV- followed by your order_reference; for a refund, CN- followed by it. |
invoice_uuid | The invoice's unique identifier. Keep it โ a refund needs it. |
invoice_type | simplified for these endpoints. |
status | validated, or validation_failed if the document broke a business rule. |
transaction_id | The record on your Transactions page. Null only if saving it failed. |
transaction_status | delivered, queued or failed. |
qr | Two QR codes. See The two QR codes. |
totals | The computed net, VAT and gross, as strings with three decimals. |
tdd | The tax-reporting position. See What happens to the invoice afterwards. |
validation_score | A score out of 100 from the validation stack. |
validation_errors | The business rules that failed, each with a code and a message. Empty when status is validated. |
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_codedefaults toRETandcredit_reason_texttoCustomer 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.
| Field | What it is |
|---|---|
qr.image_data_uri | The 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_uri | A 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:
| Field | Meaning |
|---|---|
channel | ota_fawtara_tdd โ reported to the tax authority, not transmitted over Peppol. |
deadline | 24 hours after the sale. That is the Oman reporting window for a consumer sale. |
status | cleared once the sale has passed validation and been cleared through GoRoute. |
note | A sentence describing the current position in words. |
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โ
| Status | What happened |
|---|---|
| 422 | The 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. |
| 403 | Either your key lacks the invoices:create permission, or the create_invoice feature is off for your organisation. |
201 with status: validation_failed | The 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โ
- Send Invoice โ invoicing another business over Peppol
- Send Credit Note โ the Peppol equivalent of a refund
- Validation โ what the validation layers check
- Oman Country Guide โ Oman e-invoicing in general
- Tracking โ following documents after they are issued