# EFRIS API — ERP Integration Guide

How to connect an accounting system, POS or ERP to URA's EFRIS through the EFRIS
API: what to send for each business operation, and what you get back.

---

## Contents

1. [Before you start](#1-before-you-start)
2. [Connecting](#2-connecting)
3. [The integration flow](#3-the-integration-flow)
4. [Products](#4-products)
5. [Stock](#5-stock)
6. [Invoices and receipts](#6-invoices-and-receipts)
7. [Credit notes](#7-credit-notes)
8. [Lookups](#8-lookups)
9. [Printing the fiscal document](#9-printing-the-fiscal-document)
10. [Reliability rules](#10-reliability-rules)
11. [Errors](#11-errors)
12. [Appendix — code tables](#appendix--code-tables)

---

## 1. Before you start

Your EFRIS provider sets up your account. Before your first request, confirm with
them that it has:

| Item | Why it matters |
| --- | --- |
| **TIN, legal name, address and phone** | Printed as the seller on every document. They come from your account, never from the request. |
| **EFRIS device number** | Issued by URA and required on every document. |
| **Business email address** | URA refuses documents without it. |
| **VAT registration status** | Decides whether you issue tax invoices or receipts — see [Tax invoice or receipt](#tax-invoice-or-receipt). |
| **Test or live** | Your provider sets whether your documents go to URA's test or live system. Moving to live needs no change in your code. |
| **API key** | Keep it secret. If it leaks, ask your provider to replace it; the old key stops working at once. |

---

## 2. Connecting

### Base URL

```
https://efris.midwayug.com/api/external/efris
```

Every path in this guide is relative to it, so `POST /submit-invoice` means
`POST https://efris.midwayug.com/api/external/efris/submit-invoice`.

Keep the base URL and the API key in your system's settings, not in its code:
both can change.

### Headers

```http
X-API-Key: YOUR_API_KEY
Content-Type: application/json
```

### Field names

Send fields in **`snake_case` or `camelCase`**, whichever your system produces.
The tables below list the accepted spellings.

### Responses

**Success** — each section below shows what comes back. Lookups return:

```json
{
  "success": true,
  "result": { "...": "the data" },
  "returnCode": "00"
}
```

**Failure** — every failure carries `detail`: what is wrong and what to do, in
words your user — usually an accountant — can act on. Show it to them.

When URA refused the document, `detail` starts with URA's code and message and
ends with `What to do:`, and a `ura` object carries the same parts separately —
`field` is the field in plain words, `hint` is what to do:

```json
{
  "success": false,
  "error": "Bad request",
  "detail": "EFRIS error 2122: goodsDetails-->itemCode:Item code and Name is not configured with URA. What to do: This item is not registered with URA. Register the product (item code and name) first, then send the invoice again.",
  "ura": {
    "code": "2122",
    "message": "goodsDetails-->itemCode:Item code and Name is not configured with URA.",
    "field": "Items → item code",
    "hint": "This item is not registered with URA. Register the product (item code and name) first, then send the invoice again."
  }
}
```

### HTTP status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `200` | Accepted | Store the result. |
| `400` | The request is incomplete, or URA refused the document | Show `detail`; fix what it says. Do not resend unchanged. |
| `401` | Missing, wrong or disabled API key | Check the key; contact your provider. |
| `403` | Licence expired | Contact your provider. |
| `404` | No such path, or no such record | Check the path. |
| `422` | The body is not in the expected shape | See `detail` for the fields. |
| `502` | URA could not be reached or answered with a problem | Show `detail`. Queries can be retried; for sales, read [Reliability rules](#10-reliability-rules) first. |
| `500` | Your account is not fully set up, or an unexpected fault | Show `detail` and contact your provider. If it quotes a reference, pass it on. |

---

## 3. The integration flow

Do these in order.

| Step | Call | When |
| --- | --- | --- |
| 1. Check the connection | `GET /diagnostic-test` | At setup, and whenever something stops working |
| 2. Load reference data | `GET /units-of-measure`, `GET /payment-methods`, `GET /commodity-categories`, `GET /excise-duty`, `GET /branches` | At setup; refresh occasionally |
| 3. Register products | `POST /register-product` | Once per product, and again when it changes |
| 4. Record stock | `POST /stock-increase` | On every goods receipt, opening stock first |
| 5. Fiscalise sales | `POST /submit-invoice` or `POST /submit-receipt` | On every sale |
| 6. Reverse sales | `POST /submit-credit-note` | On returns and corrections |

### Step 1 — `GET /diagnostic-test`

Shows your account's details and whether the connection to URA works:

```json
{
  "company_name": "Nakawa Traders Ltd",
  "tin": "1000000001",
  "device_number": "1000000001_02",
  "test_mode_enabled": true,
  "reachable": true
}
```

If `reachable` is `false`, pass the response to your provider. Do not go further
until it is `true`.

---

## 4. Products

A product **must be registered before it can be sold or stocked**. Register each
product once; update it with `operationType: "102"` when its price or description
changes.

### `POST /register-product`

```json
{
  "operationType": "101",
  "item_code": "WTR-500",
  "item_name": "Bottled water 500ml",
  "unit_price": "1500",
  "commodity_code": "50202301",
  "unit_of_measure": "101",
  "is_service": false,
  "stock_prewarning": "10"
}
```

**Response**

```json
{
  "success": true,
  "product_code": "WTR-500",
  "efris_status": "Registered",
  "message": "Product registered successfully"
}
```

### Fields

| Field | Also accepted as | Required | Notes |
| --- | --- | --- | --- |
| `item_code` | `itemCode`, `goodsCode` | **Yes** | Your product code. Invoices refer to it. |
| `item_name` | `itemName`, `goodsName` | **Yes** | |
| `unit_price` | `unitPrice` | **Yes** | Tax-inclusive. |
| `commodity_code` | `commodityCode`, `commodityCategoryId` | **Yes** | URA commodity category, from `GET /commodity-categories`. It decides the tax URA applies. |
| `operationType` | `operation_type` | No | `101` new (default), `102` update. |
| `unit_of_measure` | `unitOfMeasure`, `measureUnit` | **Yes** | The unit it is sold in, from `GET /units-of-measure`. Refused without it. |
| `currency` | | No | `101` (UGX) by default. |
| `is_service` | `isService` | No | `true` for a service. |
| `goods_type_code` | `goodsTypeCode` | No | `101` goods (default), `102` fuel. |
| `stock_prewarning` | `stockPrewarning` | No | Goods only. |
| `description` | | No | |
| `have_excise_tax` | `haveExciseTax` | No | `101` yes, `102` no (default). See [Excise](#products-with-excise-duty). |
| `excise_duty_code` | `exciseDutyCode` | When excise applies | From `GET /excise-duty`. Leave it out otherwise. |
| `have_piece_unit` | `havePieceUnit` | No | `101` yes, `102` no. See [Packaging](#multi-unit-packaging). |
| `piece_measure_unit` | `pieceMeasureUnit` | **Yes**, with piece units | The unit one piece is counted in, from `GET /units-of-measure`. Refused without it; never copied from the main unit. |
| `piece_unit_price` | `pieceUnitPrice` | With piece units | Price of one piece. |
| `package_scaled_value` | `packageScaledValue` | With piece units | Outer unit multiplier, usually `1`. |
| `piece_scaled_value` | `pieceScaledValue` | With piece units | Pieces per outer unit, e.g. `24`. |
| `customs_info` | | No | Imported goods: `{ measure_unit, unit_price, package_scaled, customs_scaled }`. |
| `other_units` | | No | Further selling units. |

### Multi-unit packaging

For goods sold both in bulk and singly — a crate of 24 bottles priced either way:

| Field | Send |
| --- | --- |
| `have_piece_unit` | `"101"` |
| `package_scaled_value` | The outer unit multiplier, usually `"1"` |
| `piece_scaled_value` | Pieces in one outer unit, e.g. `"24"` |
| `piece_measure_unit` | Unit code of the piece |
| `piece_unit_price` | Price of one piece |

A product registered with piece units must be invoiced with `package_scaled`,
`piece_scaled` and `piece_qty` on each line.

### Products with excise duty

Send `have_excise_tax: "101"` and the `excise_duty_code`. Look codes up with
`GET /excise-duty?excise_name=beer`.

| Excise type | Example | `have_piece_unit` |
| --- | --- | --- |
| Percentage of value | Beer at 60% | `"102"` is fine, unless the product is naturally multi-unit |
| Fixed amount per unit | Mineral water at UGX 150 per litre | **Must be `"101"`**, or URA refuses it (error 676) |

For a fixed-amount code, also send `piece_measure_unit` as the excise unit (for
example the litre code), `piece_scaled_value` as the scale factor (`"1"` for a
single-unit product), and `piece_unit_price` as `unit_price ÷ piece_scaled_value`.

**Percentage example**

```json
{
  "operationType": "101",
  "item_code": "BELL-500",
  "item_name": "Bell Lager 500ml",
  "unit_price": "4720",
  "commodity_code": "2203001000",
  "unit_of_measure": "102",
  "have_piece_unit": "102",
  "have_excise_tax": "101",
  "excise_duty_code": "LED190100"
}
```

**Fixed-amount example**

```json
{
  "operationType": "101",
  "item_code": "WATER-1L",
  "item_name": "Mineral Water 1L",
  "unit_price": "2000",
  "commodity_code": "50202310",
  "unit_of_measure": "102",
  "have_piece_unit": "101",
  "piece_measure_unit": "102",
  "piece_scaled_value": "1",
  "package_scaled_value": "1",
  "piece_unit_price": "2000",
  "have_excise_tax": "101",
  "excise_duty_code": "LED190400"
}
```

### Updating a product

Send the same `item_code` with `"operationType": "102"`. Sending `101` for a code
that already exists is refused.

---

## 5. Stock

URA tracks stock for each registered product. A sale of goods with no recorded
stock is refused, unless negative stock is allowed for your business (check with
`GET /negative-stock-configuration`).

### 5a. Stock received — `POST /stock-increase`

```json
{
  "goodsStockIn": {
    "supplierName": "Crown Beverages Ltd",
    "supplierTin": "1000001234",
    "stockInType": "102",
    "stockInDate": "2026-09-25",
    "goodsTypeCode": "101"
  },
  "goodsStockInItem": [
    { "goodsCode": "WTR-500", "quantity": "240", "unitPrice": "1000", "measureUnit": "101" }
  ]
}
```

**`stockInType`**

| Code | Meaning | Supplier fields |
| --- | --- | --- |
| `101` | Import | `supplierName` required; `supplierTin` when the supplier has one |
| `102` | Local purchase | `supplierName` required; `supplierTin` when the supplier has one |
| `103` | Manufacture / assembly | Leave them **empty** (refused with a supplier); send `productionBatchNo` and `productionDate` instead |
| `104` | Opening stock | `supplierName` if you have one; when none is sent, `Opening Stock` is sent. Use when first going live |

Send `productionBatchNo` and `productionDate` (`YYYY-MM-DD`) only with type `103`.

### 5b. Stock written down — `POST /stock-decrease`

```json
{
  "goodsStockIn": {
    "operationType": "102",
    "adjustType": "102",
    "remarks": "Damaged in the warehouse"
  },
  "goodsStockInItem": [
    { "goodsCode": "WTR-500", "quantity": "12", "unitPrice": "1000", "measureUnit": "101" }
  ]
}
```

**`adjustType`** — required

| Code | Meaning |
| --- | --- |
| `101` | Expired goods |
| `102` | Damaged goods |
| `103` | Personal use |
| `104` | Others — `remarks` required |
| `105` | Raw materials |

`operationType` can be left out: the address already says which way the stock moves.
Stock written down has no supplier: leave out `supplierTin`, `supplierName` and `stockInType`.

### Stock fields

The header can be sent as `goodsStockIn` or `goods_stock_in`; the items as
`goodsStockInItem` or `goods_stock_in_item`.

| Header field | Also accepted as |
| --- | --- |
| `supplierTin` | `supplier_tin` |
| `supplierName` | `supplier_name` |
| `stockInType` | `stock_in_type` |
| `stockInDate` | `stock_in_date` |
| `adjustType` | `adjust_type` |
| `productionBatchNo` | `production_batch_no` |
| `productionDate` | `production_date` |
| `remarks` | |

| Item field | Also accepted as | Notes |
| --- | --- | --- |
| `goodsCode` | `goods_code`, `item_code` | A registered product code. Send it or `commodityGoodsId` |
| `commodityGoodsId` | `commodity_goods_id` | URA's id for the product, instead of `goodsCode` |
| `quantity` | `qty` | |
| `unitPrice` | `unit_price` | |
| `measureUnit` | `measure_unit`, `unit_of_measure` | Required. The unit this quantity is counted in; for an item held in two units, the one you mean |
| `remarks` | | |
| `fuelTankId`, `lossQuantity`, `originalQuantity` | `fuel_tank_id`, `loss_quantity`, `original_quantity` | Fuel only |

**Rules**

| Rule | When |
| --- | --- |
| `stockInType` required | Stock received |
| `supplierName` required | Stock received, except type `103` (none) and `104` (`Opening Stock` when none is sent) |
| `supplierTin` empty | Stock written down, or type `103` |
| `supplierName` and `stockInType` empty | Stock written down |
| `adjustType` required | Stock written down |
| `adjustType` empty | Stock received |
| `productionBatchNo`, `productionDate` | Only with type `103` |
| `goodsCode` or `commodityGoodsId` | Every item |
| `measureUnit` required | Every item, on every stock movement and transfer |
| `remarks` required | Stock written down with `adjustType` `104` |
| `operationType` `101` | Only to `/stock-increase` |
| `operationType` `102` | Only to `/stock-decrease` |

### 5c. Transfer between branches — `POST /stock-transfer`

Moves stock between two branches of your business. Get branch ids from
`GET /branches`.

```json
{
  "goodsStockTransfer": {
    "source_branch_id": "206637525568955296",
    "destination_branch_id": "206637528324276772",
    "transfer_type_code": "101",
    "remarks": "",
    "roll_back_if_error": "0",
    "goods_type_code": "101"
  },
  "goodsStockTransferItem": [
    { "goods_code": "WTR-500", "measure_unit": "101", "quantity": "48", "remarks": "" }
  ]
}
```

| Header field | Also accepted as | Notes |
| --- | --- | --- |
| `source_branch_id` | `sourceBranchId` | **Required** |
| `destination_branch_id` | `destinationBranchId` | **Required**; must differ from the source |
| `transfer_type_code` | `transferTypeCode` | **Required**: `101` out-of-stock adjust, `102` error adjust, `103` other — `remarks` required |
| `remarks` | | |
| `roll_back_if_error` | `rollBackIfError` | `"1"` cancels the whole transfer if any line fails; `"0"` (default) keeps the rest |
| `goods_type_code` | `goodsTypeCode` | `101` goods (default), `102` fuel |

### Stock responses

```json
{
  "success": true,
  "message": "Stock increase submitted successfully"
}
```

If URA refuses any line, the response is an error whose `detail` names each line
and why.

---

## 6. Invoices and receipts

### Tax invoice or receipt

Which document you issue follows your VAT registration with URA:

| Your business is | You issue | Send sales to | Tax on the items |
| --- | --- | --- | --- |
| Registered for VAT | Tax invoice | `POST /submit-invoice` | **Every item** states its tax type: `18` standard, `0` zero-rated or `"-"` exempt |
| Not registered for VAT | Receipt | `POST /submit-receipt` | **None** — leave `tax_rate` out, or send `0` |

Nothing is changed for you: a sale is issued exactly as you send it, or refused
with a message saying what to fix. Keep your system's documents and tax settings
in line with your VAT registration, and change them if it changes.

**The wrong document is refused.** A business without VAT that sends a tax
invoice is told to issue receipts:

```json
{
  "detail": "Nakawa Traders Ltd (TIN 1000000001) is not registered for VAT, so URA accepts only receipts from it, with no VAT on the items. Issue this sale as a receipt, to /api/external/efris/submit-receipt."
}
```

A VAT-registered business that sends a receipt is told, likewise, to issue a
tax invoice.

**Every item on a tax invoice needs a tax type.** Standard (`18`), zero-rated
(`0`) and exempt (`"-"`) are all tax types, and each is kept exactly as you send
it — goods zero-rated for local sale stay zero-rated. An item with no tax type at
all is refused:

```json
{
  "detail": "This business is registered for VAT, so each item on its tax invoice must state its VAT. No tax rate was given for: Bottled water 500ml. Set the VAT on the item in your system (18 standard, 0 zero-rated, \"-\" exempt) and send the sale again, so your books match the invoice URA issues."
}
```

**A receipt carries no VAT.** The price the customer pays is the whole amount, so
a business without VAT records its sales without VAT. A receipt sent with VAT on
any item is refused, naming the items:

```json
{
  "detail": "A receipt carries no VAT - it is what URA issues a business that is not registered for VAT. VAT was charged on: Bottled water 500ml (18%). Remove the VAT in your system (set the item to no VAT) and send the sale again, so your books match the receipt URA issues."
}
```

**A receipt:**

```json
{
  "format": "simple",
  "invoice_number": "RCPT-2026-000118",
  "invoice_date": "2026-09-25",
  "customer_name": "Walk-in Customer",
  "payment_method": "102",
  "operator": "Jane Kasule",
  "items": [
    {
      "item_name": "Bottled water 500ml",
      "item_code": "WTR-500",
      "quantity": 12,
      "unit_price": 1500,
      "unit_of_measure": "101",
      "goods_category_id": "50202301"
    }
  ]
}
```

The response has the same shape as an invoice's ([6h](#6h-the-response)), with
`fiscal_data.invoice_kind` `"2"`. Print it as a receipt, with no VAT.

### 6a. Two rules for every sale

- **Always include `"format": "simple"`.**
- **Send tax-inclusive prices.** A product costing 10,000 before 18% VAT is sent
  as `unit_price: 11800`.

### 6b. A tax invoice — `POST /submit-invoice`

```json
{
  "format": "simple",
  "invoice_number": "INV-2026-000412",
  "invoice_date": "2026-09-25",
  "customer_name": "Nakawa Traders Ltd",
  "customer_tin": "1000999888",
  "customer_email": "accounts@nakawatraders.ug",
  "customer_phone": "0772000000",
  "buyer_type": "0",
  "payment_method": "102",
  "currency": "UGX",
  "operator": "Jane Kasule",
  "remarks": "Thank you for your business",
  "items": [
    {
      "item_name": "Bottled water 500ml",
      "item_code": "WTR-500",
      "quantity": 12,
      "unit_price": 1500,
      "tax_rate": 18,
      "unit_of_measure": "101",
      "goods_category_id": "50202301"
    }
  ]
}
```

### Invoice fields

| Field | Also accepted as | Required | Notes |
| --- | --- | --- | --- |
| `format` | | **Yes** | `"simple"` |
| `invoice_number` | `invoiceNumber` | **Yes** | Your document number. Each number can be used once. |
| `invoice_date` | `invoiceDate` | **Yes** | `YYYY-MM-DD` |
| `customer_name` | `customerName` | **Yes** | |
| `items` | `goods_details`, `goodsDetails` | **Yes** | At least one line |
| `buyer_type` | `buyerType` | No | `0` business, `1` individual (default), `2` foreigner, `3` government |
| `customer_tin` | `customerTin` | For business buyers | |
| `customer_address` | `customerAddress` | No | |
| `customer_email` | `customerEmail` | No | |
| `customer_phone` | `customerPhone` | No | |
| `payment_method` | `paymentMethod` | Send it | A code from `GET /payment-methods` (see the [table](#payment-methods)). Without it the sale prints as credit. |
| `currency` | | No | `UGX` by default |
| `operator` | | No | The person serving, printed as "served by". Without it, your account's default operator is used. |
| `remarks` | | No | Up to 500 characters, printed on the document |
| `invoice_industry_code` | `invoiceIndustryCode` | No | `101` general (default), `102` export goods, `112` export service — see 6f |
| `delivery_terms_code` | `deliveryTermsCode` | For exports | Incoterm |
| `customs` | | For export goods | See 6f |

Do not send seller details; they come from your account.

### Item fields

| Field | Also accepted as | Required | Notes |
| --- | --- | --- | --- |
| `item_name` | `item`, `itemName` | **Yes** | |
| `item_code` | `itemCode` | **Yes** | A registered product code |
| `quantity` | `qty` | **Yes** | |
| `unit_price` | `unitPrice` | **Yes** | Tax-inclusive |
| `tax_rate` | `taxRate` | **Yes** (tax invoices) | `18` standard · `0` zero-rated · `"-"` exempt. Receipts: leave out or `0`. |
| `unit_of_measure` | `unitOfMeasure` | **Yes** | The unit this quantity is sold in: one the product is registered with (its unit, or its piece unit for a piece sale). Refused without it. |
| `goods_category_id` | `goodsCategoryId`, `commodity_code` | No | The product's commodity category |
| `discount` | | No | Tax-inclusive discount amount — see 6c |
| `package_scaled` | `pack` | Piece-unit products | |
| `piece_scaled` | `stick` | Piece-unit products | |
| `piece_qty` | `pieceQty` | Piece-unit products and exports | |
| `piece_measure_unit` | `pieceMeasureUnit` | Exports | |
| `excise_*` | | Excise items | See 6d |
| `deemed_*`, `vat_project_*` | | Deemed items | See 6e |
| `total_weight`, `hs_code`, `hs_name` | camelCase forms | Exports | See 6f |

### 6c. Discounts

Add `discount` — the tax-inclusive amount — to the line:

```json
{
  "item_name": "Bottled water 500ml",
  "item_code": "WTR-500",
  "quantity": 5,
  "unit_price": 24000,
  "tax_rate": 18,
  "discount": 10000
}
```

The discount appears as its own line on the fiscalised document.

### 6d. Excise duty items

Add these to each line that carries excise:

```json
{
  "item_name": "Bell Lager 500ml",
  "item_code": "BELL-500",
  "quantity": 24,
  "unit_price": 4720,
  "tax_rate": 18,
  "unit_of_measure": "101",
  "excise_flag": "1",
  "category_id": "BEV001",
  "category_name": "Beer & Malt Beverages",
  "excise_rule": "2",
  "excise_rate": "650",
  "excise_tax": "7800",
  "excise_unit": "102",
  "excise_currency": "UGX",
  "pack": "1",
  "stick": "500"
}
```

| Field | Also accepted as | Notes |
| --- | --- | --- |
| `excise_flag` | `exciseFlag` | `"1"` has excise, `"2"` none (default) |
| `excise_rule` | `exciseRule` | `"1"` percentage, `"2"` fixed amount per unit |
| `excise_rate` | `exciseRate` | Percentage: `0.10` for 10%. Fixed: the amount per unit, e.g. `650`. |
| `excise_tax` | `exciseTax` | The line's excise amount, as your system calculates it |
| `excise_unit` | `exciseUnit` | Fixed-amount only — see [appendix](#excise-units) |
| `excise_currency` | `exciseCurrency` | e.g. `UGX` |
| `category_id`, `category_name` | `categoryId`, `categoryName` | The excise category |

### 6e. Deemed VAT items

For businesses supplying under a deemed-VAT project (strategic investors,
aid-funded contractors, government MDAs and so on). Send `deemed_flag: "1"` on
each deemed line:

```json
{
  "format": "simple",
  "invoice_number": "INV-DEEMED-001",
  "invoice_date": "2026-09-25",
  "customer_name": "Ministry of Works",
  "customer_tin": "1000555444",
  "buyer_type": "3",
  "payment_method": "101",
  "items": [
    {
      "item_name": "Consulting services",
      "item_code": "SVC-CONSULT-01",
      "quantity": 1,
      "unit_price": 5000000,
      "tax_rate": 18,
      "goods_category_id": "73110000",
      "deemed_flag": "1",
      "deemed_exempt_code": "101",
      "vat_project_id": "893997229738400343",
      "vat_project_name": "Strategic Investor Project"
    }
  ]
}
```

| Field | Also accepted as | Required | Notes |
| --- | --- | --- | --- |
| `deemed_flag` | `deemedFlag` | Yes | `"1"` deemed, `"2"` not (default) |
| `deemed_exempt_code` | `deemedExemptCode` | When deemed | See [appendix](#deemed-exempt-codes) |
| `vat_project_id` | `vatProjectId` | **When deemed** | From URA's project registry |
| `vat_project_name` | `vatProjectName` | **When deemed** | |

### 6f. Export invoices

Send `invoice_industry_code` `102` (goods) or `112` (services):

```json
{
  "format": "simple",
  "invoice_number": "EXP-2026-001",
  "invoice_date": "2026-09-25",
  "customer_name": "Nairobi Traders Ltd",
  "buyer_type": "2",
  "payment_method": "107",
  "currency": "USD",
  "invoice_industry_code": "102",
  "delivery_terms_code": "FOB",
  "customs": {
    "sad_number": "SAD12345",
    "office": "Busia",
    "cif": "5000",
    "ware_house_number": "WH001",
    "ware_house_name": "Busia Warehouse",
    "destination_country": "Kenya",
    "origin_country": "Uganda",
    "import_export_flag": "2",
    "confirm_status": "0",
    "valuation_method": "CIF",
    "prn": "PRN123"
  },
  "items": [
    {
      "item_name": "Green coffee beans",
      "item_code": "COFFEE-001",
      "quantity": 500,
      "unit_price": 12000,
      "tax_rate": 0,
      "unit_of_measure": "103",
      "total_weight": 500,
      "piece_qty": 500,
      "piece_measure_unit": "103",
      "hs_code": "0901110000",
      "hs_name": "Coffee, not roasted, not decaffeinated"
    }
  ]
}
```

| Required | For |
| --- | --- |
| `delivery_terms_code` — `FOB`, `CIF`, `CFR`, `EXW`, `DAP`, `DDP`, `DPU`, `CIP`, `CPT`, `FAS`, `FCA` | `102` and `112` |
| `total_weight` (net kg), `piece_qty`, `piece_measure_unit` on **every line** | `102` and `112` |
| `customs` block | `102` |
| `hs_code`, `hs_name` | Optional |

**Customs block fields:** `sad_number`, `office`, `cif`, `ware_house_number`,
`ware_house_name`, `destination_country`, `origin_country`, `import_export_flag`
(`1` import, `2` export — default), `confirm_status` (default `0`),
`valuation_method`, `prn`.

Exported goods are usually zero-rated: `tax_rate: 0`. Check an export's status
with `GET /export-invoice-status?invoiceNo={fdn}`.

### 6g. Sale types at a glance

| Sale | `invoice_industry_code` | Extra fields |
| --- | --- | --- |
| Normal sale | leave out | None |
| With discount | leave out | `discount` on the line |
| With excise | leave out | `excise_flag: "1"`, `excise_rule`, `excise_rate`, `excise_tax`, `category_id` |
| Deemed VAT | leave out | `deemed_flag: "1"`, `deemed_exempt_code`, `vat_project_id`, `vat_project_name` |
| Export goods | `102` | `delivery_terms_code`, `customs`, and per line `total_weight`, `piece_qty`, `piece_measure_unit` |
| Export service | `112` | `delivery_terms_code`, and per line `total_weight`, `piece_qty`, `piece_measure_unit` |

### 6h. The response

```json
{
  "success": true,
  "message": "Invoice successfully submitted to EFRIS",
  "invoiceNumber": "INV-2026-000412",
  "fdn": "325043056477",
  "verificationCode": "234893273725405146366",
  "qrCode": "…",
  "fiscalized_at": "2026-09-25T11:30:00",

  "seller": {
    "tin": "1000000001",
    "legal_name": "Nakawa Traders Ltd",
    "trade_name": "Nakawa Traders Ltd",
    "address": "Plot 1, Kampala Road, Kampala",
    "reference_number": "INV-2026-000412",
    "served_by": "Jane Kasule"
  },
  "fiscal_data": {
    "document_type": "Original",
    "invoice_kind": "1",
    "fdn": "325043056477",
    "verification_code": "234893273725405146366",
    "device_number": "1000000001_02",
    "efris_invoice_id": "8741236598741236",
    "issued_date": "25/09/2026",
    "issued_time": "11:30:00",
    "qr_code": "…"
  },
  "buyer": {
    "name": "Nakawa Traders Ltd",
    "tin": "1000999888",
    "buyer_type": "0"
  },
  "items": [
    {
      "item": "Bottled water 500ml",
      "itemCode": "WTR-500",
      "qty": "12",
      "unitOfMeasure": "101",
      "unitPrice": "1500.00",
      "total": "18000.00",
      "taxRate": "0.18",
      "tax": "2745.76",
      "taxCategoryCode": "01",
      "discountFlag": "2"
    }
  ],
  "tax_details": [
    {
      "taxCategoryCode": "01",
      "netAmount": "15254.24",
      "taxRate": "0.18",
      "taxAmount": "2745.76",
      "grossAmount": "18000.00",
      "taxRateName": "Standard Rate (18%)"
    }
  ],
  "summary": {
    "net_amount": 15254.24,
    "tax_amount": 2745.76,
    "gross_amount": 18000,
    "gross_amount_words": "Eighteen Thousand Shillings Only",
    "payment_mode": "Cash",
    "currency": "UGX",
    "number_of_items": 1,
    "remarks": "Thank you for your business"
  },
  "fullEfrisResponse": { "...": "the same document, for reprinting" }
}
```

**Store against the sale in your system:**

| Value | Why |
| --- | --- |
| `fiscal_data.invoice_kind` | `"1"` tax invoice, `"2"` receipt — which to print |
| `invoiceNumber` | Your invoice number, exactly as you sent it. It is never changed. |
| `fdn` | The fiscal document number. Lookups, credit notes and export queries use it. |
| `verificationCode` | Printed on the document |
| `qrCode` | Printed on the document |
| `fiscal_data.efris_invoice_id` | URA's id for the document; needed to cancel a credit note against it |
| The whole response | So the document can be reprinted |

---

## 7. Credit notes

A credit note reverses part or all of a fiscalised invoice or receipt. It is an
**application** to URA: a successful response means URA received it, not that it
is approved. Track it with the `referenceNo` in the response.

### `POST /submit-credit-note`

```json
{
  "credit_note_number": "CN-2026-001",
  "credit_note_date": "2026-09-25",
  "original_invoice_number": "325043056477",
  "original_fdn": "325043056477",
  "customer_name": "Nakawa Traders Ltd",
  "customer_tin": "1000999888",
  "buyer_type": "0",
  "reason": "Goods returned",
  "reason_code": "101",
  "items": [
    {
      "item_name": "Bottled water 500ml",
      "item_code": "WTR-500",
      "quantity": 2,
      "unit_price": 1500,
      "tax_rate": 18,
      "unit_of_measure": "101"
    }
  ]
}
```

- **Send positive quantities and amounts.**
- **Credit each item as it was sold:** the same item code, unit price and tax
  type as on the original invoice, and always send `original_fdn`. Against a tax
  invoice every item states its tax type; one without is refused.

**Response**

```json
{
  "success": true,
  "referenceNo": "24092500001",
  "credit_note_number": "CN-2026-001",
  "submitted_at": "2026-09-25T09:10:00",
  "message": "Credit note application submitted successfully to EFRIS"
}
```

### Credit notes against receipts

A credit note against a receipt carries no VAT, as the receipt had none: leave
`tax_rate` out, or send `0`. One sent with VAT is refused:

```json
{
  "detail": "This credit note is against a receipt, and a receipt carries no VAT - so its credit note carries none either. VAT was charged on: Bottled water 500ml (18%). Remove the VAT from the credit note in your system (set the item to no VAT) and send it again, so your books match URA's."
}
```

### Credit note fields

| Field | Required | Notes |
| --- | --- | --- |
| `credit_note_number` | **Yes** | Your credit note number |
| `original_invoice_number` | **Yes** | |
| `original_fdn` | **Send it** | The original's fiscal document number |
| `customer_name` | **Yes** | |
| `reason` | **Yes** | Free text |
| `reason_code` | Send it | See below |
| `items` | **Yes** | |
| `credit_note_date` | No | `YYYY-MM-DD` |
| `customer_tin` | For business buyers | |
| `buyer_type` | No | `1` by default |
| `currency` | No | `UGX` by default |
| `remarks` | No | Up to 500 characters. Sent as you give them; left empty if you give none |

**`reason_code`**

| Code | Meaning |
| --- | --- |
| `101` | Return of products (also expired or damaged) |
| `102` | Cancellation of purchase |
| `103` | Invoice amount wrongly stated |
| `104` | Partial or complete waive-off |
| `105` | Other — explain in `reason` |

### Credit note item fields

| Field | Also accepted as | Notes |
| --- | --- | --- |
| `item_name` | | |
| `item_code` | | As on the original invoice |
| `quantity` | `qty` | Quantity credited |
| `unit_price` | `unitPrice` | As on the original invoice |
| `tax_rate` | `taxRate` | **Yes** against a tax invoice — as on the original. Against a receipt: leave out or `0`. |
| `unit_of_measure` | `unitOfMeasure` | **Yes** — the unit on the original invoice line. Refused without it. |
| `excise_*`, `category_id`, `category_name` | camelCase forms | Only when the original line had excise; send the same values |

### After submitting

| To | Call |
| --- | --- |
| Find which invoices can be credited | `GET /creditable-invoices` |
| See what remains creditable on an invoice | `GET /invoice-remain-details?invoiceNo={fdn}` |
| Track the application | `GET /credit-notes?referenceNo={referenceNo}` |
| Cancel an issued credit note | `POST /cancel-credit-note` — below |

### Cancelling a credit note — `POST /cancel-credit-note`

```json
{
  "ori_invoice_id": "8741236598741236",
  "invoice_no": "325043056999",
  "reason_code": "103",
  "reason": "Credit note issued in error",
  "invoice_apply_category_code": "104"
}
```

| Field | Also accepted as | Required | Notes |
| --- | --- | --- | --- |
| `ori_invoice_id` | `oriInvoiceId` | **Yes** | `fiscal_data.efris_invoice_id` of the original invoice |
| `invoice_no` | `invoiceNo` | **Yes** | The credit note's fiscal document number |
| `reason_code` | `reasonCode` | **Yes** | `101` buyer refused it as incorrect, `102` not delivered as incorrect, `103` other |
| `reason` | | With code `103` | |
| `invoice_apply_category_code` | `invoiceApplyCategoryCode` | **Yes** | `104` to cancel a credit note |

---

## 8. Lookups

Most return `{ "success": true, "result": …, "returnCode": "00" }`.

| Purpose | Call | Parameters |
| --- | --- | --- |
| Check your connection | `GET /diagnostic-test` | — |
| Your registration details and branches | `GET /registration-details` | — |
| Branches (ids for stock transfers) | `GET /branches` | — |
| Units of measure (returned as `units`: a list of `code` and `name`) | `GET /units-of-measure` | — |
| Payment methods — the codes for `payment_method` | `GET /payment-methods` | — |
| Commodity categories | `GET /commodity-categories` | `pageNo`, `pageSize` |
| Excise duty codes and rates | `GET /excise-duty` | `excise_name` or `excise_code` |
| HS codes (exports) | `GET /hs-codes` | — |
| A buyer's details by TIN | `GET /taxpayer/{tin}` | — |
| Exchange rate | `GET /exchange-rate` | `currency` (required), `issueDate` |
| Your registered products — all of them in one answer, or `502` if URA did not send its whole list (try again) | `GET /goods` | `search`, `goods_code`, `goods_name` |
| Negative stock setting | `GET /negative-stock-configuration` | — |
| A sale, by **your** invoice number | `GET /invoice/{invoice_number}` | — (returns its `fdn` and `status`) |
| Search invoices and receipts | `GET /invoices` | `page`, `pageSize`, `startDate`, `endDate`, `buyerLegalName`, `invoiceNo`, `invoiceKind` |
| Invoices that can be credited | `GET /creditable-invoices` | `pageNo`, `pageSize`, `invoiceNo`, `buyerTin`, `startDate`, `endDate` |
| What remains creditable | `GET /invoice-remain-details` | `invoiceNo` (required) |
| Credit note applications | `GET /credit-notes` | `referenceNo` |
| Export status | `GET /export-invoice-status` | `invoiceNo` (required) |

---

## 9. Printing the fiscal document

The `submit-invoice` and `submit-receipt` response has everything needed to
print, with no second call.

### Where each printed value comes from

| Printed as | Response field |
| --- | --- |
| Seller TIN | `seller.tin` |
| Legal name / trade name | `seller.legal_name`, `seller.trade_name` (in capitals) |
| Seller address | `seller.address` |
| Seller reference | `seller.reference_number` |
| Served by | `seller.served_by` |
| Document type | `fiscal_data.document_type` |
| Tax invoice or receipt | `fiscal_data.invoice_kind` — `"1"` tax invoice, `"2"` receipt (title it as a receipt, with no VAT) |
| **Fiscal document number** | `fdn` |
| Verification code | `verificationCode` |
| Device number | `fiscal_data.device_number` |
| Issued date and time | `fiscal_data.issued_date`, `fiscal_data.issued_time` |
| Buyer name / TIN | `buyer.name`, `buyer.tin` |
| Goods lines | `items[]` — `item`, `qty`, `unitPrice`, `total`, `taxCategoryCode` |
| Tax breakdown | `tax_details[]` — `taxRateName`, `netAmount`, `taxAmount`, `grossAmount` |
| Net / tax / gross | `summary.net_amount`, `summary.tax_amount`, `summary.gross_amount` |
| Amount in words | `summary.gross_amount_words` |
| Payment mode | `summary.payment_mode` |
| Remarks | `summary.remarks` |
| QR code | `qrCode` |

Discount lines appear in `items` with `discountFlag: "0"` and a negative `total`;
print them under the line they reduce.

### Tax categories

| `taxCategoryCode` | Letter | Printed as |
| --- | --- | --- |
| `01` | A | Standard (18%) |
| `02` | B | Zero rate (0%) |
| `03` | C | Exempt |
| `04` | D | Deemed (18%) |
| `05` | E | Excise duty |
| `11` | F | VAT not applicable (receipts) |

### Layout — the six URA sections

Print in this order, single column:

```
┌───────────────────────────────────────────┐
│  EFRIS     e-INVOICE / TAX INVOICE    1/1 │  header bar
├───────────────────────────────────────────┤
│ A  Seller details                         │
│    TIN · Legal name · Trade name          │
│    Address · Reference · Served by        │
├───────────────────────────────────────────┤
│ B  URA information                        │
│    Document type · FDN (prominent)        │
│    Verification code · Device number      │
│    Issued date · Issued time              │
├───────────────────────────────────────────┤
│ C  Buyer details                          │
│    Name · TIN (business / government)     │
├───────────────────────────────────────────┤
│ D  Goods and services                     │
│    No · Item · Qty · Unit price · Total · │
│    Tax category                           │
├───────────────────────────────────────────┤
│ E  Tax details                            │
│    Category · Net · Tax · Gross           │
├───────────────────────────────────────────┤
│ F  Summary                                │
│    Net · Tax · Gross (bold)               │
│    Amount in words · Payment mode         │
│    Remarks                                │
├───────────────────────────────────────────┤
│  *** END OF e-INVOICE / TAX INVOICE ***   │  footer bar
├───────────────────────────────────────────┤
│  [QR code]   Scan to verify with URA      │
└───────────────────────────────────────────┘
```

For a receipt, title the header and footer as a receipt.

### Suggested styling

| Element | Style |
| --- | --- |
| Header and footer bars | Blue (`#2563EB` → `#1D4ED8`), white bold text |
| Section headings | Light grey background (`#F3F4F6`), small bold text |
| FDN | Bold, blue (`#1D4ED8`), larger than surrounding text |
| Verification code | Monospace |
| Legal and trade name | Capitals |
| Gross amount | Bold |
| Amount in words | Italic, grey (`#6B7280`) |
| Dividers | 1px grey (`#D1D5DB`) |

### Example: reading the response in Dart / Flutter

```dart
Map<String, dynamic> _map(dynamic v) => (v as Map?)?.cast<String, dynamic>() ?? {};

final r         = response;                 // the submit-invoice JSON
final fiscal    = _map(r['fiscal_data']);
final seller    = _map(r['seller']);
final summary   = _map(r['summary']);

final fdn              = r['fdn'] ?? fiscal['fdn'] ?? '';
final verificationCode = r['verificationCode'] ?? fiscal['verification_code'] ?? '';
final qrCode           = r['qrCode'] ?? fiscal['qr_code'] ?? '';
final isReceipt        = fiscal['invoice_kind'] == '2';

final legalName = (seller['legal_name'] ?? '').toString().toUpperCase();
final tradeName = (seller['trade_name'] ?? seller['legal_name'] ?? '').toString().toUpperCase();
final reference = seller['reference_number'] ?? r['invoiceNumber'] ?? '';

final gross = summary['gross_amount'] ?? 0;
final words = summary['gross_amount_words'] ?? '';

String categoryLabel(String code) => const {
      '01': 'A - Standard (18%)',
      '02': 'B - Zero rate (0%)',
      '03': 'C - Exempt',
      '04': 'D - Deemed (18%)',
      '05': 'E - Excise duty',
      '11': 'F - VAT not applicable',
    }[code] ??
    'Category $code';
```

### Reprinting

Store the full response against the sale in your own database, and reprint from
it; `fullEfrisResponse` holds the same document for that purpose.

---

## 10. Reliability rules

### Your invoice number is the sale — retrying is safe

Your `invoice_number` is sent to URA exactly as given and never changed, and a
number is fiscalised at most once. When a `submit-invoice` or `submit-receipt`
call times out, the connection drops or you get a `502`, **send it again with the
same number**:

- Already fiscalised: you get that same document back — same FDN, verification
  code and QR — with `alreadyFiscalised: true`. Nothing new is issued.
- Being fiscalised by another request right now: `409`, send it again in a minute.
- Refused before, or never reached URA: it is fiscalised now.
- Fiscalised for a different amount: `409` — that number belongs to another sale.
  Give this sale its own number.

**Never give a retry a new number** — URA would see a new document and fiscalise
the sale twice. If several tills or installations issue invoices under one TIN,
give each its own prefix (`KMP-000123`, `JJA-000123`).

Queries (`GET`) are always safe to retry.

### Other rules

| Rule | Why |
| --- | --- |
| Register before selling | A sale of an unregistered item is refused. |
| Record stock before selling | Unless negative stock is allowed for your business. |
| Send piece fields for piece-unit products | Otherwise stock is counted wrongly. |
| Always send `payment_method` | Without it, the sale is recorded as credit. |
| Always send `unit_of_measure` | Products, invoice, receipt and credit note lines are refused without it. Use a unit the product is registered with. |
| Always send `"format": "simple"` | See 6a. |
| Match your documents to your VAT registration | VAT-registered: tax invoices, every item with a tax type. Not registered: receipts, with no VAT. |

---

## 11. Errors

Every error's `detail` says what is wrong and what to do; show it to your user.
The most common ones:

### Refused before reaching URA

These return `400`:

| `detail` starts with | Fix |
| --- | --- |
| `Missing required field: …` | Add the named field. |
| `Invoice must have at least one item` | Add the sale's items. |
| `… is not registered for VAT, so URA accepts only receipts from it` | Issue the sale as a receipt, to `/submit-receipt`, with no VAT on the items. |
| `… is registered for VAT since …, so URA accepts only tax invoices from it` | Issue the sale as a tax invoice, to `/submit-invoice`. |
| `This business is registered for VAT, so each item on its tax invoice must state its VAT…` | Give each named item its tax type: `18`, `0` or `"-"`. |
| `A receipt carries no VAT … VAT was charged on: …` | Your business is not registered for VAT. Set the named items to no VAT and send the sale again. |
| `This credit note is against a tax invoice, so each item must state its VAT…` | Give each item the tax type it had on the invoice. |
| `This credit note is against a receipt … VAT was charged on: …` | Send the credit note without VAT, as the receipt was. |
| `Missing required fields: goodsStockIn and goodsStockInItem` | Send the stock header and its items. |
| `adjustType is required when reducing stock` | Add `adjustType`. |
| `Item N (…) has no unit of measure` | Send `measureUnit` for that item. |
| `Line N (…) of the invoice has no unit of measure` (or receipt, credit note) | Send `unit_of_measure` for that line: the unit its quantity is counted in. |
| `The product has no unit of measure` | Send `unit_of_measure`: the unit it is sold in. |
| `The product is also sold in pieces, so piece_measure_unit is required` | Send `piece_measure_unit`: the unit one piece is counted in. |
| `transfer_type_code is required on a stock transfer` | Say why the stock is moving: `101`, `102` or `103`. |
| `remarks is required when adjustType is 104 (others)` | Say in `remarks` why the stock was written down. |
| `A stock decrease has no supplier and no stock-in type` | Remove the named fields. |
| `supplierName is required when receiving stock` | Send the supplier the stock came from. Only stock made here (`stockInType` `103`) has none. |
| `Stock made here (stockInType 103) has no supplier` | Remove `supplierName` and `supplierTin`. |
| `stockInType is required when receiving stock` | Say where the stock came from: `101`, `102`, `103` or `104`. |
| `Stock received has no reason (adjustType)` | Remove `adjustType`; it is for stock written down. |
| `… only with stock made here (stockInType 103)` | Remove `productionBatchNo` and `productionDate`, unless the stock was made here. |
| `Item N names no product` | Send `goodsCode` or `commodityGoodsId` for that item. |
| `operationType … does not belong on the stock … route` | Send stock received to `/stock-increase` and stock written down to `/stock-decrease`. |

### Refused by URA

These return `400` with `detail` starting `EFRIS error <code>:` and a `ura`
object. Common codes:

| Code | Meaning | Fix |
| --- | --- | --- |
| `45` | Some lines refused | Each line's own reason is listed in `detail`. |
| `602` | Item code already registered | Use `operationType: "102"` to update it, or a new code. |
| `676` | Fixed-amount excise product without piece units | Send `have_piece_unit: "101"` and the piece fields — see [Excise](#products-with-excise-duty). |
| `1115` | Seller email missing | Your provider must add your business email. |
| `1142` | Customer TIN missing | Send `customer_tin`, or sell to an individual. |
| `1462` | Credit note excise differs from the original | Send the same excise fields as the original line. |
| `1464` | Credit note line differs from the original | Credit each item as it was sold, and send `original_fdn`. |
| `2091` | Item out of stock | Record the stock received first. |
| `2122` | Item not registered | Register the product first. |
| `2243` | Wrong unit of measure | Use the product's registered unit. |

### Other failures

| Symptom | What to do |
| --- | --- |
| `401` "Invalid API key or API access disabled" | Check the key; it may have been replaced or disabled. |
| `403` "license expired…" | Contact your provider. |
| `502` or `500` "Could not reach URA's EFRIS system…" | URA is down or busy. Retry queries later; for sales, see [Reliability rules](#10-reliability-rules). |
| `500` with a setup message | Your account is not fully set up. Contact your provider. |
| `500` with a reference | Quote the reference to your provider. |

---

## Appendix — code tables

### Buyer types

| Code | Meaning | TIN |
| --- | --- | --- |
| `0` | Business (B2B) | Required |
| `1` | Individual (B2C) — default | — |
| `2` | Foreigner | — |
| `3` | Government (B2G) | Send it |

### Payment methods

| Code | Meaning |
| --- | --- |
| `101` | Credit — default |
| `102` | Cash |
| `103` | Cheque |
| `104` | Demand draft |
| `105` | Mobile money |
| `106` | Visa / Mastercard |
| `107` | EFT |
| `108` | POS |
| `109` | RTGS |
| `110` | SWIFT transfer |

The table is a guide. `GET /payment-methods` returns URA's current list, for your
system's payment dropdown:

```json
{
  "success": true,
  "result": [
    { "code": "101", "name": "Credit" },
    { "code": "102", "name": "Cash" }
  ],
  "returnCode": "00"
}
```

### Tax rates

| Send | Meaning | Category |
| --- | --- | --- |
| `18` | Standard VAT | `01` |
| `0` | Zero-rated | `02` |
| `"-"` | Exempt | `03` |
| `18` with `deemed_flag: "1"` | Deemed VAT | `04` |
| leave out, or `0` — receipts | No VAT (business not registered for VAT) | `11` |

On a tax invoice every item needs one of the first four; an item with none is
refused.

### Invoice industry codes

| Code | Meaning |
| --- | --- |
| `101` | General — default |
| `102` | Export goods |
| `112` | Export service |

### Excise units

| Code | Meaning |
| --- | --- |
| `101` | Per stick |
| `102` | Per litre |
| `103` | Per kg |
| `104` | Per user per day |
| `105` | Per minute |
| `106` | Per 1,000 sticks |
| `107` | Per 50 kg |

The full list is in `GET /units-of-measure`.

### Deemed exempt codes

| Code | Meaning |
| --- | --- |
| `101` | Strategic investor |
| `102` | Petroleum licensee |
| `103` | Aid-funded project contractor |
| `104` | Government MDA |
| `105` | VAT and excise duty exempt |
| `106` | Excise duty exempt |
| `107` | Mining licensee |
| `108` | EACOP licensee |
| `109` | EACOP level 1 contractor |

### Yes / no flags

| Where | Yes | No |
| --- | --- | --- |
| Product registration (`have_excise_tax`, `have_piece_unit`) | `101` | `102` |
| Invoice lines (`excise_flag`, `deemed_flag`) | `1` | `2` |
| Invoice line `discountFlag` (in responses) | `1` has a discount · `0` is the discount line · `2` none | |
