# OneInvoice Integration API — Developer Guide

**Base URL:** `https://api.oneinvoice.com/v1` · **Interactive reference (Swagger):** `https://api.oneinvoice.com/docs` · **OpenAPI JSON:** `https://api.oneinvoice.com/docs-json`

One host serves every country. Your credential belongs to one tenant, the tenant belongs to one country, and the API routes every call to that country's platform. Country-specific data (TRN / VATIN / GSTIN, HSN codes, Peppol vs IRP) rides on the same resources — there are no per-country endpoints.

## 1. Authentication

**API key (recommended for ERPs).** Created by a tenant or platform administrator in the portal: *Administration → Integration → Create key*. Shown once. Send it on every request:

```
X-Api-Key: oi_live_97d05602_…
```

A key carries **scopes** (the permissions it may exercise). The presets: *Everything*, *Invoices* (create, submit, read, cancel), *Read only*, *Purchases*, *Reports*, or a custom pick. A call outside the key's scopes answers `403 APIKEY.SCOPE_MISSING`. Keys can be **disabled** (takes effect within a minute), **re-enabled** and **revoked** (final) from the same page.

**OAuth 2 bearer (for signed-in users and platform tools).** `Authorization: Bearer <access token>` from the OneInvoice identity provider; the token's own permissions apply.

## 2. Conventions

- **JSON** in and out, UTF-8. Dates are `YYYY-MM-DD`; timestamps ISO-8601 UTC. Money as decimal **strings** (`"262.50"`), never floats.
- **Idempotency:** send `Idempotency-Key: <unique per operation>` on every POST. The same key with the same body returns the first result for 24 hours; the same key with a different body is treated as a new request.
- **Correlation:** every response carries `x-correlation-id`; quote it when raising a ticket.
- **Errors** are `application/problem+json`: `{ "type", "title", "status", "detail", "code", "retryable", "instance" }`. `code` is stable — branch on it, not on `detail`. Common: `APIKEY.UNKNOWN / DISABLED / REVOKED / EXPIRED` (401), `APIKEY.SCOPE_MISSING` (403), `PLATFORM.PERMISSION_DENIED` (403), `INVOICE.NOT_FOUND` (404), `INVOICE.INVALID` (422, with `issues[]`), `INVOICE.IMMUTABLE` (409), `GATEWAY.UPSTREAM_UNREACHABLE` (502, retryable).
- **Rate limit:** 600 requests per minute per key; `429` with `Retry-After` beyond it.
- **Paging:** list endpoints take `limit` (≤ 200) and `cursor`; the response carries `nextCursor` (null at the end).

## 3. The invoice lifecycle

```
DRAFT ──validate──▶ VALIDATED ──approve──▶ APPROVED ──send──▶ TRANSMITTING ──▶ DELIVERED
                                                            │                    │
   (invalid: stays DRAFT, issues[] returned)                └──▶ REJECTED ◀──────┘ (buyer refusal / transmission failure)
India: APPROVED ──register──▶ CLEARANCE_REQUESTED ──▶ DELIVERED (IRN issued) | REJECTED (IRP refusal)
```

`POST /invoices/{id}/submit` runs validate → approve → send (or register) in one call and returns the invoice; an invalid document answers **422** with `issues[]` and the draft is unchanged. Individual steps are also available as `…/validation`, `…/approval`, `…/transmission` if you prefer to drive them separately.

Statuses per country:
- **UAE / Oman (Peppol):** `status` above plus `peppol` (SUBMITTED → DELIVERED / REJECTED), `mls` (buyer's message-level status), `taxData` (submitted to the FTA / OTA, accepted / rejected).
- **India (IRP):** `clearance` — `status` CLEARED with `irn`, `ackNo`, `ackDate`, `signedQr`; or REJECTED with the IRP `errorCode` / `errorMessage`.

## 4. Resources

### 4.1 Invoices — `/invoices`

| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | `/invoices` | invoice:invoice:create | Create a draft (invoice, credit note, debit note) |
| GET | `/invoices` | invoice:invoice:read | List (`status`, `from`, `to`, `organizationId`, `cursor`, `limit`) |
| GET | `/invoices/{id}` | invoice:invoice:read | The invoice with its status |
| POST | `/invoices/{id}/submit` | invoice:invoice:approve (+send) | Validate, approve, send / register |
| GET | `/invoices/{id}/status` | invoice:invoice:read | Lifecycle status in one object |
| GET | `/invoices/{id}/document` | invoice:invoice:read | The frozen document: UBL XML, or INV-01 JSON for India |
| POST | `/invoices/{id}/cancel` | invoice:invoice:void | Cancel a draft / approved invoice; India: cancel the IRN within 24 h |
| POST | `/invoices/{id}/credit-note` | invoice:invoice:create | Raise a credit note against an approved / delivered invoice |

**Create — request body** (the same JSON the portal's *Import JSON* accepts):

```json
{
  "organizationId": "01M0PKBPQGFV6MGN19RAPCTH7H",
  "type": "COMMERCIAL_INVOICE",
  "number": "INV-2026-000123",
  "issueDate": "2026-09-13",
  "dueDate": "2026-10-13",
  "currency": "AED",
  "countryCode": "AE",
  "supplier": {
    "legalName": "United Technologies LLC",
    "endpointSchemeId": "0235", "endpointValue": "100100309312345",
    "taxRegistrations": [{ "identifier": "100100309312345", "scheme": "VAT" }],
    "legalRegistrationId": "CN-1234567", "legalRegistrationScheme": "TL",
    "address": { "line1": "Office 1204, Business Bay", "city": "Dubai", "countrySubdivision": "AE-DU", "postalCode": "00000", "countryCode": "AE" }
  },
  "customer": {
    "legalName": "OneInvoice Test Customer LLC",
    "endpointSchemeId": "0235", "endpointValue": "100100333312345",
    "taxRegistrations": [{ "identifier": "100100333312345", "scheme": "VAT" }],
    "address": { "line1": "Al Maryah Island", "city": "Abu Dhabi", "countrySubdivision": "AE-AZ", "countryCode": "AE" }
  },
  "lines": [
    { "id": "1", "name": "Consulting services", "quantity": "10", "unitCode": "HUR", "unitPrice": "250.00",
      "taxCategory": { "code": "S", "percent": "5" }, "commodityCode": "" }
  ],
  "paymentMeansCode": "30",
  "note": "Thank you for your business"
}
```

Country variants: **Oman** — `currency: "OMR"`, `countryCode: "OM"`, VATIN `OM1100077496` as the tax registration, addresses in governorates (`OM-MA` …), Peppol scheme `0248`. **India** — `currency: "INR"`, `countryCode: "IN"`, GSTIN as the tax registration with `scheme: "GST"`, state as `countrySubdivision` (`IN-KA` …), **`commodityCode` (HSN/SAC) required on every line**, `taxCategory.percent` a GST rate (5, 12, 18, 28 …); no Peppol endpoint. Optional on every country: `channel` (`PEPPOL` default, `DIRECT` = you deliver the document yourself, the platform still reports tax data), `buyerKind` (`BUSINESS` default, `CONSUMER` for B2C).

**Response** (201): the invoice as stored — `id`, `status: "DRAFT"`, totals computed (`totals.taxExclusiveAmount`, `taxTotalAmount`, `payableAmount`), `lines[]` with `netAmount`, and `createdAt`.

**Status — response** (`GET /invoices/{id}/status`):

```json
{ "id": "01M…", "number": "INV-2026-000123", "status": "DELIVERED", "channel": "PEPPOL", "countryCode": "AE",
  "validatedAt": "2026-09-13T08:01:02Z", "transmittedAt": "2026-09-13T08:01:40Z", "rejectionReason": null,
  "clearance": null }
```

### 4.2 Received invoices (purchases) — `/received-invoices`

| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | `/received-invoices` | accesspoint:received-document:read | Documents received for your tenant (`status`, `cursor`, `limit`) |
| GET | `/received-invoices/{id}` | same | The document with its business content |
| POST | `/received-invoices/{id}/accept` | accesspoint:received-document:decide | Accept — issues the positive MLS to the sender |
| POST | `/received-invoices/{id}/reject` | same | Reject with `{ "reason": "…" }` — issues the negative MLS |

### 4.3 Reports — `/reports`

`GET /reports/transactions`, `/reports/tax-data`, `/reports/peppol`, `/reports/mls`, `/reports/failures`, `/reports/dashboard` — scope `reporting:report:read`. Filters: `range` (`last7`, `last30`, `last90`, `thisMonth`, or `from`/`to`), `direction`, per-report statuses, `q` (UUID / number / correlation id), `page`, `pageSize` (≤ 200), `sort`, `sortDir`. Responses are `{ items, page, pageSize, total }`.

## 5. Webhooks (next release)

Register an endpoint and secret; the platform delivers `invoice.validated`, `invoice.delivered`, `invoice.rejected`, `mls.received`, `invoice.cleared`, `tax_data.accepted` as signed POSTs (`X-OneInvoice-Signature: sha256=…` over the raw body), retried with backoff for 24 hours, with a delivery log in the portal. Until then, poll `GET /invoices/{id}/status`.

## 6. Quick start

```bash
# 1. list
curl -H "X-Api-Key: $KEY" "https://api.oneinvoice.com/v1/invoices?limit=5"
# 2. create
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -H "Idempotency-Key: erp-000123" \
  --data @invoice.json "https://api.oneinvoice.com/v1/invoices"
# 3. submit (validate + approve + send)
curl -X POST -H "X-Api-Key: $KEY" -H "Idempotency-Key: erp-000123-submit" "https://api.oneinvoice.com/v1/invoices/$ID/submit"
# 4. status / document
curl -H "X-Api-Key: $KEY" "https://api.oneinvoice.com/v1/invoices/$ID/status"
curl -H "X-Api-Key: $KEY" "https://api.oneinvoice.com/v1/invoices/$ID/document" -o invoice.xml
```

## 7. Environments

Staging: `https://api.oneinvoice.com` (this document). Production per country will use the same host; the key decides the tenant and therefore the country. Test with a key on a test tenant; never reuse a production key in a test system.
