Skip to content
innovorder
⌘K

Integration path

Integration Guide: Stripe Payments

System map · Stripe payment
Order
Stripe
Paid order

This guide explains how to accept online card payments through Innovorder's Stripe integration: how the platform model works, exactly what POST /orders expects in the payment metadata field, how to save cards for reuse, and how to handle 3-D Secure (SCA) challenges.

How Stripe works at Innovorder

  • We use Stripe Connect with direct charges: there is a single Innovorder platform Stripe account, and each restaurant is a connected account (acct_…).
  • Card PaymentMethods are always created and stored on the platform account.
  • When an order is placed, the Innovorder backend does the bridging for you: it clones the PaymentMethod to the restaurant's connected account, then creates and confirms the PaymentIntent there.
  • Client-side, you only ever use the Innovorder platform publishable key. You never use a restaurant's own Stripe keys. The single exception is the 3-D Secure step (step 5 below).

API-driven only - Stripe Checkout is not supported

Stripe's hosted payment page (checkout.stripe.com) and Payment Links cannot be used with Innovorder. The payment is created by our backend on the restaurant's connected account as an integral part of the order transaction (order validation, ticket creation, automatic refund on failure, 3-D Secure orchestration). A Checkout Session would create a payment outside that lifecycle, and no order would exist. Stripe is only used client-side for card collection and tokenization; everything payment-related goes through the Innovorder API.

1. Get the Platform Publishable Key

Tokenizing cards requires the Innovorder platform publishable key. This key is provided as part of an integration project: contact support@innovorder.fr to set one up and receive the key for the target environment.

Important: never create Stripe objects with a restaurant's own keys, and never pass a stripeAccount option when tokenizing. A PaymentMethod created on the wrong account cannot be used by our backend and the order will fail with resource_missing.

2. Tokenize the Card

Initialize the Stripe SDK (Stripe.js on the web, or the Stripe mobile SDK) with the platform publishable key, without any stripeAccount parameter, and create a PaymentMethod from the card fields. The resulting pm_… id lives on the platform account, which is exactly where our backend expects it.

javascript
const stripe = await loadStripe(INNOVORDER_PLATFORM_PUBLISHABLE_KEY); // no stripeAccount option

const { paymentMethod } = await stripe.createPaymentMethod({
    type: 'card',
    card: cardElement,
});
// paymentMethod.id → "pm_..." - send it in the order payment metadata (step 3)

3. Create the Order - the Metadata Contract

A Stripe payment is one entry of the payments array of POST /orders (see Create Orders for the full request body). The Stripe reference goes in the payment's metadata field, which must be a JSON-encoded string (a string containing JSON - note the escaping below) holding at least one of these two keys:

KeyTypeUse case
stripePaymentMethodIdstring (pm_…)One-shot payment with a freshly tokenized card (step 2). Also works for guest / anonymous checkout.
cardIdintegerA card previously saved through POST /cards/v2 (step 4). Requires the end customer to be authenticated and to own the card.

POST/ordersCreate an order paid with Stripe

Standard order creation with a Stripe payment entry. The metadata field carries the Stripe reference as a JSON-encoded string. We recommend sending an idempotency-key header to protect against double submissions.

Parameters

NameTypeRequiredDescription
idempotency-keystring (header)NoRecommended. 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-07-23T12:30:00.000Z",
  "payments": [
    {
      "paymentMethodId": 169092,
      "amount": 3150,
      "quantity": 1,
      "metadata": "{\"stripePaymentMethodId\":\"pm_1AbCdEf...\"}"
    }
  ]
}
Request Body Properties

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

PropertyTypeExampleDescription
restaurantIdinteger2285Identifier of the restaurant.
channelIdinteger2Identifier of the associated channel.
consumptionModestring"MODE_TAKE_AWAY"The consumption mode value.
menuIdinteger24194Identifier of the menu.
cartarray[…]List of cart entries.
cart[]object{…}Object containing cart fields.
cart[].productIdinteger1670891Identifier of the product.
cart[].quantityinteger1The quantity value.
cart[].stepsarray[]List of steps entries.
expectedAtstring"2026-07-23T12:30:00.000Z"Date or timestamp for expected.
paymentsarray[…]List of payments entries.
payments[]object{…}Object containing payments fields.
payments[].paymentMethodIdinteger169092Identifier of the associated payment method.
payments[].amountinteger3150The amount value.
payments[].quantityinteger1The quantity value.
payments[].metadatastring"{\"stripePaymentMethodId\":\"pm_1AbCdEf...\"}"Additional metadata supplied with the response.

Response

json
{
  "status": 200,
  "code": "order_created",
  "message": "Your order was successfully created.",
  "data": {
    "orderId": 1927055,
    "ticketNumber": "1747_2285_2026-07-23_2",
    "mainStatus": "VALIDATED",
    "paymentStatus": "PAID",
    "requiredAction": null
  }
}
Response Properties

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

PropertyTypeExampleDescription
statusinteger200HTTP status code returned by the API.
codestring"order_created"Machine-readable application code for the result.
messagestring"Your order was successfully created."Human-readable result message. Do not use this value for program logic.
dataobject{…}Endpoint-specific response payload.
data.orderIdinteger1927055Identifier of the order.
data.ticketNumberstring"1747_2285_2026-07-23_2"The ticket number value.
data.mainStatusstring"VALIDATED"The main status value.
data.paymentStatusstring"PAID"The payment status value.
data.requiredActionnullnullThe required action value.
Note: paymentMethodId (integer) is Innovorder's internal id of the "Stripe" payment method configured for the restaurant - it is not a Stripe object. The Stripe reference only ever travels inside metadata.

When is stripe_payment_metadata_invalid (400) returned?

  • metadata is missing or an empty string (it is optional in the general order schema, but required for Stripe payments);
  • metadata is not parseable JSON;
  • the parsed object contains neither key (e.g. "{}");
  • cardId is not a JSON integer - "{\"cardId\":\"42\"}" (string) fails, "{\"cardId\":42}" passes;
  • stripePaymentMethodId is not a string, or is an empty string;
  • cardId is sent on an unauthenticated request - guest checkout can only use stripePaymentMethodId.
Classic trap: sending metadata as a JSON object instead of a string fails even earlier with a generic invalid_parameters error. It really must be "metadata": "{\"stripePaymentMethodId\":\"pm_xxx\"}" - not "metadata": {"stripePaymentMethodId": "pm_xxx"}. Extra keys inside the JSON string are tolerated and ignored; if both keys are present, cardId takes precedence.

4. Save a Card for Reuse (Optional)

To let an authenticated customer reuse a card, register it through the Cards (Stripe) SetupIntent flow - the SDK stays initialized with the platform key (no stripeAccount):

javascript
// 1. Request a SetupIntent (no cardToken) - POST /cards/v2
//    { "name": "My Visa" }  →  { "client_secret": "seti_..._secret_..." }

// 2. Confirm it client-side with the card fields
const { setupIntent } = await stripe.confirmCardSetup(clientSecret, {
    payment_method: { card: cardElement },
});

// 3. Persist the card - POST /cards/v2
//    { "name": "My Visa", "cardToken": setupIntent.payment_method }
//    →  { "cardId": 4521, "brand": "visa", "last4": "4242", "name": "My Visa" }

// 4. Pay with the saved card - POST /orders
//    "metadata": "{\"cardId\":4521}"

The Stripe Customer is created automatically on our platform account at the first card save - you never create or fetch it. There are no Ephemeral Keys and no "PaymentSheet-style" Customer endpoints in the Innovorder API: our flow does not use them.

5. Handle 3-D Secure (SCA)

The PaymentIntent is confirmed server-side. If the cardholder's bank requires authentication, POST /orders still returns HTTP 200, but with code required_action:

json
{
  "status": 200,
  "code": "required_action",
  "message": "Additional action required.",
  "data": {
    "orderId": 1927055,
    "requiredAction": {
      "type": "challengeStripe",
      "intentId": "pi_xxx",
      "requires_action": true,
      "payment_intent_client_secret": "pi_xxx_secret_yyy"
    }
  }
}
  1. Re-initialize the Stripe SDK on the restaurant's connected account: platform publishable key + stripeAccount: <stripeUserId> (acct_…). The stripeUserId is exposed in the restaurant's Stripe module configuration returned by Get Web Ordering Configuration (restaurant.moduleStripe.stripeUserId). This is the only place the connected account ever appears client-side - the PaymentIntent lives on the restaurant's account, so the challenge must run there.
  2. Run the challenge: handleNextAction / handleCardAction(payment_intent_client_secret).
  3. Confirm the order with POST /orders/confirm (see Confirm Payment), sending the intentId from requiredAction as paymentIntentId.
javascript
const stripeConnected = await loadStripe(INNOVORDER_PLATFORM_PUBLISHABLE_KEY, {
    stripeAccount: restaurant.moduleStripe.stripeUserId, // acct_...
});

const { paymentIntent, error } = await stripeConnected.handleCardAction(clientSecret);
if (!error) {
    // POST /orders/confirm  { "orderId": 1927055, "paymentIntentId": paymentIntent.id }
}

If the intent still requires action at confirm time, POST /orders/confirm returns a 400 stripe_still_requires_action with the client_secret again in extraData, so you can retry the challenge.

Error Reference

CodeHTTPMeaning
stripe_payment_metadata_invalid400The payment metadata does not match the contract of step 3.
invalid_parameters400Schema violation - e.g. metadata sent as an object instead of a string.
stripe_not_activated400Stripe is not configured/activated for that restaurant.
resource_missing500The pm_… was not found on the platform account - almost always a PaymentMethod created with the wrong key or with a stripeAccount option set.
card_not_found / card_forbidden400 / 403cardId unknown, or not owned by the authenticated customer.
stripe_card_declined / stripe_card_expired403 / 400Bank refusal.
required_action200Not an error - a 3-D Secure challenge is needed (step 5).
stripe_still_requires_action400Challenge not completed before POST /orders/confirm.