# Integration Guide: eReporting

The eReporting endpoints expose the aggregated B2C sales data required by the French e-reporting obligation (RFE), per restaurant and per business date, so that a POS integrator or an accounting firm can rebuild its own e-reporting and closure reports from the Innovorder platform.

#### Response format

Unlike the rest of this API, the eReporting endpoints answer the payload **directly**, without the `{ status, code, message, data }` envelope: the body of a 200 is the report itself. Errors keep the platform-wide envelope described at the bottom of this page.

#### Query parameters are strict

Each route accepts exactly the parameters listed on its card and nothing else: an unknown query key is refused with a `400 invalid_parameters`, the request never reaching the reporting platform. In particular do not send a `brandId`: the API resolves the brand itself from `restaurantId` in its own referential, and one supplied by the caller is never read.

Dates are validated on their **shape only** (four digits, two, two), not as calendar dates. A value such as `2026-02-31` therefore clears this API's validation and is refused by the reporting platform instead, which surfaces as `io_data_platform_request_rejected` carrying the platform's own status rather than as `invalid_parameters`. Send real dates and you never meet the distinction; rely on the error code to tell the two layers apart when you do.

### 1\. Authentication

Authenticate with your **Service Account** and send the returned `access_token` as `Authorization: Bearer <access_token>` on every call. These endpoints require that Bearer token, because the token is what identifies you to the reporting platform. An `X-API-Key` credential is not accepted on them: sending the `X-API-Key` header is refused even when an `Authorization` header accompanies it, so a client migrated to API keys must call these four routes with the Bearer token only. The scheme itself is case-insensitive.

A **Brand**, **Brand Group** or **Restaurant** account may read these reports, each within its own perimeter. Any other identity, such as a POS or kiosk device token or a customer token, is refused with a `401 permission_denied` before the reporting platform is called.

### `POST /oauth/login` - Get Access Token

Authenticate using your service account credentials. The access token is valid for 24 hours (14 days when rememberMe is true); when it expires the API responds with a 401 error (token\_expired) and you must authenticate again.

#### Request Body

```json
{
  "username": "service.account@brand.com",
  "password": "YOUR_SECURE_PASSWORD",
  "grant_type": "password"
}
```

##### Request Body Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| username | string | "service.account@brand.com" | The username value. |
| password | string | "YOUR\_SECURE\_PASSWORD" | The password value. |
| grant\_type | string | "password" | The grant type value. |

#### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1...",
  "status": 201,
  "code": "token_created",
  "message": "You have successfully logged in.",
  "data": {
    "user": {
      "userId": 1,
      "email": "service.account@brand.com",
      "role": "brand",
      "brandId": 100
    }
  },
  "token_type": "Bearer"
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| access\_token | string | "eyJhbGciOiJIUzI1..." | The access token value. |
| status | integer | 201 | HTTP status code returned by the API. |
| code | string | "token\_created" | Machine-readable application code for the result. |
| message | string | "You have successfully logged in." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.user | object | {…} | Object containing user fields. |
| data.user.userId | integer | 1 | Identifier of the user. |
| data.user.email | string | "service.account@brand.com" | Email address. |
| data.user.role | string | "brand" | The role value. |
| data.user.brandId | integer | 100 | Identifier of the brand. |
| token\_type | string | "Bearer" | The token type value. |

### 2\. Daily Report

Aggregated sales and payments for one restaurant and one business date. `orders[]` holds one entry per fiscal category present that day (`GOODS_VAT` for supply of goods, `SERVICES_VAT` for provision of services), each with its VAT breakdown; `payments[]` holds one entry per payment method and VAT rate, the VAT being allocated pro-rata across each order's payments. A day without activity returns a valid payload with empty arrays, never a 404.

### `GET /v1/ereporting/reports` - Get the daily e-reporting report

Aggregated sales and payments for one restaurant and one business date.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Innovorder restaurant identifier. |
| date | string | Yes | Business date (YYYY-MM-DD), local to the restaurant. |

#### Response

```json
{
  "date": "2026-09-03",
  "restaurantId": "123",
  "reconciled": true,
  "orders": [
    {
      "category": "SERVICES_VAT",
      "currency": "EUR",
      "total": 1234.5,
      "taxAmount": 112.23,
      "totalOrders": 87,
      "taxDueDateTypeCode": "432",
      "taxes": [
        {
          "taxRate": 10,
          "taxAmount": 112.23,
          "totalExclTax": 1122.27,
          "totalInclTax": 1234.5
        }
      ]
    }
  ],
  "payments": [
    {
      "paymentMethodId": "CreditCard",
      "paymentMethodName": "Carte bancaire",
      "currency": "EUR",
      "taxRate": 10,
      "taxAmount": 80.5,
      "total": 885.5
    }
  ]
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Required | Example | Description |
| --- | --- | --- | --- | --- |
| date | string | Yes | "2026-09-03" | The business date that was requested, echoed back as YYYY-MM-DD. |
| restaurantId | string | Yes | "123" | The restaurant that was requested. Returned as a string, even though the query parameter is an integer. |
| reconciled | boolean | Yes | true | False when the day's control totals did not balance. The figures are still served as computed. |
| orders | array | Yes | \[…\] | Sales aggregates, one entry per fiscal category present that day. These are category totals, not individual orders: use the closure orders endpoint for order-level detail. Empty on a day without activity. |
| orders\[\] | object | Yes | {…} | The aggregate of one fiscal category. |
| orders\[\].category | string | Yes | "SERVICES\_VAT" | Fiscal category: GOODS\_VAT for supply of goods, SERVICES\_VAT for provision of services. |
| orders\[\].currency | string | Yes | "EUR" | ISO 4217 code of the amounts in this entry. |
| orders\[\].total | number | Yes | 1234.5 | Total including tax for the category, with 2 decimal places. |
| orders\[\].taxAmount | number | Yes | 112.23 | VAT included in that total, and the sum of taxes\[\].taxAmount. |
| orders\[\].totalOrders | integer | Yes | 87 | Number of orders aggregated into the category. |
| orders\[\].taxDueDateTypeCode | string | Yes | "432" | VAT point date code: 35 (delivery date), 432 (paid-to date / encaissement) or 3 (invoice date / debits). |
| orders\[\].taxes | array | Yes | \[…\] | VAT breakdown of the category, one entry per rate applied. |
| orders\[\].taxes\[\] | object | Yes | {…} | The figures of one VAT rate within the category. |
| orders\[\].taxes\[\].taxRate | number | Yes | 10 | VAT rate as a percentage, e.g. 10 for 10%. |
| orders\[\].taxes\[\].taxAmount | number | Yes | 112.23 | VAT collected at that rate. |
| orders\[\].taxes\[\].totalExclTax | number | Yes | 1122.27 | Taxable base at that rate. |
| orders\[\].taxes\[\].totalInclTax | number | Yes | 1234.5 | Base plus VAT at that rate. |
| payments | array | Yes | \[…\] | Payment aggregates, one entry per payment method AND VAT rate: a method that settled sales at several rates appears once per rate. Empty on a day without activity. |
| payments\[\] | object | Yes | {…} | What one payment method collected at one VAT rate. |
| payments\[\].paymentMethodId | string | Yes | "CreditCard" | Stable identifier of the payment method. Key your own mapping on this, not on the label. |
| payments\[\].paymentMethodName | string | Yes | "Carte bancaire" | Display label of the payment method, which may be renamed or localized. |
| payments\[\].currency | string | Yes | "EUR" | ISO 4217 code of the amounts in this entry. |
| payments\[\].taxRate | number | Yes | 10 | VAT rate this entry accounts for, as a percentage. |
| payments\[\].taxAmount | number | Yes | 80.5 | VAT carried by these payments, allocated pro-rata across each order's payments. |
| payments\[\].total | number | Yes | 885.5 | Amount collected by the method at that rate, including tax. Excludes change given back and e-wallet reloads. |

`taxDueDateTypeCode` is the VAT point date code: `35` (delivery date, the default for `GOODS_VAT`), `432` (paid-to date / encaissement, the default for `SERVICES_VAT`) or `3` (invoice date / débits).

`reconciled` is `false` when the day's control totals did not balance. The payload is still served with the figures as computed. Treat it as a signal to investigate, not as an error.

Amounts are decimal numbers in the payload currency, with 2 decimal places. E-wallet reloads are not sales and are excluded; e-wallet spending appears as a payment method at consumption time. Change given back to the customer is excluded from the payment breakdown.

A restaurant that has since been **closed or deactivated** is a known limitation today: your account's perimeter is resolved from the restaurants that are still active, so such a restaurant is refused with a `403 permission_denied` even though the reporting platform still holds its figures. Export the business dates you need _before_ a site is deactivated, and contact support if you have to retrieve the history of one that already is. A`restaurantId` that exists in no Innovorder referential at all is a different answer:`400 restaurant_not_found`.

### 3\. Closures

Cash-register closures ("clôtures" / Z) for one restaurant and one business date. Several closures per date are possible (one per work slot, or one per day). `status` becomes `closed` once the business date is frozen, at the latest at 04:00 local time the following day. A closure's figures are frozen at closing time; a re-issued closure keeps the same id with a new version.

### `GET /v1/ereporting/closures` - List the closures of a business date

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Innovorder restaurant identifier. |
| date | string | Yes | Business date (YYYY-MM-DD), local to the restaurant. |

#### Response

```json
{
  "date": "2026-09-03",
  "status": "closed",
  "closures": [
    {
      "id": "pec_abc123",
      "total": 1360.03,
      "taxAmount": 123.64,
      "payments": [
        {
          "paymentMethodId": "Cash",
          "paymentMethodName": "Espèces",
          "tip": 0,
          "total": 240
        }
      ],
      "taxes": [
        {
          "taxRate": 10,
          "taxAmount": 123.64,
          "total": 1360.03
        }
      ]
    }
  ]
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Required | Example | Description |
| --- | --- | --- | --- | --- |
| date | string | Yes | "2026-09-03" | The business date that was requested, echoed back as YYYY-MM-DD. |
| status | string | Yes | "closed" | State of the business date itself, not of one closure: open while the date can still receive sales, closed once it is frozen. |
| closures | array | Yes | \[…\] | The closures of that business date, in the order the platform returns them. Empty when the date carries none. |
| closures\[\] | object | Yes | {…} | One cash-register closure (clôture / Z). |
| closures\[\].id | string | Yes | "pec\_abc123" | Closure identifier. Pass it as {closureId} to the orders endpoint below; treat it as opaque. |
| closures\[\].total | number | Yes | 1360.03 | Total including tax of the closure, in the restaurant's currency. |
| closures\[\].taxAmount | number | Yes | 123.64 | VAT included in that total. |
| closures\[\].payments | array | Yes | \[…\] | What each payment method collected in the closure. |
| closures\[\].payments\[\] | object | Yes | {…} | One payment method of the closure. Unlike the daily report, entries are not split per VAT rate and carry no currency field. |
| closures\[\].payments\[\].paymentMethodId | string | Yes | "Cash" | Stable identifier of the payment method. |
| closures\[\].payments\[\].paymentMethodName | string | Yes | "Espèces" | Display label of the payment method. |
| closures\[\].payments\[\].tip | number | Yes | 0 | Tips collected through this method, already included in total. 0 when the method carries none. |
| closures\[\].payments\[\].total | number | Yes | 240 | Amount collected by the method, including tax and tips. |
| closures\[\].taxes | array | Yes | \[…\] | VAT breakdown of the closure, one entry per rate applied. |
| closures\[\].taxes\[\] | object | Yes | {…} | The figures of one VAT rate within the closure. |
| closures\[\].taxes\[\].taxRate | number | Yes | 10 | VAT rate as a percentage. |
| closures\[\].taxes\[\].taxAmount | number | Yes | 123.64 | VAT collected at that rate. |
| closures\[\].taxes\[\].total | number | Yes | 1360.03 | Total including tax at that rate. |

### 4\. Orders of a Closure

The individual orders belonging to a closure, paginated with an opaque cursor. Keep calling with the `nextCursor` of the previous page until it comes back `null`. The orders are resolved from the `closureId` itself; `restaurantId` does not filter the page and is required only to authorize the request. It must still designate a restaurant that exists: an unknown one is refused with `400 restaurant_not_found` before the reporting platform is called.

This route is the one exception to the perimeter check described above: because a closure is not selected by `restaurantId`, it is the reporting platform, and not this API, that decides whether the closure is yours to read. A closure outside your perimeter therefore comes back as `io_data_platform_request_rejected` carrying the platform's own status (`403`for a scope you do not hold, `404` for a closure that does not exist) rather than as `permission_denied`. For the same reason the deactivated-restaurant limitation of the daily report does not apply here: nothing on this route refuses a closed site locally.

### `GET /v1/ereporting/closures/{closureId}/orders` - Paginate the orders of a closure

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| closureId | string | Yes | Closure identifier, as returned by GET /v1/ereporting/closures. Up to 128 characters, and never made only of dots (such a value is refused with a 400); percent-encode it if it contains a slash. |
| restaurantId | integer | Yes | Innovorder restaurant identifier. |
| cursor | string | No | Opaque cursor returned by the previous page. |
| limit | integer | No | Page size, between 1 and 500 (defaults to 100). |

#### Response

```json
{
  "closureId": "pec_abc123",
  "orders": [
    {
      "orderId": "ord_78910",
      "timestamp": "2026-09-03T12:41:05+02:00",
      "currency": "EUR",
      "totalInclTax": 15.6,
      "taxAmount": 1.42
    }
  ],
  "nextCursor": null
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Required | Example | Description |
| --- | --- | --- | --- | --- |
| closureId | string | Yes | "pec\_abc123" | The closure whose orders are returned, echoed back. |
| orders | array | Yes | \[…\] | The orders of this page. Individual orders here, unlike the category aggregates of the daily report. |
| orders\[\] | object | Yes | {…} | One order attached to the closure. |
| orders\[\].orderId | string | Yes | "ord\_78910" | Identifier of the order in the reporting platform. It is not the Innovorder orderId of the Orders API. |
| orders\[\].timestamp | string | Yes | "2026-09-03T12:41:05+02:00" | ISO-8601 date-time of the order, with the restaurant's UTC offset. |
| orders\[\].currency | string | Yes | "EUR" | ISO 4217 code of the amounts in this entry. |
| orders\[\].totalInclTax | number | Yes | 15.6 | Total of the order including tax. |
| orders\[\].taxAmount | number | Yes | 1.42 | VAT included in that total. |
| nextCursor | string | Yes | null | Opaque cursor of the next page, to send back as the cursor parameter. Null on the last page, which is the only way to know the pagination is over. |

### 5\. File Export over a Period

The same figures as the daily report, for every business date of a range, delivered as a file rather than as JSON in the body: `format=json` returns one array of daily payloads, `format=csv` returns a ZIP of two CSVs (transactions and payments).

The bounds of the range are not checked by this API: neither its width nor `startDate`actually preceding `endDate` is validated here. Both rules belong to the reporting platform, which caps a range at 92 days, so a range it refuses comes back as `io_data_platform_request_rejected` carrying the platform's own `400`, and not as `invalid_parameters`, which on this route only ever signals a missing, misshapen or unknown parameter.

### `GET /v1/ereporting/export` - Download the reports of a period as a file

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Innovorder restaurant identifier. |
| startDate | string | Yes | First business date of the range (YYYY-MM-DD), inclusive. |
| endDate | string | Yes | Last business date of the range (YYYY-MM-DD), inclusive. |
| format | string | Yes | json or csv. |

#### Response

```json
Binary file. Content-Disposition carries the filename, X-Checksum-Sha256 the SHA-256 of the body.
```

Compute the SHA-256 of the bytes you received and compare it with `X-Checksum-Sha256` to prove the file was not truncated in transit. Both headers are CORS-exposed.

A wide range is aggregated day by day and can legitimately take minutes: set your own client timeout to at least three minutes. The reporting platform is given 120 seconds to answer, past which the request ends on a `502 io_data_platform_unreachable` rather than on a truncated file, and the API holds the connection open for at most 150 seconds in total; a call still silent by then is over, so retry it rather than wait. Both budgets apply to the three JSON routes as well.

### Error Reference

Errors use the platform-wide body `{ status, code, message }` (plus `extraData` on validation errors).

This API never retries the reporting platform on your behalf: each call you make is a single attempt upstream. A `502` therefore means one failed attempt, not an exhausted retry budget, and re-issuing the request is your decision. Space your retries out rather than looping, since a wide export is expensive to recompute.

| Code | HTTP | Meaning |
| --- | --- | --- |
| invalid\_parameters | 400 | Missing or malformed parameter, e.g. a `date` that is not `YYYY-MM-DD`. The offending fields are listed in `extraData`. |
| token\_expired | 401 | The access token is past its 24 hours (or 14 days with `rememberMe`). Log in again. |
| ereporting\_bearer\_token\_required | 401 | The request carried no usable `Authorization: Bearer` token, or carried an `X-API-Key` header, which these endpoints do not support. |
| permission\_denied | 401 | The account's role may not read eReporting at all: only Brand, Brand Group and Restaurant accounts may. Note the 401: the same code is returned with a 403 for a scope refusal. |
| permission\_denied | 403 | The role is allowed, but the requested `restaurantId` is outside the perimeter of your account. |
| restaurant\_not\_found | 400 | No restaurant carries this `restaurantId` at all. A restaurant that exists but has been closed or deactivated answers `403 permission_denied` instead, see the limitation noted above. |
| io\_data\_platform\_request\_rejected | 4xx | The reporting platform refused the request and its own status is preserved: most often 404 for an unknown closure or 403 for a scope you do not hold. The reason is in `message`. |
| io\_data\_platform\_server\_error | 502 | The reporting platform failed. Retry later; never treat it as "no data". |
| io\_data\_platform\_unreachable | 502 | The reporting platform could not be reached at all (timeout, network). Retry later. |
| io\_data\_platform\_authentication\_failed | 502 | Your token was accepted here but refused downstream, which is an Innovorder-side misconfiguration. Contact support. |
