Alternative - User payment with Stripe SDK

Integrate Stripe Elements directly into your checkout to accept user payments for LiteAPI bookings, with full control over the payment UI.

The standard way to accept user payments with LiteAPI is the LiteAPI Payment SDK, which wraps the payment form for you and requires only a few lines of configuration. Some applications, however, need finer control over the checkout experience — custom styling beyond the SDK's appearance options, tighter integration with an existing Stripe Elements implementation, or a fully native checkout flow.

For these use cases, LiteAPI supports integrating Stripe Elements directly. You embed Stripe's Payment Element in your own frontend, and LiteAPI handles the creation of the underlying PaymentIntent, the capture of funds, and the settlement of the booking. You never need your own Stripe account or your own server-side Stripe integration — the PaymentIntent is created on LiteAPI's Stripe platform when you call the prebook endpoint.

📘

Access required

Direct Stripe integration requires LiteAPI's Stripe publishable keys (one for sandbox, one for production). These are not exposed in the dashboard — contact your account manager or customer support to request them. All other steps use your standard LiteAPI API keys.


How it works

The flow mirrors the standard user payment flow; the only difference is that your own Stripe Elements code replaces the LiteAPI Payment SDK in step 3:

  1. Search rates and select an offer (Hotel rates endpoint).
  2. Prebook the offer with usePaymentSdk: true (Prebook endpoint). The response includes a secretKey — a Stripe PaymentIntent client secret — along with a transactionId and prebookId.
  3. Mount Stripe Elements in your frontend using LiteAPI's publishable key and the secretKey from prebook, then confirm the payment with Stripe.js.
  4. Redirect — after a successful payment the user lands on your return_url, where you collect guest details.
  5. Book — finalize the reservation with the Book endpoint, passing the prebookId and the transactionId.

The amount and currency of the PaymentIntent are set automatically from the prebooked offer (including any add-ons and voucher discounts). You never create, modify, or capture the PaymentIntent yourself — you only confirm it from the client using the client secret.


Prerequisites

ItemSandboxProduction
LiteAPI API keysand_... key from your dashboardprod_... key from your dashboard
Stripe publishable keyProvided by LiteAPI (pk_test_...)Provided by LiteAPI (pk_live_...)
Stripe.jshttps://js.stripe.com/v3/ (or @stripe/stripe-js / @stripe/react-stripe-js via npm)Same

⚠️ The environments must match end-to-end: a secretKey generated with a sandbox API key can only be confirmed with the sandbox publishable key, and likewise for production. Mixing environments is the most common cause of resource_missing / "No such payment_intent" errors from Stripe.


Step 1 — Prebook with usePaymentSdk: true

Call the prebook endpoint with the offerId of the selected rate and set usePaymentSdk to true so the response includes the payment fields:

curl --request POST \
     --url https://book.liteapi.travel/v3.0/rates/prebook \
     --header 'X-API-Key: sand_c0155ab8-c683-4f26-8f94-b5e92c5797b9' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "offerId": "GEHT5Y...",
  "usePaymentSdk": true
}
'

The response contains (among other fields):

{
  "data": {
    "prebookId": "xZFwjDyps",
    "transactionId": "tr_ct_K5NdYC5eG-o4x66FhghMR",
    "secretKey": "pi_3PMY53A4FXPoRk9Y0O7lKP0d_secret_oxPGV4comgzkYDuT2jnr7ZEfo",
    "price": 234.5,
    "currency": "USD",
    "...": "..."
  }
}
FieldWhat it isWhere you use it
secretKeyThe Stripe PaymentIntent client secret (pi_..._secret_...)Passed to Stripe Elements / stripe.confirmPayment in your frontend
transactionIdLiteAPI's reference to the payment transactionPassed to the Book endpoint after payment succeeds
prebookIdThe checkout session for the ratePassed to the Book endpoint after payment succeeds

Save transactionId and prebookId together, immediately. Each prebook call generates a new pair, and they cannot be retrieved later. If they are lost after a successful payment, the booking cannot be finalized and the payment hold is released automatically after 1–2 business days.

Security note: the client secret is sensitive — anyone who has it can complete (or interfere with) the payment for that intent. Deliver it to the paying user's browser only; never log it, cache it, or embed it in URLs.


Step 2 — Mount the Payment Element

Load Stripe.js, initialize it with the LiteAPI publishable key for your environment, and create an Elements instance with the secretKey (client secret) from the prebook response.

Plain HTML / JavaScript

<html>
  <head>
    <title>Checkout</title>
    <script src="https://js.stripe.com/v3/"></script>
  </head>
  <body>
    <form id="payment-form">
      <div id="payment-element"><!-- Stripe injects the Payment Element here --></div>
      <button id="submit" type="submit">Pay now</button>
      <div id="error-message"></div>
    </form>

    <script>
      // LiteAPI's publishable key for the environment matching your API key.
      // pk_test_... for sandbox, pk_live_... for production.
      const stripe = Stripe('pk_test_XXXXXXXXXXXXXXXX');

      // The secretKey returned by the LiteAPI prebook call.
      const clientSecret = 'pi_3PMY53A4FXPoRk9Y0O7lKP0d_secret_oxPGV4comgzkYDuT2jnr7ZEfo';

      const elements = stripe.elements({
        clientSecret: clientSecret,
        appearance: { theme: 'stripe' }, // fully customizable
      });

      const paymentElement = elements.create('payment');
      paymentElement.mount('#payment-element');

      const form = document.getElementById('payment-form');
      form.addEventListener('submit', async (event) => {
        event.preventDefault();

        const { error } = await stripe.confirmPayment({
          elements,
          confirmParams: {
            // Where the user is redirected after payment.
            // Your booking finalization page — see Step 4.
            return_url: 'https://my.app.travel/booking/confirm?prebookId=xZFwjDyps',
          },
        });

        // This code only runs if there is an immediate error
        // (e.g. invalid card details). Otherwise the user is redirected.
        if (error) {
          document.getElementById('error-message').textContent = error.message;
        }
      });
    </script>
  </body>
</html>

React (@stripe/react-stripe-js)

import { loadStripe } from '@stripe/stripe-js';
import {
  Elements,
  PaymentElement,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js';

// LiteAPI's publishable key — load once, outside the component tree
const stripePromise = loadStripe('pk_test_XXXXXXXXXXXXXXXX');

function CheckoutForm({ prebookId }) {
  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!stripe || !elements) return;

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `https://my.app.travel/booking/confirm?prebookId=${prebookId}`,
      },
    });

    if (error) {
      // Show error.message to the user
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button disabled={!stripe}>Pay now</button>
    </form>
  );
}

export default function Checkout({ secretKey, prebookId }) {
  // secretKey = the client secret returned by the LiteAPI prebook call
  return (
    <Elements stripe={stripePromise} options={{ clientSecret: secretKey }}>
      <CheckoutForm prebookId={prebookId} />
    </Elements>
  );
}

Because you own the Elements instance, all of Stripe's Appearance API options, layout variants, and locale settings are available to you — this is the main advantage over the wrapped LiteAPI Payment SDK.

📘

Payment methods

The Payment Element automatically displays the payment methods enabled on the PaymentIntent (cards, wallets such as Google Pay / Apple Pay where eligible). Availability can vary by currency and region. Do not pass your own paymentMethodTypes overrides unless agreed with LiteAPI.


Step 3 — Confirm the payment

stripe.confirmPayment (shown above) handles the entire confirmation, including 3D Secure / SCA challenges when the issuing bank requires them. Two outcomes are possible:

  • Immediate error — validation or card errors are returned synchronously; display error.message and let the user retry. The same secretKey remains valid for retries.
  • Success (or pending next action) — Stripe redirects the browser to your return_url with the query parameters payment_intent, payment_intent_client_secret, and redirect_status appended.

Include everything you need to finalize the booking in the return_url (or in server-side session state keyed to the user): at minimum, a way to recover the prebookId and transactionId saved in Step 1.


Step 4 — Handle the return and verify payment status (optional)

On your return_url page, verify the payment before proceeding. Retrieve the PaymentIntent with the client secret Stripe appended to the URL:

const stripe = Stripe('pk_test_XXXXXXXXXXXXXXXX');

const clientSecret = new URLSearchParams(window.location.search)
  .get('payment_intent_client_secret');

const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);

switch (paymentIntent.status) {
  case 'succeeded':
  case 'requires_capture': // funds authorized; LiteAPI captures on booking
    // Payment OK — collect guest details and call the Book endpoint
    break;
  case 'processing':
    // Show a "payment processing" message and poll or wait
    break;
  case 'requires_payment_method':
    // Payment failed — send the user back to checkout to retry
    break;
}

Once payment is confirmed, tell the user the payment was successful and collect the remaining information needed to finalize the reservation: the booking holder's first name, last name, and email, plus guest details per room.


Step 5 — Finalize the booking

Send the guest details to the Book endpoint, passing the prebookId from Step 1 and a payment object with method set to TRANSACTION_ID and the transactionId from Step 1:

curl --request POST \
     --url https://book.liteapi.travel/v3.0/rates/book \
     --header 'X-API-Key: sand_c0155ab8-c683-4f26-8f94-b5e92c5797b9' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "prebookId": "xZFwjDyps",
  "holder": {
    "firstName": "John",
    "lastName": "A Richards",
    "email": "[email protected]"
  },
  "payment": {
    "method": "TRANSACTION_ID",
    "transactionId": "tr_ct_K5NdYC5eG-o4x66FhghMR"
  },
  "guests": [
    {
      "occupancyNumber": 1,
      "remarks": "quiet room please",
      "firstName": "Sunny",
      "lastName": "Mars",
      "email": "[email protected]"
    }
  ]
}
'

The API returns the confirmed booking, which will also appear in your dashboard under Bookings. Display the confirmation to the customer and email them a copy for their records.

If the booking call is never made, the payment hold on the customer's card is released automatically after 1 hour.


Testing in sandbox

Use your sandbox LiteAPI key and the sandbox publishable key, then test with Stripe's standard test cards:

Card numberBehavior
4242 4242 4242 4242Succeeds without authentication
4000 0027 6000 3184Triggers a 3D Secure authentication challenge
4000 0000 0000 0002Declined (generic decline)

Any future expiration date and any 3-digit CVC work with test cards. Run at least one full end-to-end test — prebook → Elements → redirect → book — including a 3DS challenge, before going live.


Going to production

  1. Swap your sandbox LiteAPI key for your production key.
  2. Swap the sandbox publishable key (pk_test_...) for the production one (pk_live_...).
  3. Ensure your return_url is an HTTPS URL on your production domain.
  4. Verify your domain with LiteAPI if you plan to offer Apple Pay (contact support).

Troubleshooting

SymptomLikely causeFix
Stripe error: No such payment_intentPublishable key environment doesn't match the API key used for prebookUse pk_test_... with sandbox keys, pk_live_... with production keys
Payment Element never rendersInvalid or expired secretKey, or clientSecret not passed to stripe.elements()Re-run prebook and use the fresh secretKey
Payment succeeded but booking failstransactionId/prebookId pair lost or mismatched (each prebook generates a new pair)Always persist both together at prebook time; if lost, the hold auto-releases in 1–2 business days
Book call rejected with payment errorpayment.method not set to TRANSACTION_ID, or transactionId from a different prebookPass the exact transactionId returned alongside the prebookId you are booking
Amount looks wrong in the payment formAdd-ons/voucher applied after prebookThe PaymentIntent amount is fixed at prebook time — re-prebook with the final cart

FAQ

Do I need my own Stripe account?
No. The PaymentIntent is created on LiteAPI's Stripe platform. You only need Stripe.js on the frontend and the publishable keys LiteAPI provides.

Can I create or modify the PaymentIntent myself?
No. The intent's amount, currency, and capture behavior are managed by LiteAPI based on the prebooked offer. Your integration only confirms the intent from the client.

Can I still use the LiteAPI Payment SDK on some pages and direct Elements on others?
Yes. Both consume the same secretKey from prebook, so you can mix approaches across products or platforms.

How long is the secretKey valid?
It is tied to the prebook session. If the session expires or the rate changes, re-run prebook and mount Elements with the new secretKey.

What about webhooks?
Booking status changes can be monitored with LiteAPI webhooks. Stripe webhooks are not available for this integration, since the Stripe account is operated by LiteAPI.


What's next



Did this page help you?