# White-label Guest App

The Innovorder Guest App serves employees and other diners in collective catering. Each enterprise deployment has its own Innovorder brand and published hostname. A catering operator can serve several enterprise deployments; a diner can only access their own brand and the restaurants configured for that deployment.

This page documents the built-in Guest App API. The [third-party integration guide](https://developers.innovorder.io/docs/guides/guide-guest-app-integration.md) covers a separate service-account integration pattern. All identifiers and example hostnames below are fictional. Provider credentials and deployment activation must be provisioned before the corresponding features are available.

## Authentication and browser boundary

The Guest App browser calls its own `/api/*` backend. That backend authenticates an Innovorder customer, stores the OAuth bearer token in Redis and exposes only a Secure HttpOnly session cookie. Every mutation requires the exact parent origin and an `x-csrf-token` from `GET /api/bootstrap`. The Innovorder endpoints serving diners use `Authorization: Bearer <customer-token>`. Guest App editing requires an administrator or brand backoffice bearer token, with server-side brand ownership checks. Restaurant and brand-group roles are excluded. Application creation and domain/HTTPS operations remain administrator-only. Configuration reads remain public. Their JSON responses use the standard `{status, code, message, data}` envelope.

Signup, required fields, CAPTCHA, editable email, Stripe recharge and automatic recharge are derived from the published deployment and the live Innovorder brand configuration. The app does not enable unsupported features by hiding a button alone. Supported locales are fr, en, es, de, it, ar, zh-CN, ja, ko and nl; Arabic uses RTL. Dish translations are entered manually, with a fallback to the dish source language when a translation is missing.

Configuration uses a Modules editor for visibility and home ordering. Communications are enabled when the historical configuration omits features.communications; false returns an empty feed without deleting published content. Custom modules are administrator-configured HTTPS destinations, presented in the app as sandboxed iframes. They use a backend-verified SSO proof, never an Innovorder OAuth token. Preview does not load external modules.

### `POST /guest-app/custom-module-session` - Create a short-lived custom module SSO proof

Authenticated convive only. Requires a published, enabled module and an authorized restaurant in the session deployment. Returns no-store. HS256 JWT expires in 60 seconds; issuer innovorder-guest-app, audience urn:innovorder:guest:&lt;deploymentId&gt;:&lt;moduleId&gt;. Claims include sub, firstName, lastName, email, deploymentId, moduleId, brandId, restaurantId, parentOrigin, targetOrigin, iat, exp and jti. The partner backend must validate all expected claims and atomically reject replayed jti values. The Guest BFF delivers the token through a strict-origin iframe postMessage handshake, never a URL parameter. The external app must implement guest.custom.ready/session/authenticated/error/close messages with version 1 and verify the parent window and origin.

#### Request Body

```json
{
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "moduleId": "89bb7181-96b2-45b8-b89a-464414bb3518",
  "restaurantId": 200
}
```

##### 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 |
| --- | --- | --- | --- |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| moduleId | string | "89bb7181-96b2-45b8-b89a-464414bb3518" | Identifier of the associated module. |
| restaurantId | integer | 200 | Identifier of the restaurant. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "url": "https://partner.example.org/app",
    "token": "<short-lived-signed-proof>",
    "expiresAt": "2026-09-24T12:01:00Z"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://partner.example.org/app" | The url value. |
| data.token | string | "&lt;short-lived-signed-proof&gt;" | The token value. |
| data.expiresAt | string | "2026-09-24T12:01:00Z" | Date or timestamp for expires. |

### `GET /guest-app/deployments/{deploymentId}/history/{entryId}/receipts/{receiptId}` - Download a sales receipt or eligible e-wallet reload PDF

Authenticated convive only. Use a receipt key returned by the history detail receipts array. Ownership, deployment restaurants, association with the history entry and reload eligibility are checked again at download. Sales use the exact report-order snapshot; reloads reuse the shared paid, non-cancelled eligibility. Binary application/pdf with Content-Disposition attachment and Cache-Control no-store. Shared ticket-export quota: 60 requests/minute per actor (429 with Retry-After). No OAuth token is passed in a URL.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | UUID | Yes | Authorized Guest application. |
| entryId | string | Yes | History key, sale:&lt;id&gt; or wallet:&lt;id&gt;. |
| receiptId | string | Yes | Receipt key from the selected history detail. |

#### Response

```json
Binary PDF. Missing, unrelated or ineligible documents are unavailable; no fallback to another customer or restaurant.
```

### `GET /guest-app/configuration` - Resolve a published guest deployment

Public read by exact normalized hostname. Draft configuration and private provider profiles are never returned. An unknown or unpublished hostname is unavailable.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| hostname | string | Yes | Published hostname, without scheme or path. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "deploymentId": "11111111-1111-4111-8111-111111111111",
    "brandId": 100,
    "hostname": "guests.example.test",
    "name": "Example enterprise",
    "version": 1,
    "topUpLimits": {
      "minimumAmount": 100,
      "maximumAmount": 40000
    },
    "configuration": {
      "displayName": "Example restaurant",
      "defaultLocale": "fr",
      "locales": [
        "fr",
        "en"
      ],
      "country": "FR",
      "currency": "EUR",
      "timeZone": "Europe/Paris",
      "restaurants": [
        {
          "restaurantId": 200,
          "name": "Main restaurant"
        }
      ]
    }
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| data.brandId | integer | 100 | Identifier of the brand. |
| data.hostname | string | "guests.example.test" | The hostname value. |
| data.name | string | "Example enterprise" | The name value. |
| data.version | integer | 1 | The version value. |
| data.topUpLimits | object | {…} | Object containing top up limits fields. |
| data.topUpLimits.minimumAmount | integer | 100 | The minimum amount value. |
| data.topUpLimits.maximumAmount | integer | 40000 | The maximum amount value. |
| data.configuration | object | {…} | Object containing configuration fields. |
| data.configuration.displayName | string | "Example restaurant" | The display name value. |
| data.configuration.defaultLocale | string | "fr" | The default locale value. |
| data.configuration.locales | array | \[…\] | List of locales entries. |
| data.configuration.locales\[\] | string | "fr" | The locales value. |
| data.configuration.country | string | "FR" | The country value. |
| data.configuration.currency | string | "EUR" | ISO 4217 currency code. |
| data.configuration.timeZone | string | "Europe/Paris" | The time zone value. |
| data.configuration.restaurants | array | \[…\] | List of restaurants entries. |
| data.configuration.restaurants\[\] | object | {…} | Object containing restaurants fields. |
| data.configuration.restaurants\[\].restaurantId | integer | 200 | Identifier of the restaurant. |
| data.configuration.restaurants\[\].name | string | "Main restaurant" | The name value. |

The configuration example is shortened. Its full contract includes brandHash; logo, icon and hero URLs; primary, accent and background colors; constrained typography, corner radius and card style; module ordering; badge format; feature flags; restaurant ordering/Atlas URLs; and legal/support links. Top-level topUpLimits are read-only values supplied by the API, not theme configuration or browser-selected limits. Administrators and owning brand users can publish and restore deployment versions. Brand writes and restorations preserve technical settings: brand binding, domain, country, currency, time zones, badge format, integration identifiers and custom URLs. Brand users can edit identity, languages, assistance, restaurants and administrator-enabled services; automatic integration links remain server-generated. Publication validates that configured restaurants and currency belong to the dedicated brand.

## Automatic deployment setup

### `GET /guest-app/admin/deployments/{deploymentId}/setup` - Prepare the administration editor

Administrators and owning brand users. Returns permissions (technical, createApplication, editableFeatures), brand defaults, the available restaurants, resolved v1/v2 entry links and configuration availability for Ordering and Atlas. No signing key, partner secret or customer token is returned.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | string (UUID) | Yes | Deployment being configured. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "brand": {
      "brandId": 100,
      "name": "Example company",
      "brandHash": "example",
      "currency": "EUR",
      "locale": "fr"
    },
    "ordering": {
      "configured": true
    },
    "atlas": {
      "automatic": true,
      "configured": false,
      "status": "pending",
      "embedUrl": "https://atlas.example/embed/guest-footfall"
    },
    "restaurants": [
      {
        "restaurantId": 200,
        "name": "Main restaurant",
        "orderingUrls": {
          "v1": "https://ordering-v1.example/home?brandId=100",
          "v2": "https://ordering-v2.example/example"
        }
      }
    ]
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.brand | object | {…} | Object containing brand fields. |
| data.brand.brandId | integer | 100 | Identifier of the brand. |
| data.brand.name | string | "Example company" | The name value. |
| data.brand.brandHash | string | "example" | The brand hash value. |
| data.brand.currency | string | "EUR" | ISO 4217 currency code. |
| data.brand.locale | string | "fr" | The locale value. |
| data.ordering | object | {…} | Object containing ordering fields. |
| data.ordering.configured | boolean | true | The configured value. |
| data.atlas | object | {…} | Object containing atlas fields. |
| data.atlas.automatic | boolean | true | The automatic value. |
| data.atlas.configured | boolean | false | The configured value. |
| data.atlas.status | string | "pending" | HTTP status code returned by the API. |
| data.atlas.embedUrl | string | "https://atlas.example/embed/guest-footfall" | The embed url value. |
| data.restaurants | array | \[…\] | List of restaurants entries. |
| data.restaurants\[\] | object | {…} | Object containing restaurants fields. |
| data.restaurants\[\].restaurantId | integer | 200 | Identifier of the restaurant. |
| data.restaurants\[\].name | string | "Main restaurant" | The name value. |
| data.restaurants\[\].orderingUrls | object | {…} | Object containing ordering urls fields. |
| data.restaurants\[\].orderingUrls.v1 | string | "https://ordering-v1.example/home?brandId=100" | The v1 value. |
| data.restaurants\[\].orderingUrls.v2 | string | "https://ordering-v2.example/example" | The v2 value. |

### Automatic customer domains

Creating an application also initializes durable domain onboarding. An Innovorder administrator copies an A record and the exact Certificate Manager CNAME to the customer's DNS provider. Verification continues without an open browser. A domain becomes ready only after DNS, certificate, certificate-map association and a TLS health check succeed. Content must also be explicitly published; DNS verification never publishes a draft.

### `GET /guest-app/admin/deployments/{deploymentId}/domain` - Read domain onboarding and DNS instructions

Admin only. Returns preparation state, DNS and certificate status, exact DNS values, last/next checks, safe errors, publication status and whether a pending hostname can be corrected. No private keys or Google resource references are returned.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | UUID | Yes | Deployment identifier. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "hostname": "lunch.customer.fr",
    "revision": 1,
    "state": "dns_pending",
    "ready": false,
    "legacy": false,
    "editable": true,
    "published": false,
    "dnsState": "missing",
    "certificateState": "PROVISIONING",
    "checkedAt": null,
    "nextCheckAt": "2026-09-18T12:00:00Z",
    "error": "dns_missing",
    "records": [
      {
        "type": "A",
        "name": "lunch.customer.fr",
        "relativeName": "lunch",
        "value": "34.1.2.3",
        "purpose": "routing"
      },
      {
        "type": "CNAME",
        "name": "_acme-challenge_example.lunch.customer.fr.",
        "relativeName": "_acme-challenge_example.lunch",
        "value": "example.authorize.certificatemanager.goog.",
        "purpose": "certificate"
      }
    ]
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.hostname | string | "lunch.customer.fr" | The hostname value. |
| data.revision | integer | 1 | The revision value. |
| data.state | string | "dns\_pending" | The state value. |
| data.ready | boolean | false | The ready value. |
| data.legacy | boolean | false | The legacy value. |
| data.editable | boolean | true | The editable value. |
| data.published | boolean | false | The published value. |
| data.dnsState | string | "missing" | The dns state value. |
| data.certificateState | string | "PROVISIONING" | The certificate state value. |
| data.checkedAt | null | null | Date or timestamp for checked. |
| data.nextCheckAt | string | "2026-09-18T12:00:00Z" | Date or timestamp for next check. |
| data.error | string | "dns\_missing" | The error value. |
| data.records | array | \[…\] | List of records entries. |
| data.records\[\] | object | {…} | Object containing records fields. |
| data.records\[\].type | string | "A" | The type value. |
| data.records\[\].name | string | "lunch.customer.fr" | The name value. |
| data.records\[\].relativeName | string | "lunch" | The relative name value. |
| data.records\[\].value | string | "34.1.2.3" | The value value. |
| data.records\[\].purpose | string | "routing" | The purpose value. |

### `POST /guest-app/admin/deployments/{deploymentId}/domain/check` - Prioritize a domain check

Admin only. Idempotently makes the existing background request due. It does not launch another concurrent worker, wait for DNS propagation or publish content.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "scheduled": true
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.scheduled | boolean | true | The scheduled value. |

### `PUT /guest-app/admin/deployments/{deploymentId}/domain` - Change a pending or active domain

Admin only. Requires the domain revision from GET /domain. Rejects duplicates, stale revisions and checks currently holding a lease. For an active or legacy domain, keeps the serving hostname until its replacement passes DNS, certificate, map and HTTPS checks. Pending corrections retire only the superseded onboarding. Cutover atomically updates the deployment hostname and revision and retires the previous domain; its certificate cleanup is deferred by one day. Saved and published content remain unchanged. GET /domain returns the target hostname plus deploymentId, deploymentHostname and deploymentRevision for the currently serving deployment. Use the returned deployment revision for saves; guests must sign in on the new domain. The old address is no longer served after cutover.

#### Request Body

```json
{
  "hostname": "correct.customer.fr",
  "revision": 1
}
```

##### 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 |
| --- | --- | --- | --- |
| hostname | string | "correct.customer.fr" | The hostname value. |
| revision | integer | 1 | The revision value. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "deploymentId": "11111111-1111-4111-8111-111111111111",
    "hostname": "correct.customer.fr",
    "revision": 2
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| data.hostname | string | "correct.customer.fr" | The hostname value. |
| data.revision | integer | 2 | The revision value. |

States: preparing, dns\_pending, certificate\_pending, activating, ready; retired applies to replaced domains. Errors distinguish dns\_missing, dns\_wrong\_target, dns\_ipv6\_conflict, dns\_unavailable, certificate\_blocked, google\_unavailable, https\_pending and platform\_unavailable. Existing domains retain their current certificate during migration. The public configuration adds domainReady; the Guest BFF does not open new domains while it is false. An application is reachable only when technically activated and published. Domains are checked every minute initially, every fifteen minutes after 24 hours, and hourly after activation. Keep both DNS records for renewal.

### `POST /guest-app/admin/deployments/:deploymentId/atlas-sync` - Synchronize the managed Atlas embed partner

Admin only. Idempotent synchronization from the current published configuration: origin, brand, restaurants and colors. Draft changes are ignored. The partner secret stays in Atlas. Manual revocations remain effective. A temporary failure returns guest\_atlas\_unavailable; publication remains saved.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | string | Yes | Guest deployment UUID. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "automatic": true,
    "configured": true,
    "status": "ready"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.automatic | boolean | true | The automatic value. |
| data.configured | boolean | true | The configured value. |
| data.status | string | "ready" | HTTP status code returned by the API. |

Each configured restaurant may opt in with `orderingVersion: 'v1' | 'v2'` and `atlasAutomatic: true`. The API replaces the corresponding URL with its own environment-specific value when saving or publishing. Existing configurations without these properties retain their custom URLs unchanged. Ordering enters the native consumption-mode flow; no sit-in mode is forced. The configured restaurant is bound by the signed Guest SSO context. Atlas tokens remain short-lived and are generated only for an authenticated guest, never persisted in configuration.

The server origin overrides are GUEST\_APP\_ORDERING\_V1\_ORIGIN, GUEST\_APP\_ORDERING\_V2\_ORIGIN and GUEST\_APP\_ATLAS\_ORIGIN. They accept HTTPS origins only. Atlas is provisioned automatically from the current publication. The setup response reports live partner status; secrets remain encrypted in Atlas. No per-company environment profile is needed. Unpublished drafts never alter the live partner scope.

Templates now include `essentiel` version 1, alongside Maison, Pulse, Market and Signature. Missing template references still resolve to Maison v1. Publishing and restoring keep the same optimistic revision and tenant checks; no SQL migration is needed.

The Guest-only v1 adapter uses the ordinary OAuth JWT exchange, waits for authenticated identity and verifies server-provided Guest metadata before rendering. It does not invoke the historical balance/email update flow. It strips authentication parameters, preserves the strict parent-message origin/source checks and uses single-use server handoffs for external payment restoration. The legacy engine retains its native language catalog with an English fallback when the requested translation is unavailable; Guest controls and the Essentiel renderer support the ten Guest languages.

## Published menus and private ratings

Weekly menu occurrences are distinct from orderable catalogue products. A publication snapshots the dish content. Dates use the restaurant time zone. A rating opens at the service start and closes at the end of local day J+7. There is one editable integer score per customer and occurrence, no comment and no purchase-proof requirement. A diner receives only their own ratings; Administrators and owning brand users access aggregate counts and score distribution. Explicit closures are returned as closedServices entries with date and service (lunch or dinner), separately from missing menu data. A service cannot be both closed and populated with dishes; a closure-only week can be published.

### `GET /guest-app/restaurants/{restaurantId}/menus` - Read published menu weeks

Customer-only. Restaurant must belong to the customer brand and the published deployment. The inclusive date range may span at most 63 days.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Configured restaurant. |
| from | string | Yes | First local date, YYYY-MM-DD. |
| to | string | Yes | Last local date, YYYY-MM-DD. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": [
    {
      "menuWeekId": "11111111-1111-4111-8111-111111111111",
      "restaurantId": 200,
      "weekStart": "2026-09-07",
      "title": "Weekly menu",
      "revision": 1,
      "status": "published",
      "publishedVersion": 1,
      "closedServices": [
        {
          "date": "2026-09-07",
          "service": "dinner"
        }
      ],
      "occurrences": [
        {
          "occurrenceId": "22222222-2222-4222-8222-222222222222",
          "dishId": "33333333-3333-4333-8333-333333333333",
          "date": "2026-09-07",
          "service": "lunch",
          "serviceStartTime": "11:30",
          "position": 0,
          "ratingOpensAt": "2026-09-07T09:30:00.000Z",
          "ratingClosesAt": "2026-09-14T21:59:59.999Z",
          "dish": {
            "name": "Vegetable bowl",
            "description": "",
            "category": "main",
            "sourceLocale": "en",
            "translations": {
              "fr": {
                "name": "Assiette de légumes",
                "description": ""
              }
            }
          }
        }
      ]
    }
  ]
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | array | \[…\] | Endpoint-specific response payload. |
| data\[\] | object | {…} | Endpoint-specific response payload. |
| data\[\].menuWeekId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated menu week. |
| data\[\].restaurantId | integer | 200 | Identifier of the restaurant. |
| data\[\].weekStart | string | "2026-09-07" | The week start value. |
| data\[\].title | string | "Weekly menu" | The title value. |
| data\[\].revision | integer | 1 | The revision value. |
| data\[\].status | string | "published" | HTTP status code returned by the API. |
| data\[\].publishedVersion | integer | 1 | The published version value. |
| data\[\].closedServices | array | \[…\] | List of closed services entries. |
| data\[\].closedServices\[\] | object | {…} | Object containing closed services fields. |
| data\[\].closedServices\[\].date | string | "2026-09-07" | The date value. |
| data\[\].closedServices\[\].service | string | "dinner" | The service value. |
| data\[\].occurrences | array | \[…\] | List of occurrences entries. |
| data\[\].occurrences\[\] | object | {…} | Object containing occurrences fields. |
| data\[\].occurrences\[\].occurrenceId | string | "22222222-2222-4222-8222-222222222222" | Identifier of the associated occurrence. |
| data\[\].occurrences\[\].dishId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated dish. |
| data\[\].occurrences\[\].date | string | "2026-09-07" | The date value. |
| data\[\].occurrences\[\].service | string | "lunch" | The service value. |
| data\[\].occurrences\[\].serviceStartTime | string | "11:30" | The service start time value. |
| data\[\].occurrences\[\].position | integer | 0 | The position value. |
| data\[\].occurrences\[\].ratingOpensAt | string | "2026-09-07T09:30:00.000Z" | Date or timestamp for rating opens. |
| data\[\].occurrences\[\].ratingClosesAt | string | "2026-09-14T21:59:59.999Z" | Date or timestamp for rating closes. |
| data\[\].occurrences\[\].dish | object | {…} | Object containing dish fields. |
| data\[\].occurrences\[\].dish.name | string | "Vegetable bowl" | The name value. |
| data\[\].occurrences\[\].dish.description | string | "" | The description value. |
| data\[\].occurrences\[\].dish.category | string | "main" | The category value. |
| data\[\].occurrences\[\].dish.sourceLocale | string | "en" | The source locale value. |
| data\[\].occurrences\[\].dish.translations | object | {…} | Object containing translations fields. |
| data\[\].occurrences\[\].dish.translations.fr | object | {…} | Object containing fr fields. |
| data\[\].occurrences\[\].dish.translations.fr.name | string | "Assiette de légumes" | The name value. |
| data\[\].occurrences\[\].dish.translations.fr.description | string | "" | The description value. |

### `GET /guest-app/restaurants/{restaurantId}/ratings` - Read my dish ratings

Returns the authenticated customer’s own ratings for the allowed restaurant, including ratings whose edit window has closed.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": [
    {
      "occurrenceId": "22222222-2222-4222-8222-222222222222",
      "score": 4,
      "updatedAt": "2026-09-07T12:00:00.000Z"
    }
  ]
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | array | \[…\] | Endpoint-specific response payload. |
| data\[\] | object | {…} | Endpoint-specific response payload. |
| data\[\].occurrenceId | string | "22222222-2222-4222-8222-222222222222" | Identifier of the associated occurrence. |
| data\[\].score | integer | 4 | The score value. |
| data\[\].updatedAt | string | "2026-09-07T12:00:00.000Z" | Timestamp when this resource was last updated. |

### `PUT /guest-app/occurrences/{occurrenceId}/rating` - Create or update my private score

Customer-only. Tenant, published occurrence and rating window are checked on the server. Repeating the same occurrence updates the existing score.

#### Request Body

```json
{
  "score": 4
}
```

##### 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 |
| --- | --- | --- | --- |
| score | integer | 4 | The score value. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "occurrenceId": "22222222-2222-4222-8222-222222222222",
    "score": 4,
    "updatedAt": "2026-09-07T12:00:00.000Z"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.occurrenceId | string | "22222222-2222-4222-8222-222222222222" | Identifier of the associated occurrence. |
| data.score | integer | 4 | The score value. |
| data.updatedAt | string | "2026-09-07T12:00:00.000Z" | Timestamp when this resource was last updated. |

## Restaurant menu editor

Menu editing and aggregate reporting allow administrators and owning brand users with backoffice bearer tokens. Brand-group and restaurant roles cannot access these routes, including through direct API calls. Restaurant and brand scope checks remain in place. Draft writes use revisions; a conflicting edit returns 409. Republish corrections retain occurrence IDs and existing ratings, while duplicating a week creates new dated occurrences.

| Operation | Endpoint | Input |
| --- | --- | --- |
| GET | `/guest-app/admin/dishes` | brandId; optional restaurantId |
| POST | `/guest-app/admin/dishes` | brandId; optional restaurantId; dish name, description, category, imageUrl, allergens, labels, sourceLocale, translations |
| PUT | `/guest-app/admin/dishes/{dishId}` | Dish fields; optional restaurantId query |
| DELETE | `/guest-app/admin/dishes/{dishId}` | Archive; optional restaurantId query |
| GET | `/guest-app/admin/restaurants/{restaurantId}/menu-weeks` | from, to |
| PUT | `/guest-app/admin/restaurants/{restaurantId}/menu-weeks/{weekStart}` | revision on update, title, occurrences (at most 200), optional closedServices |
| POST | `/guest-app/admin/restaurants/{restaurantId}/menu-weeks/{weekStart}/publish` | revision |
| POST | `/guest-app/admin/restaurants/{restaurantId}/menu-weeks/{weekStart}/duplicate` | targetWeekStart |
| GET | `/guest-app/admin/restaurants/{restaurantId}/rating-aggregates` | from, to; returns occurrenceId, dishId, count, average, distribution |

Each occurrence contains dishId, date, service (lunch or dinner), serviceStartTime and position; retain occurrenceId when correcting an existing occurrence. Week starts are Mondays in YYYY-MM-DD format. Omitting closedServices on an edit preserves existing closures; send \[\] to clear them. Duplicating a week shifts closure dates as well as occurrences.

## Scheduled communications

### `GET /guest-app/admin/deployments/{deploymentId}/communications/{communicationId}` - Open a communication directly

Administrator or owning brand user. Returns the same draft/published/revision/status shape as a list item, independent of pagination. Both identifiers must be UUIDs. Ownership is checked before reading, and the message must belong to this deployment; a missing or differently-scoped message returns 404. The backoffice uses this endpoint for reloadable edit/duplicate links.

Communications belong to an application and are published independently from its configuration. Administrators and owning brand users can edit them. Saving changes only the draft; the published snapshot remains visible until explicit republication. Every update, publication and withdrawal requires the current revision; stale revisions return 409. Duplication creates a new draft through POST.

### `GET /guest-app/brands/{brandId}/status` - Read Guest App installation status

Administrators and owning brand users. Installed means at least one deployment exists, including unpublished applications and domains awaiting DNS. No channel activation or deactivation mutation is used.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "installed": true
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.installed | boolean | true | The installed value. |

### `GET /guest-app/admin/deployments/{deploymentId}/communications` - List communications

Administrators and owning brand users. Optional search (title, up to 160 characters) and opaque cursor. Returns up to 50 items plus nextCursor. Status is draft, scheduled, active, ended or withdrawn; a published item may also have pending draft changes.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "items": [],
    "nextCursor": null
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.items | array | \[\] | List of items entries. |
| data.nextCursor | null | null | Cursor to use when retrieving the next page. |

### `POST /guest-app/admin/deployments/{deploymentId}/communications` - Create a communication draft

The application must have a saved configuration. All targeted restaurants and translation locales must belong to it. Use PUT on /communications/{communicationId} with {content, revision} to edit.

#### Request Body

```json
{
  "content": {
    "title": "Cooking workshop",
    "body": "<p>Meet our <strong>chef</strong>.</p>",
    "sourceLocale": "en",
    "translations": {
      "fr": {
        "title": "Atelier cuisine",
        "body": "<p>Rencontrez notre chef.</p>"
      }
    },
    "audience": "restaurants",
    "restaurantIds": [
      200
    ],
    "startsLocal": "2026-10-01T12:00",
    "endsLocal": "2026-10-01T14:00",
    "timeZone": "Europe/Paris"
  }
}
```

##### 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 |
| --- | --- | --- | --- |
| content | object | {…} | Object containing content fields. |
| content.title | string | "Cooking workshop" | The title value. |
| content.body | string | "&lt;p&gt;Meet our &lt;strong&gt;chef&lt;/strong&gt;.&lt;/p&gt;" | The body value. |
| content.sourceLocale | string | "en" | The source locale value. |
| content.translations | object | {…} | Object containing translations fields. |
| content.translations.fr | object | {…} | Object containing fr fields. |
| content.translations.fr.title | string | "Atelier cuisine" | The title value. |
| content.translations.fr.body | string | "&lt;p&gt;Rencontrez notre chef.&lt;/p&gt;" | The body value. |
| content.audience | string | "restaurants" | The audience value. |
| content.restaurantIds | array | \[…\] | List of restaurant ids entries. |
| content.restaurantIds\[\] | integer | 200 | The restaurant ids value. |
| content.startsLocal | string | "2026-10-01T12:00" | The starts local value. |
| content.endsLocal | string | "2026-10-01T14:00" | The ends local value. |
| content.timeZone | string | "Europe/Paris" | The time zone value. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "communicationId": "33333333-3333-4333-8333-333333333333",
    "deploymentId": "11111111-1111-4111-8111-111111111111",
    "revision": 1,
    "status": "draft",
    "published": null
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.communicationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated communication. |
| data.deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| data.revision | integer | 1 | The revision value. |
| data.status | string | "draft" | HTTP status code returned by the API. |
| data.published | null | null | The published value. |

### `PUT /guest-app/admin/deployments/{deploymentId}/communications/{communicationId}` - Save a communication draft

Same content as creation, plus revision. Draft and published snapshots are separate; editing never changes the currently visible message.

### `POST /guest-app/admin/deployments/{deploymentId}/communications/{communicationId}/publication` - Publish or withdraw a communication

Set withdrawn=true to remove a message immediately; withdrawal remains possible after a targeted restaurant is removed. At most 100 non-expired published communications may coexist per application. Publication does not require a scheduling job.

#### Request Body

```json
{
  "revision": 1,
  "withdrawn": false
}
```

##### 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 |
| --- | --- | --- | --- |
| revision | integer | 1 | The revision value. |
| withdrawn | boolean | false | The withdrawn value. |

Title: 160 characters; body: 20,000 UTF-8 bytes after sanitization; optional buttonLabel: 80 characters, required with buttonUrl. Optional imageUrl and buttonUrl accept HTTPS only. Rich text permits paragraphs, bold, italic, lists and links; scripts, styles, embeds and unsafe protocols are removed server-side. Translations use the same text fields. Missing translations fall back to the complete source text with contentLocale identifying its language.

Enter startsLocal and endsLocal as YYYY-MM-DDTHH:mm in the application's IANA timeZone. The server stores UTC startsAt/endsAt and retains the original zone when editing. Nonexistent or ambiguous local times are rejected. Start is inclusive, end exclusive and strictly later than start. Application-wide audience ignores restaurantIds; restaurant targeting requires a nonempty permitted selection.

### `GET /guest-app/deployments/{deploymentId}/communications` - Read active diner communications

Customer-only. Requires restaurantId and locale. The deployment must be published and belong to the customer brand; restaurant membership is enforced. Returns only published, non-withdrawn messages active at server time for the selected restaurant, most recently started first. Removed restaurants are excluded. The Guest BFF derives deploymentId from its authenticated session and exposes GET /api/communications; responses are never PWA-cached.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "serverNow": "2026-10-01T10:00:00.000Z",
    "nextChangeAt": "2026-10-01T12:00:00.000Z",
    "restaurantScoped": true,
    "items": [
      {
        "communicationId": "33333333-3333-4333-8333-333333333333",
        "title": "Cooking workshop",
        "body": "<p>Meet our chef.</p>",
        "contentLocale": "en",
        "startsAt": "2026-10-01T10:00:00.000Z",
        "endsAt": "2026-10-01T12:00:00.000Z"
      }
    ]
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.serverNow | string | "2026-10-01T10:00:00.000Z" | The server now value. |
| data.nextChangeAt | string | "2026-10-01T12:00:00.000Z" | Date or timestamp for next change. |
| data.restaurantScoped | boolean | true | The restaurant scoped value. |
| data.items | array | \[…\] | List of items entries. |
| data.items\[\] | object | {…} | Object containing items fields. |
| data.items\[\].communicationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated communication. |
| data.items\[\].title | string | "Cooking workshop" | The title value. |
| data.items\[\].body | string | "&lt;p&gt;Meet our chef.&lt;/p&gt;" | The body value. |
| data.items\[\].contentLocale | string | "en" | The content locale value. |
| data.items\[\].startsAt | string | "2026-10-01T10:00:00.000Z" | Date or timestamp for starts. |
| data.items\[\].endsAt | string | "2026-10-01T12:00:00.000Z" | Date or timestamp for ends. |

All built-in modules and custom modules can be reordered. The optional features.badge and features.balance flags default to enabled for historical configurations. moduleTitles provides optional plain-text display names for built-ins. moduleOrder accepts badge, balance, communications, menus, ratings, ordering, topUp, automaticTopUp, wallet, attendance, registration and custom:&lt;moduleId&gt;. Registration remains on the sign-in screen; ratings require menus. Existing technical capabilities and editor permissions are unchanged.

The home screen shows three active cards, then Show more, in the configured moduleOrder. Badge and balance have no fixed position. restaurantScoped keeps restaurant selection available for active targeted messages. nextChangeAt covers upcoming starts and ends across the application; refresh at this instant using serverNow, every minute while visible, after restaurant changes and on return to the app. No pushes, emails or automatic popups are sent. Preview uses fictitious messages only.

## Durable Stripe recharge

Create one UUID per deliberate recharge and retain it through retries. The API stores immutable customer, card, amount, currency and Stripe-account terms before provider calls. It commits the PaymentIntent ID before confirming any charge. Successful receipt verification credits the Innovorder ledger once under a database lock. Polling reconciles the same operation; it does not create a new payment. Automatic reload continues to use the existing customer automatic-reload settings where enabled; CASH and POSTPAYMENT accounts are not eligible. An upstream null automatic-reload configuration means inactive; a configuration object means active, and the Guest BFF normalizes this explicitly.

## Combined sales and wallet receipts

### `GET /guest-app/deployments/:deploymentId/history` - Read the diner’s combined receipt history

Customer only. Groups sales with explicitly linked ewallet movements, retaining sales without wallet payments and standalone credits/debits. Ambiguous references are not merged. Sorted by latest activity, then type and identifier. Sale totals and wallet amounts remain separate; all money is in minor units.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | string | Yes | Published Guest deployment UUID. |
| cursor | string | No | Opaque nextCursor from the previous page. |
| limit | number | No | 1–50 groups; default 20. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "entries": [
      {
        "key": "sale:123",
        "kind": "sale",
        "occurredAt": "2026-09-16T12:00:00Z",
        "saleAt": "2026-09-15T12:00:00Z",
        "restaurantName": "Main restaurant",
        "timeZone": "Europe/Paris",
        "currency": "EUR",
        "amount": 1000,
        "status": "PAID",
        "number": "T123",
        "walletCount": 1,
        "walletType": null
      }
    ],
    "nextCursor": null
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.entries | array | \[…\] | List of entries entries. |
| data.entries\[\] | object | {…} | Object containing entries fields. |
| data.entries\[\].key | string | "sale:123" | The key value. |
| data.entries\[\].kind | string | "sale" | The kind value. |
| data.entries\[\].occurredAt | string | "2026-09-16T12:00:00Z" | Date or timestamp for occurred. |
| data.entries\[\].saleAt | string | "2026-09-15T12:00:00Z" | Date or timestamp for sale. |
| data.entries\[\].restaurantName | string | "Main restaurant" | The restaurant name value. |
| data.entries\[\].timeZone | string | "Europe/Paris" | The time zone value. |
| data.entries\[\].currency | string | "EUR" | ISO 4217 currency code. |
| data.entries\[\].amount | integer | 1000 | The amount value. |
| data.entries\[\].status | string | "PAID" | HTTP status code returned by the API. |
| data.entries\[\].number | string | "T123" | The number value. |
| data.entries\[\].walletCount | integer | 1 | The wallet count value. |
| data.entries\[\].walletType | null | null | The wallet type value. |
| data.nextCursor | null | null | Cursor to use when retrieving the next page. |

### `GET /guest-app/deployments/:deploymentId/history/:entryId` - Read receipt details and associated wallet movements

Revalidates customer, brand and published restaurant scope. Includes item lines, recorded totals, adjustments and every associated wallet movement with its own signed amount, currency, date and status. Missing/foreign entries return 404. No payment secrets or other diners’ data.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| deploymentId | string | Yes | Published deployment UUID. |
| entryId | string | Yes | Key returned by the list: sale:&lt;id&gt; or wallet:&lt;id&gt;. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "key": "wallet:456",
    "kind": "wallet",
    "amount": 2000,
    "currency": "EUR",
    "lines": [],
    "adjustments": [],
    "totals": null,
    "transactions": [
      {
        "id": "456",
        "amount": 2000,
        "currency": "EUR",
        "type": "1",
        "status": "1",
        "occurredAt": "2026-09-16T12:00:00Z",
        "paymentOrigin": "4"
      }
    ]
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.key | string | "wallet:456" | The key value. |
| data.kind | string | "wallet" | The kind value. |
| data.amount | integer | 2000 | The amount value. |
| data.currency | string | "EUR" | ISO 4217 currency code. |
| data.lines | array | \[\] | List of lines entries. |
| data.adjustments | array | \[\] | List of adjustments entries. |
| data.totals | null | null | The totals value. |
| data.transactions | array | \[…\] | List of transactions entries. |
| data.transactions\[\] | object | {…} | Object containing transactions fields. |
| data.transactions\[\].id | string | "456" | The id value. |
| data.transactions\[\].amount | integer | 2000 | The amount value. |
| data.transactions\[\].currency | string | "EUR" | ISO 4217 currency code. |
| data.transactions\[\].type | string | "1" | The type value. |
| data.transactions\[\].status | string | "1" | HTTP status code returned by the API. |
| data.transactions\[\].occurredAt | string | "2026-09-16T12:00:00Z" | Date or timestamp for occurred. |
| data.transactions\[\].paymentOrigin | string | "4" | The payment origin value. |

The browser uses `/api/history` and `/api/history/:entryId`; the BFF derives tenant and customer from the session. Responses are `no-store`. The preview uses fictitious data without financial API calls. Failed reads stay distinct from empty history.

### `POST /guest-app/cards` - Create or finalize a leased card setup

Customer-only. Generate leaseId once on the client and reuse it for retries. deploymentId, name and retain belong to that lease. Without cardToken the response provides the Stripe SetupIntent client secret. After Stripe setup succeeds, repeat the same request with its pm\_ payment-method ID as cardToken; the response is the saved Card.

#### Request Body

```json
{
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "name": "Personal card",
  "leaseId": "44444444-4444-4444-8444-444444444444",
  "retain": false
}
```

##### 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 |
| --- | --- | --- | --- |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| name | string | "Personal card" | The name value. |
| leaseId | string | "44444444-4444-4444-8444-444444444444" | Identifier of the associated lease. |
| retain | boolean | false | The retain value. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "leaseId": "44444444-4444-4444-8444-444444444444",
    "client_secret": "seti_example_secret_redacted"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.leaseId | string | "44444444-4444-4444-8444-444444444444" | Identifier of the associated lease. |
| data.client\_secret | string | "seti\_example\_secret\_redacted" | The client secret value. |

A successful finalization returns the Card (including cardId) used for recharge. With retain=false, an abandoned temporary card lease is eligible for cleanup after one hour, except while a nonterminal recharge references that card. The same lease protects setup and attachment retries; do not issue a replacement lease after an ambiguous response. retain=true explicitly saves the payment method. The Guest BFF exposes this as POST /api/cards and derives deploymentId from the session hostname. The interface prevents an unmounted card form from starting a recharge.

### `POST /guest-app/topups` - Start or recover a Stripe recharge

Customer-only. A modern saved Stripe PaymentMethod belonging to this customer is required. Amount is an integer in the configured currency’s minor unit. Reusing operationId with a different card, amount, deployment or transient choice returns 409.

#### Request Body

```json
{
  "operationId": "33333333-3333-4333-8333-333333333333",
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "cardId": 300,
  "amount": 2000,
  "transient": false
}
```

##### 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 |
| --- | --- | --- | --- |
| operationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated operation. |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| cardId | integer | 300 | Identifier of the associated card. |
| amount | integer | 2000 | The amount value. |
| transient | boolean | false | The transient value. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "operationId": "33333333-3333-4333-8333-333333333333",
    "status": "requires_action",
    "paymentIntentId": "pi_example",
    "clientSecret": "pi_example_secret_redacted"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.operationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated operation. |
| data.status | string | "requires\_action" | HTTP status code returned by the API. |
| data.paymentIntentId | string | "pi\_example" | Identifier of the associated payment intent. |
| data.clientSecret | string | "pi\_example\_secret\_redacted" | The client secret value. |

### `GET /guest-app/topups/{operationId}` - Reconcile the recharge status

Returns pending, requires\_action, succeeded or failed. The operation must belong to the caller. Only requires\_action exposes clientSecret. Never infer success from a browser redirect or a 202 response.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "operationId": "33333333-3333-4333-8333-333333333333",
    "status": "succeeded",
    "paymentIntentId": "pi_example",
    "transactionId": 400
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.operationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated operation. |
| data.status | string | "succeeded" | HTTP status code returned by the API. |
| data.paymentIntentId | string | "pi\_example" | Identifier of the associated payment intent. |
| data.transactionId | integer | 400 | Identifier of the transaction. |

### `POST /guest-app/topups/{operationId}/resume` - Resume the same intent after customer authentication

No customer-supplied amount, card or intent is trusted on resume. The API reloads the stored operation and retrieves its Stripe intent. A five-minute reconciliation worker also recovers interrupted operations.

#### Request Body

```json
{}
```

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "operationId": "33333333-3333-4333-8333-333333333333",
    "status": "succeeded",
    "transactionId": 400
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.operationId | string | "33333333-3333-4333-8333-333333333333" | Identifier of the associated operation. |
| data.status | string | "succeeded" | HTTP status code returned by the API. |
| data.transactionId | integer | 400 | Identifier of the transaction. |

The browser BFF exposes corresponding `/api/topups` routes and maps statuses to pending, action and confirmed. Temporary card leases are scoped to this customer; after a terminal payment outcome they are disabled and detached, with durable retry if cleanup fails. Abandoned temporary leases are cleaned after one hour unless an operation is still pending. Provider secrets remain exclusively in the API runtime.

## Ordering and Atlas launch sessions

### `POST /guest-app/sso` - Launch Ordering with a one-use customer session

The target restaurant must be configured for this deployment. Returns a one-use JWT URL valid for 60 seconds. Ordering consumes the ticket atomically, resets prior guest state and removes the login token from the address bar.

#### Request Body

```json
{
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "restaurantId": 200
}
```

##### 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 |
| --- | --- | --- | --- |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| restaurantId | integer | 200 | Identifier of the restaurant. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "url": "https://ordering.example.test/?autoLoginJWT=REDACTED&embedded=true",
    "expiresAt": "2026-09-07T10:01:00.000Z"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://ordering.example.test/?autoLoginJWT=REDACTED&embedded=true" | The url value. |
| data.expiresAt | string | "2026-09-07T10:01:00.000Z" | Date or timestamp for expires. |

The Guest SSO exchange through `/oauth/login` also returns `data.guestAppContext`, containing the verified `parentOrigin` and `restaurantId`. Ordering activates its embedded bridge only when those values match the requested context. URL parameters and the referrer alone are never an authorization source. Disabled restaurants keep their historical menus and ratings accessible to authorized diners and Innovorder administrators, but cannot issue a new Ordering session.

### `POST /guest-app/ordering-handoff` - Move an authenticated order out of the iframe

Customer-only. The path must remain on the configured Ordering origin. The API stores an untrusted cart snapshot (up to 128 KiB) and returns a fresh one-use SSO URL plus an opaque guestHandoff code. This avoids relying on iframe storage being shared with a top-level browser page.

#### Request Body

```json
{
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "restaurantId": 200,
  "path": "/payment",
  "cart": {}
}
```

##### 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 |
| --- | --- | --- | --- |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| restaurantId | integer | 200 | Identifier of the restaurant. |
| path | string | "/payment" | The path value. |
| cart | object | {…} | Object containing cart fields. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "url": "https://ordering.example.test/payment?autoLoginJWT=REDACTED&guestHandoff=REDACTED",
    "expiresAt": "2026-09-07T10:01:00.000Z",
    "cartExpiresAt": "2026-09-07T10:05:00.000Z"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://ordering.example.test/payment?autoLoginJWT=REDACTED&guestHandoff=RED… | The url value. |
| data.expiresAt | string | "2026-09-07T10:01:00.000Z" | Date or timestamp for expires. |
| data.cartExpiresAt | string | "2026-09-07T10:05:00.000Z" | Date or timestamp for cart expires. |

### `GET /guest-app/ordering-handoff/{code}` - Consume the one-use cart handoff after OAuth

Authenticate as the same customer and brand first. The opaque code is valid for five minutes and is consumed once; expired, foreign or already-consumed codes return 404. The fresh SSO ticket has its separate 60-second lifetime. Ordering normalizes the returned cart and requests a new API preview; copied prices and totals are never authoritative.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "cart": {}
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.cart | object | {…} | Object containing cart fields. |

The same-origin browser endpoint is POST /api/ordering-handoff with restaurantId, path and cart. The BFF resolves deploymentId from its authenticated context. Never transfer the bearer token through postMessage.

### `POST /guest-app/atlas-session` - Launch the restricted historical-footfall embed

Requires enabled attendance. Atlas synchronizes the managed partner from the current publication and signs the session; missing partners are created automatically. The signed token has one brand, one restaurant, its IANA time zone, audience atlas-embed and features.guestFootfall=true. It cannot access ordinary financial analytics.

#### Request Body

```json
{
  "deploymentId": "11111111-1111-4111-8111-111111111111",
  "restaurantId": 200
}
```

##### 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 |
| --- | --- | --- | --- |
| deploymentId | string | "11111111-1111-4111-8111-111111111111" | Identifier of the associated deployment. |
| restaurantId | integer | 200 | Identifier of the restaurant. |

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "url": "https://atlas.example.test/embed/guest-footfall?locale=fr#token=REDACTED",
    "expiresAt": "2026-09-07T10:15:00.000Z"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://atlas.example.test/embed/guest-footfall?locale=fr#token=REDACTED" | The url value. |
| data.expiresAt | string | "2026-09-07T10:15:00.000Z" | Date or timestamp for expires. |

Atlas shows average ticket counts per 15-minute slot over the previous 28 completed local business dates. All slots use the same observed-day denominator. It is historical information, not live occupancy, unique people or waiting time. An empty history differs from service unavailability. The Guest BFF supplies these launch sessions through `/api/ordering-session` and `/api/atlas-session` with a restaurantId; it derives the deployment server-side.

```javascript
// For Atlas only; keep the short JWT in the fragment and in memory.
window.addEventListener('message', async (event) => {
  if (event.origin !== atlasOrigin || event.source !== iframe.contentWindow) return;
  if (event.data?.type !== 'atlas:token-refresh-needed') return;
  const { url } = await renewGuestAtlasSession(); // same-origin BFF + CSRF
  const token = new URL(url).hash.slice('#token='.length);
  iframe.contentWindow.postMessage({ type: 'atlas:token', token }, atlasOrigin);
});
```

## Apple and Google Wallet

### `GET /guest-app/wallet/{deploymentId}/{provider}` - Add my badge to a device wallet

Customer-only; provider is apple or google. Apple returns application/vnd.apple.pkpass bytes. Google returns an Add to Google Wallet URL. Missing issuer configuration returns 503 guest\_wallet\_unavailable. A blocked or replaced badge is reconciled by the Wallet worker.

#### Response

```json
{
  "status": 200,
  "code": "guest_app_succeed",
  "message": "Guest App request succeeded",
  "data": {
    "url": "https://pay.google.com/gp/v/save/REDACTED"
  }
}
```

##### Response Properties

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

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "guest\_app\_succeed" | Machine-readable application code for the result. |
| message | string | "Guest App request succeeded" | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://pay.google.com/gp/v/save/REDACTED" | The url value. |

These passes identify a badge; they are not NFC payment credentials and do not replace the POS eligibility check. Apple device registration, updates and signed-pass downloads are handled under `/guest-app/wallet/apple/v1`using the ApplePass protocol. API deployment profiles contain issuer certificates/private keys on the server only. No private credential is returned by configuration or customer endpoints. The API runtime requires the OpenSSL command-line binary to produce the detached CMS signature; verify this prerequisite in the promoted API image.

## Errors and activation

Invalid input is 400; expired customer sessions are 401; unauthorized tenant, restaurant or capability is 403; missing resources are 404; revision/payment-term conflicts and closed rating windows are 409; missing provider configuration or upstream failures are 503. Process the machine-readable error code rather than its translated message. Administrative role denials use 401 with the permission\_denied code. Apply all three Guest App API migrations (deployment/menu/ratings/wallet, durable recharge, then handoff/card leases), configure approved provider profiles and validate connected sandbox flows before enabling a deployment.
