Skip to main content
This guide walks through a complete booking integration against the Way API: from showing a brand’s experiences to charging a card and confirming the booking. By the end you will have executed every call in this sequence:
  1. Get brand settings → currency, payment methods, payment platform
  2. Get all listings → what can be booked
  3. Get listing sessions → when it can be booked, for how much
  4. Get custom questions → what to ask the guest
  5. Create a payment intent → Stripe client secret
  6. Create a booking → the booking (pending payment)
  7. Confirm the payment with Stripe → Way finalizes the booking
Way never touches the card. Card data goes directly to Stripe from your checkout; Way only ever sees a Stripe client secret and reconciles the result server-side. As long as you use Stripe Elements (recommended below), your integration stays out of PCI-DSS SAQ-D scope.
All examples run against staging (https://api.staging.letsway.com) with these placeholder credentials - substitute your own from the dashboard:
1

Authenticate and read the brand configuration

Start with Get brand settings - it doubles as an auth smoke test and tells you three things you need later: the brand’s currency, its enabled payment methods, and its payment platform (US vs EU Stripe - this decides which Stripe publishable key you use in step 8).
You’ll need a Stripe publishable key from Way to initialize Stripe.js in step 8 - ask your Way representative during onboarding; there is no API call to fetch it. This is Way’s key, not one from your own Stripe account: payments are created on Way’s Stripe platform, so a key from your own account will not work. Keys are specific to the environment (staging vs production) and to your brand’s paymentPlatform (stripe-us vs stripe-eu) - for this guide, ask for the staging key.
2

Pick a listing and detect its type

Fetch the brand’s listings with Get all listings and show them to your user. When one is selected, load its full detail with Get a listing.
The kind field decides how the rest of the flow branches. The same endpoints serve both kinds - only a few request fields differ, called out at each step below.From the listing detail, keep the experienceId and (for resource listings) the resourceGroups[].id values - you need them for availability and booking.
For resource listings the listing-level experienceId is null - each session returned in the next step carries the experienceId to book with.
3

Choose a session and compute the price

Get availability for the listing with Get listing sessions:
The response is a data.sessions array:
Record the guest’s chosen startDateTime (this becomes sessionTime in the booking payload), duration (becomes sessionDuration), and a price tier name per participant - the booking payload references tiers by name.For richer date-picker UX (month views, first-available shortcuts), see the Availability endpoints.
4

Collect required booking information

Experiences can define extra questions that must be answered at booking time. Fetch them with Get custom questions:
An empty items array means there is nothing extra to ask. Render any questions in your checkout form and collect the answers keyed by questionId.Answers travel inside the booking call (step 7) - there is no separate save endpoint. The Create a booking payload accepts them at three scopes, depending on who or what the question is about:All three use the same shape:
key is the selected option’s key for select/choice questions and null for free-text and checkbox answers (value is a string, or a boolean for checkboxes).
Answers are validated when you call book-bulk: required questions must be answered, and select answers must match one of the question’s options - violations return 422. Way stores a snapshot of each question with the answer, so later edits to a question don’t distort past bookings.
If you display terms and conditions, fetch them with Get terms and conditions.
5

Create your cart - locally

There is no create-cart endpoint, by design. You mint the cart ID yourself - any UUID (v4 or v7) - and Way accepts it lazily: the first time the platform hears about your cartId is when it appears in the payment-intent URL or the booking payload.
Rules that keep checkout reliable:
  • Reuse the same cartId across the payment intent → booking → payment confirmation of one checkout attempt. It’s the thread that ties them together.
  • Rotate to a fresh UUID on any failure before retrying. A cart that already has a payment intent or booking attached can be rejected as non-reusable (422).
  • Nothing to clean up on success. On failure, optionally call Discard cart and start over with a fresh ID.
Assemble the cart items in your own state. This data[] item shape is used identically by the payment intent (step 6) and the booking (step 7). Every field traces back to something you fetched earlier:
Omit resourceGroupId and resourceQuantity entirely.
6

Create the payment intent

For card payments with a total above zero, create a payment intent scoped to your cart with Create a payment intent. The body’s data[] takes the cart items from step 5:
Keep the paymentIntentSecret - Stripe.js uses it to mount the payment form in step 8. The final charge is confirmed against the clientSecret returned by the booking call in step 7, which is the authoritative one for the checkout.Skip this step entirely when the total is zero (fully discounted) or the guest pays with a non-card method (step 10).
To show a running total before checkout, call this endpoint with "getOnlyTotalNetAmount": true - it prices the cart without initializing a payment.
7

Create the booking - before the card is charged

Ordering: the booking is created first, then the card is charged. Create a booking creates a booking in processing state that already holds the inventory, and returns the definitive Stripe clientSecret to charge against:
The data[] items are the same cart items from step 5, now carrying the custom-question answers collected in step 4 (per participant here; experience-level answers go in data[].additionalCustomQuestionResponses, purchaser-level ones in purchaser.customQuestionResponses).
Three things to note here:
  • The link between booking and payment is the cartId - there is no payment-intent ID field in this payload. Way correlates them server-side.
  • The confirmationCode in this response is the cart’s confirmation code (subjectType: "cart"). Each booking created from the cart gets its own confirmation code - you retrieve it in step 9 using the cartConfirmationCode filter.
  • amount here is in Stripe’s minor units for card payments (17800 = $178.00) because it echoes the payment intent - unlike every other amount in the API, which uses major units. See Amounts and currency.
8

Charge the card with Stripe

Use Stripe Elements with the clientSecret from the booking response. The card number lives only inside Stripe’s iframe; your code and the Way API never see it.
redirect: "if_required" triggers the 3D Secure challenge only when the issuer demands it. On redirect flows, Stripe returns to your return_url with payment_intent_client_secret in the query - re-check the intent status on return. Brands on stripe-eu will hit SCA challenges far more often than US brands.
A fully headless integrator can confirm the intent against Stripe’s API instead (this is a Stripe call, not a Way call):
The {id} is the part of the client secret before _secret_. Authenticate the call the same way Stripe.js does: pass the brand’s publishable key as the bearer token together with the client_secret in the form body. Use Stripe test tokens like pm_card_visa on staging.
Sending raw card numbers to Stripe’s REST API puts you in PCI-DSS SAQ-D scope. Unless you are already a certified PCI processor, use Stripe Elements.
Way exposes no confirm or status endpoint for payment intents - confirmation and status checks are entirely Stripe-side (stripe.confirmPayment, stripe.retrievePaymentIntent). Way learns the payment settled through its own Stripe webhook and finalizes the booking server-side; there is no “payment done” call for you to make.
9

Verify success - and handle failure

After confirmPayment, check paymentIntent.status === "succeeded". That’s your UX signal; the authoritative reconciliation is Way’s Stripe webhook, which flips the booking from processing to confirmed and sends the guest their confirmation email.Verify from your server with Get bookings, filtering by cartConfirmationCode (from step 7) or cartId:
Each item carries the booking’s own confirmationCode (the code the guest sees) and its id - the booking ID you’ll need for Get booking, cancellation, and rescheduling later. Subscribe to the booking.completed webhook to be notified without polling.On payment failure:
  1. Call Discard cart to release the cart.
  2. Rotate to a fresh cartId and restart from step 6 - the old cart is not reusable.
A card booking that never gets a confirmed payment stays processing and keeps holding capacity until platform cleanup runs. Fine for testing - but in production, always discard the cart on failure rather than abandoning it.
10

Non-card payment methods

paymentMethod in the booking payload also accepts cash and room-charge. These skip Stripe entirely - no payment intent, no confirmation - and the booking is confirmed synchronously in the book-bulk response. The same applies to any booking with a zero total.For room-charge (posting the cost to the guest’s hotel folio), validate the guest’s reservation first with Validate room charge, then pass the returned reservation fields into book-bulk under paymentDetail. The full flow - validation, payload fields, and per-PMS behavior for OPERA, StayNTouch, Infor, and Mews - is covered in the Room Charge Integration guide.

Common errors