Integration path
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/loginGet 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
{
"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
{
"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/reportsGet 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
{
"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. ArestaurantId 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/closuresList 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
{
"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 (403for 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}/ordersPaginate 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
{
"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 startDateactually 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/exportDownload 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
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. |