# Receiving purchase invoices

Inbound flow: poll the purchase-invoice list with a `since` cursor → fetch detail (and the original file if needed) → import into the ERP → `mark-exported`. Receiving is **pull-based**: there are no webhooks anywhere in this API. Poll on a schedule (every 1–15 minutes is typical for an ERP; never faster than every 2 seconds).

## Statuses and sources

```
NEW → SEEN → EXPORTED          (DIGITIZED_PENDING for digitized documents, see below)
```

| Status | Meaning |
|---|---|
| `NEW` | Received, not yet looked at through the API |
| `SEEN` | Detail was fetched via `GET /partner/v1/purchase-invoices/{id}` |
| `EXPORTED` | Confirmed imported into the ERP via `mark-exported` — the end state |
| `DIGITIZED_PENDING` | Created by AI digitization, extraction awaiting your confirmation flow ([digitization](https://sandbox.bilnex.io/docs/digitization.md)) |

| `source` | Meaning |
|---|---|
| `E_INVOICE` | Arrived over the Estonian operator network |
| `PEPPOL` | Arrived over Peppol |
| `DIGITIZED` | Extracted from a PDF by Bilnex. Not uploadable through the Partner API in v1 — see [digitization](https://sandbox.bilnex.io/docs/digitization.md) |

## 1. Poll — `GET /partner/v1/purchase-invoices?since=&status=&page=`

```bash
curl -s "https://sandbox.bilnex.io/partner/v1/purchase-invoices?since=2026-08-06T00:00:00Z" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "X-Company-Id: <RECEIVER_COMPANY_ID>"
```

```json
{
  "items": [
    {
      "id": "d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15",
      "status": "NEW",
      "source": "E_INVOICE",
      "sender": { "name": "Valge Klaar OÜ", "regCode": "95000002" },
      "invoiceNumber": "2026-0001",
      "date": "2026-08-06",
      "dueDate": "2026-08-20",
      "currency": "EUR",
      "netAmount": 950.00,
      "vatAmount": 228.00,
      "totalAmount": 1178.00,
      "receivedAt": "2026-08-06T09:06:31Z"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "hasMore": false,
  "nextSince": "2026-08-06T09:06:31Z"
}
```

Flips `PURCHASE_LISTED` (and `EINVOICE_RECEIVED_AT_RECEIVER` when an `E_INVOICE`-sourced document sent from your sandbox sender appears in the list).

**Cursor pattern.** `since` filters by `receivedAt` (exclusive of documents at or before the instant already covered). The response's `nextSince` is the cursor for the next poll — persist it and pass it back verbatim. On the first ever poll, use a date safely in the past (e.g. `2026-01-01T00:00:00Z`).

**Pagination.** Within one poll, while `hasMore` is `true`, request the next `page` (1-based, page size 50) with the SAME `since`. Only advance your stored cursor to `nextSince` after the last page.

**Dedupe by `id`.** The cursor is time-based; a document can appear twice across polls at the boundary. Keep a processed-ids record (or a unique constraint on the Bilnex id in your ERP) and skip duplicates silently.

`status=` filters (e.g. `status=NEW` to see only unprocessed documents). Note: filtering by `NEW` alone misses `DIGITIZED_PENDING` documents; cursor-poll everything and branch on status instead.

## 2. Detail — `GET /partner/v1/purchase-invoices/{id}`

```bash
curl -s https://sandbox.bilnex.io/partner/v1/purchase-invoices/d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15 \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "X-Company-Id: <RECEIVER_COMPANY_ID>"
```

```json
{
  "id": "d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15",
  "status": "SEEN",
  "source": "E_INVOICE",
  "sender": {
    "name": "Valge Klaar OÜ",
    "regCode": "95000002",
    "iban": "EE382200221020145685"
  },
  "invoiceNumber": "2026-0001",
  "date": "2026-08-06",
  "dueDate": "2026-08-20",
  "currency": "EUR",
  "items": [
    {
      "description": "Consulting services, July 2026",
      "quantity": 10.00,
      "unit": "h",
      "price": 95.00,
      "vatRate": 24,
      "totalPrice": 950.00
    }
  ],
  "netAmount": 950.00,
  "vatAmount": 228.00,
  "totalAmount": 1178.00,
  "receivedAt": "2026-08-06T09:06:31Z"
}
```

Fetching detail moves a `NEW` document to `SEEN` (visible in this very response). Detail adds `items[]` and the sender's `iban` on top of the list fields.

## 3. Original file — `GET /partner/v1/purchase-invoices/{id}/original`

Returns a short-lived URL to the source document: the e-invoice XML for `E_INVOICE`/`PEPPOL`, the uploaded source PDF for `DIGITIZED`.

```bash
curl -s https://sandbox.bilnex.io/partner/v1/purchase-invoices/d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15/original \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "X-Company-Id: <RECEIVER_COMPANY_ID>"
```

```json
{
  "url": "https://sandbox.bilnex.io/files/purchase/d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15.xml?sig=8b42e3a97c0d15f6&exp=1786360282",
  "expiresAt": "2026-08-06T09:26:22Z"
}
```

Download immediately; store bytes, not the URL. Attach the original to the ERP document for audit.

## 4. Confirm the import — `POST /partner/v1/purchase-invoices/{id}/mark-exported`

MANDATORY after every successful import into the ERP. This is how Bilnex (and the customer's Bilnex UI) knows the document reached accounting — skipping it leaves documents looking unprocessed forever.

```bash
curl -s -X POST https://sandbox.bilnex.io/partner/v1/purchase-invoices/d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15/mark-exported \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "X-Company-Id: <RECEIVER_COMPANY_ID>"
```

```json
{
  "id": "d94b7f2a-1c5e-4a8b-9e3d-6f0a2b7c8d15",
  "status": "EXPORTED",
  "exportedAt": "2026-08-06T09:08:02Z"
}
```

Idempotent: repeating the call returns 200 with the same `exportedAt`. Order your importer so `mark-exported` runs only after the ERP write is durable — if the ERP write fails, do not mark; the document stays visible for the next attempt. Flips `MARKED_EXPORTED`.

## Testing inbound without a full send

Two ways to get documents into the receiver's list in sandbox:

1. **Real path** — send an `E_INVOICE`/`PEPPOL` invoice from the sender company to the receiver company (regCode `95000003`); it arrives instantly.
2. **Trigger** — `POST /partner/v1/sandbox/trigger` with `{"event":"inbound_einvoice"}` or `{"event":"inbound_peppol"}` (with `X-Company-Id: <RECEIVER_COMPANY_ID>`) injects a document from the Sandbox Capable OÜ fixture. See [sandbox](https://sandbox.bilnex.io/docs/sandbox.md).

## Reference importer loop

```
cursor = load_cursor() or "2026-01-01T00:00:00Z"
page = 1
loop:
  resp = GET /purchase-invoices?since={cursor}&page={page}
  for doc in resp.items:
    if already_imported(doc.id): continue
    detail = GET /purchase-invoices/{doc.id}
    erp_write(detail)                      # durable ERP insert first
    POST /purchase-invoices/{doc.id}/mark-exported
    record_imported(doc.id)
  if resp.hasMore: page += 1; continue
  save_cursor(resp.nextSince); page = 1
  sleep(poll_interval)                     # >= 2 seconds; minutes in production
```

API version 2026-08-01. /partner/v1 changes are additive-only; see [versioning-policy](https://sandbox.bilnex.io/docs/versioning-policy.md).

---
**Building with a coding agent?** Start from [agents.md](https://sandbox.bilnex.io/agents.md) or install the skill: `npx skills add https://sandbox.bilnex.io`. Machine-readable index: [llms.txt](https://sandbox.bilnex.io/llms.txt). Every page on this site is also plain markdown — append `.md`.
