# Integration Guide: Paygreen Meal Vouchers

This guide explains how to accept meal vouchers (Conecs titres-restaurant, Swile, Restoflash) on an order created through the API, using the restaurant's Paygreen module: exactly what `POST /orders` expects in the payment `metadata` field, how the customer pays on Paygreen's hosted page, and how the order is confirmed afterwards.

#### How Paygreen works at Innovorder

-   Paygreen is configured **per restaurant** in the Innovorder back office (Paygreen Shop ID and secret key). There is nothing to configure through the API.
-   The payment happens on **Paygreen's hosted payment page**: the API creates a Paygreen Payment Order and returns its URL, and you send the customer there. The voucher network (Conecs, Swile, Restoflash) and any complementary card payment are chosen on that page.
-   The order is confirmed **asynchronously**: Paygreen notifies Innovorder by webhook once the payment is captured, and the order then becomes a paid order. There is no confirm endpoint to call on your side.
-   The Paygreen environment is set **per Innovorder platform**, not per restaurant: the production platform only talks to Paygreen production. Sandbox Paygreen credentials cannot be used on a production restaurant (the back office refuses them when saved). To test against your Paygreen sandbox, ask [support@innovorder.fr](mailto:support@innovorder.fr) for a test brand on the Innovorder preproduction platform, which is wired to Paygreen's sandbox.

### 1\. Find the Paygreen Payment Method

Call [List Payment Methods](https://developers.innovorder.io/docs/restaurants/payment-methods.md) (`GET /payment_methods`) for the restaurant and channel of the order, and pick the entry whose `code` is `Paygreen`. Its `paymentMethodId` is the value to send in `payments[]`. The payment method must be enabled for the consumption mode of the order, otherwise `POST /orders` answers `payment_method_not_activated` (403).

Eligible amount: the part of the order payable with meal vouchers is computed server-side from the products' `isEligibleLuncheon` flag. You do not send it: `amount` is the total the customer pays through Paygreen, vouchers and complementary card included.

### 2\. Create the Order - the Metadata Contract

Create the order with `POST /orders` (see [Create Orders](https://developers.innovorder.io/docs/orders/orders-create.md) for the full request body). The payment entry carries a `metadata` field, which must be a **JSON-encoded string** with the following keys:

| Key | Required | Description |
| --- | --- | --- |
| paygreenPaymentType | Yes | Voucher family: `TRD` (meal vouchers, Conecs), `RESTOFLASH` or `LUNCHR` (Swile). Any other value is rejected with `paygreen_payment_metadata_invalid`. |
| firstName, lastName, email | Guest orders | Identity of the payer, forwarded to Paygreen. All three are required when the order is not placed by an authenticated Innovorder customer; they are ignored otherwise. |

### `POST /orders` - Create an order paid with meal vouchers through Paygreen

Standard order creation with a Paygreen payment entry. The metadata field carries the voucher family and, for a guest order, the payer identity, as a JSON-encoded string. The response is a 200 with code required\_action: the order exists but is not paid yet. We recommend sending an idempotency-key header to protect against double submissions.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| idempotency-key | string (header) | No | Recommended. Unique key protecting against double order submission. |

#### Request Body

```json
{
  "restaurantId": 2285,
  "channelId": 2,
  "consumptionMode": "MODE_TAKE_AWAY",
  "menuId": 24194,
  "cart": [
    {
      "productId": 1670891,
      "quantity": 1,
      "steps": []
    }
  ],
  "expectedAt": "2026-09-15T12:30:00.000Z",
  "payments": [
    {
      "paymentMethodId": 169093,
      "amount": 3150,
      "quantity": 1,
      "metadata": "{\"paygreenPaymentType\":\"TRD\",\"firstName\":\"Alice\",\"lastName\":\"Martin\",\"email\":\"alice.martin@example.com\"}"
    }
  ]
}
```

##### 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 |
| --- | --- | --- | --- |
| restaurantId | integer | 2285 | Identifier of the restaurant. |
| channelId | integer | 2 | Identifier of the associated channel. |
| consumptionMode | string | "MODE\_TAKE\_AWAY" | The consumption mode value. |
| menuId | integer | 24194 | Identifier of the menu. |
| cart | array | \[…\] | List of cart entries. |
| cart\[\] | object | {…} | Object containing cart fields. |
| cart\[\].productId | integer | 1670891 | Identifier of the product. |
| cart\[\].quantity | integer | 1 | The quantity value. |
| cart\[\].steps | array | \[\] | List of steps entries. |
| expectedAt | string | "2026-09-15T12:30:00.000Z" | Date or timestamp for expected. |
| payments | array | \[…\] | List of payments entries. |
| payments\[\] | object | {…} | Object containing payments fields. |
| payments\[\].paymentMethodId | integer | 169093 | Identifier of the associated payment method. |
| payments\[\].amount | integer | 3150 | The amount value. |
| payments\[\].quantity | integer | 1 | The quantity value. |
| payments\[\].metadata | string | "{\\"paygreenPaymentType\\":\\"TRD\\",\\"firstName\\":\\"Alice\\",\\"lastName\\":\\"Mart… | Additional metadata supplied with the response. |

#### Response

```json
{
  "status": 200,
  "code": "required_action",
  "message": "Additional action required.",
  "data": {
    "orderId": 1927055,
    "requiredAction": {
      "type": "paygreenPaymentToProceed",
      "transactionId": "po_3f9c2a1b7d8e4c5f9a1b2c3d4e5f6a7b",
      "paymentUrl": "https://payment.paygreen.fr/po_3f9c2a1b7d8e4c5f9a1b2c3d4e5f6a7b",
      "result": {
        "status": "payment_order.pending",
        "threeDSecureStatus": null,
        "paymentErrorStatus": 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 | "required\_action" | Machine-readable application code for the result. |
| message | string | "Additional action required." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.orderId | integer | 1927055 | Identifier of the order. |
| data.requiredAction | object | {…} | Object containing required action fields. |
| data.requiredAction.type | string | "paygreenPaymentToProceed" | The type value. |
| data.requiredAction.transactionId | string | "po\_3f9c2a1b7d8e4c5f9a1b2c3d4e5f6a7b" | Identifier of the transaction. |
| data.requiredAction.paymentUrl | string | "https://payment.paygreen.fr/po\_3f9c2a1b7d8e4c5f9a1b2c3d4e5f6a7b" | The payment url value. |
| data.requiredAction.result | object | {…} | Object containing result fields. |
| data.requiredAction.result.status | string | "payment\_order.pending" | HTTP status code returned by the API. |
| data.requiredAction.result.threeDSecureStatus | null | null | The three dsecure status value. |
| data.requiredAction.result.paymentErrorStatus | null | null | The payment error status value. |

Note: `paymentMethodId` is Innovorder's internal id of the "Paygreen" payment method configured for the restaurant (step 1). `requiredAction.transactionId` is the Paygreen **Payment Order id** (`po_…`): store it on your side, it is your reconciliation key with the Paygreen back office and it is _not_ returned in the order's `payments[]` afterwards.

Classic trap: as for Stripe, `metadata` is a JSON _string_, not a JSON object: `"metadata": "{\"paygreenPaymentType\":\"TRD\"}"`. An object is rejected with `invalid_parameters` (400).

### 3\. Send the Customer to the Payment Page

Open `requiredAction.paymentUrl` for the customer, either as a full-page redirect or inside an iframe. Always use the URL returned by the API, never build it yourself. The customer selects the voucher network, pays the eligible part with vouchers and any remainder by card, all on the Paygreen page.

The return and cancel URLs are fixed by Innovorder: after paying or cancelling, Paygreen sends the customer back to an Innovorder-hosted page whose only job is to post a `window.postMessage` to its **parent window**. It is therefore designed for the iframe integration:

```javascript
window.addEventListener('message', (event) => {
    if (event.data?.id === 'paygreen-insite') {
        // Customer came back from the payment page.
        // event.data.message = { pid: 'po_...', result: 'payment_order.successed' | ..., orderId: '1927055' }
        closePaymentIframe();
        startPollingOrder(event.data.message.orderId); // step 4 - the order status is the only proof of payment
    } else if (event.data?.id === 'close-modal') {
        // Customer cancelled on the payment page: the order will end refused.
        closePaymentIframe();
    }
});
```

With a full-page redirect the customer lands on that (blank) Innovorder page: prefer the iframe, or keep your own "waiting for payment" screen open in a first tab and poll the order from there (step 4). In both cases, treat the message only as a signal to start polling, never as the payment confirmation.

### 4\. Wait for the Confirmation

Paygreen notifies Innovorder by webhook when the Payment Order reaches its final state. Until then the order is **awaiting action** and unpaid, and it is not yet part of the order read endpoints: `GET /v1/orders/{orderId}` answers `omnichannel_order_not_found` (400) in the meantime.

1.  Poll [Get Order Details](https://developers.innovorder.io/docs/orders/orders-retrieve.md) (`GET /v1/orders/{orderId}`) every few seconds with the `orderId` of step 2, until it returns the order with `status` `PAID`. Ticket, kitchen and customer notifications are triggered at that very moment.
2.  If Paygreen reports the payment as refused, expired or cancelled, the order is set to **refused** and never appears in the order endpoints. Create a new order for a retry; do not reuse the `orderId`.
3.  Give up after the lifetime of the Paygreen payment page (a few minutes): a customer who never came back leaves an expired Payment Order and a refused order behind, nothing to clean up on your side.

### Error Reference

| Code | HTTP | Meaning |
| --- | --- | --- |
| paygreen\_payment\_metadata\_invalid | 400 | The payment `metadata` is missing, is not valid JSON, or its `paygreenPaymentType` is not `TRD`, `RESTOFLASH` or `LUNCHR`. |
| invalid\_parameters | 400 | Schema violation - e.g. `metadata` sent as an object instead of a string. |
| paygreen\_not\_activated | 403 | The Paygreen module is not configured or not active on the restaurant. |
| payment\_method\_not\_activated | 403 | The `paymentMethodId` is not enabled for the consumption mode of the order. |
| paygreen\_payment\_type\_not\_supported | 403 | The requested voucher family is disabled on the restaurant's Paygreen module. |
| paygreen\_payment\_refused | 422 | Paygreen refused to create the Payment Order (shop not activated, invalid sub-shop…). The order is not created. |
| required\_action | 200 | Not an error - the order is created and waits for the customer to pay on `paymentUrl` (step 3). |
| omnichannel\_order\_not\_found | 400 | On `GET /v1/orders/{orderId}`: the order is not paid yet (keep polling) or ended refused. |
