# Simple EFD API integration guide

Base URL: **https://sandbox.efdsimple.co.tz/api/v2**

Request your sandbox email, password and branch ID from your Simple EFD integration representative. Each integration receives an isolated test company. No taxpayer registration or TRA submission occurs in this environment.

## Client workflow

1. `POST /session` with `email`, `password` and `device_name`. A successful `201`
   reply contains `token`, `token_type: "Bearer"`, `expires_at`, `company_id` and
   `abilities`. Tokens expire after 30 days; `DELETE /session` revokes the caller's
   token immediately. Sign in with the test user's email and password.
2. Send `Accept: application/json` and `Authorization: Bearer TOKEN` on subsequent
   requests. Send `Content-Type: application/json` for JSON request bodies.
3. `GET /branches` discovers assigned branch IDs. IDs are server-owned; never
   assume an ID or use another company's branch.
4. `GET /bootstrap?branch_id=ID` returns readiness, buyer identification types,
   payment choices and tax codes.
5. `POST /receipts/preview` with the sale facts below. This does not save a
   receipt or consume a receipt number. It returns authoritative totals and
   `data.review_hash`.
6. `POST /receipts` with the same sale, a stable `Idempotency-Key` and optionally
   `X-Receipt-Review-Hash` from preview. The example below sends both headers.
7. Read the returned `data.id` through `GET /receipts/{receipt}` or the stored
   print snapshot at `GET /receipts/{receipt}/print`.

Example sign-in (replace PASSWORD with the password provided with your sandbox access):

```sh
curl -i https://sandbox.efdsimple.co.tz/api/v2/session \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"email":"YOUR_SANDBOX_EMAIL","password":"PASSWORD","device_name":"My integration"}'
```

Save this as `sale.json`, replacing `branch_id` with the discovered ID:

```json
{
  "branch_id": 1,
  "customer_id_type": 6,
  "customer_name": "SANDBOX BUYER",
  "payment_type": "CASH",
  "items": [
    {
      "name": "SANDBOX ITEM",
      "quantity": "1.00",
      "unit_price": "118.00",
      "discount": "0.00",
      "tax_code": "A"
    }
  ]
}
```

```sh
curl -i https://sandbox.efdsimple.co.tz/api/v2/receipts/preview \
  -H 'Authorization: Bearer TOKEN' -H 'Content-Type: application/json' \
  --data-binary @sale.json

curl -i https://sandbox.efdsimple.co.tz/api/v2/receipts \
  -H 'Authorization: Bearer TOKEN' -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: my-sandbox-sale-000001' \
  -H 'X-Receipt-Review-Hash: HASH_FROM_PREVIEW' \
  --data-binary @sale.json
```

### Sale fields and amounts

| Field | Contract |
| --- | --- |
| `branch_id` | Assigned positive integer branch ID. |
| `customer_id_type` | Integer: 1 TIN, 2 driving licence, 3 voter ID, 4 passport, 5 national identity (NID), 6 no ID. |
| `customer_id` | Required unless type 6; string, 5–30 characters. |
| `customer_name` | Required string, 2–100 characters. |
| `customer_mobile` | Optional; null/omitted or a 10-digit string. |
| `customer_vrn` | Optional; null/omitted or a 9-digit string. |
| `invoice_number` | Optional string, maximum 20 characters. |
| `payment_type` | `CASH` cash, `CHEQUE` cheque, `CCARD` credit card, `EMONEY` electronic/mobile money, `INVOICE` invoice. Send the exact uppercase string. |
| `items` | Ordered JSON array, 1–500 lines. |
| `items[].name` | Required nonempty string, maximum 100 characters. |
| `items[].quantity` | Required positive decimal **string**, up to two fractional digits. |
| `items[].unit_price` | Required decimal **string**, up to two fractional digits. |
| `items[].discount` | Required decimal **string**, up to two fractional digits; a line amount. |
| `items[].tax_code` | `A`, `C`, `D` or `E`; sandbox registration has A = 18%, C/D/E = 0%. |

For ID types 1–5, send `customer_id` as a 5–30 character string; for type 6,
omit it or send null. `GET /bootstrap?branch_id=YOUR_BRANCH_ID` returns
`id_types`, `payment_types` and `tax_codes` for building the client's choices.
Tax rates above describe the sandbox's VAT-enabled registration. Non-VAT test
accounts do not charge VAT. Production classifications and rates follow the
business's configured registration; use receipt preview for calculated amounts.

Prices are tax inclusive. For the example, the server derives inclusive `118.00`,
exclusive `100.00`, tax `18.00` and discount `0.00`. Quantity and money must be
strings, not JSON floating-point numbers. The shared writer also checks ranges,
positive quantities, discount limits and fiscal text. Do not submit fiscal
numbers, VAT totals, acceptance status, verification codes or client-computed
totals as authoritative input. Receipt money responses are decimal strings.

### Retries and conflicts

Use a distinct key per new sale, 16–128 ASCII characters matching
`[A-Za-z0-9][A-Za-z0-9._:-]{15,127}`. Persist the key and sale before sending.
After a timeout or lost reply, send **the same key and same sale**. Never mint a
new key just because a response was lost.

First creation returns `201` and `Idempotent-Replayed: false`; an identical retry
returns `200`, the same receipt ID and `Idempotent-Replayed: true`. Reusing the
key with changed sale details returns `409`. Keys are company-wide, so changing
branches must not create a duplicate. An outdated review hash also returns `409`.
After `503` or an uncertain response, reconcile by retrying the original key and
facts; inspect the receipt before treating the sale as confirmed.

### Available endpoints

All paths below are relative to `/api/v2`. Exact request/response schemas are in
[`openapi.json`](openapi.json), derived from the modern API contract.

| Method | Path | Purpose |
| --- | --- | --- |
| POST / GET / DELETE | `/session` | Sign in, inspect current identity, revoke current token. |
| GET | `/branches` | Assigned branches in the current company. |
| GET | `/bootstrap?branch_id=…` | Sale readiness and choices. |
| GET / POST | `/customers` | List/create saved buyers. |
| GET / PUT | `/customers/{customer}` | Read/update saved buyer. |
| POST | `/customers/{customer}/deactivate` | Deactivate a buyer. |
| GET / POST | `/items` | List/create saved items. |
| GET / PUT | `/items/{item}` | Read/update saved item. |
| POST | `/items/{item}/deactivate` | Deactivate an item. |
| POST | `/receipts/preview` | Calculate/review sale without saving. |
| GET / POST | `/receipts` | History/create a pending synthetic receipt. |
| GET | `/receipts/{receipt}` | Stored receipt detail. |
| GET | `/receipts/{receipt}/print` | Stored print data with simulation notice. |
| GET | `/zreports`, `/zreports/{zreport}` | Read daily-close history. |

Creating customers/items also requires `Idempotency-Key`; both a first create and
an exact replay return `201`, distinguished by `Idempotent-Replayed`. Updating
or deactivating directory records requires the current integer `version`; stale
versions return `409`. Saved buyers/items are conveniences: receipt creation
still accepts the sale facts and freezes its own snapshot.

Branches, customers and items paginate forwards using `after_id`,
`next_after_id` and `has_more`. Receipts and Z-reports paginate backwards using
`before_id`, `next_before_id` and `has_more`. `limit` is 1–100, default 50. Follow
the returned cursor until `has_more` is false. Z-report history starts empty:
there is no sandbox daily-close creation or filing endpoint in this release.

### Status codes and test cases

| Code | Meaning / useful test |
| --- | --- |
| 200 | Successful read/review/update/logout or identical receipt replay. |
| 201 | Session, directory entry or receipt created. |
| 401 | Missing, incorrect, expired or revoked bearer/credentials. |
| 403 | Current account/permission does not allow the operation. |
| 404 | Unknown record, another company's record, or unavailable endpoint. |
| 409 | Changed retry, changed review, stale directory version or inconsistent print amounts. |
| 422 | Invalid request; inspect `message` and field-level `errors` when present. |
| 429 | Rate limit; honor `Retry-After`. Login has 5/minute per IP, receipt create 30/minute, general API 60/minute. |
| 503 | Runtime unavailable or receipt/registration needs review. |

Errors use HTTP status codes and JSON `message`, with `errors` for validation.
IDs outside your assigned test company must not be accessible. A revoked token must immediately return 401.


## Production access

After integration testing and business onboarding, Simple EFD provides a production base URL and separate credentials. Configure the issued production values; sandbox credentials and tokens cannot access production. Do not replay sandbox test sales against production.
