Integrating Turneo into your Mobile App

What do you need to start?

Before you get started with this guide you will need to:

  • Have an active Experience Store in your Turneo profile
  • Have at least one Turneo Element created in the Elements tab (for the embedded approach)
  • Have a Turneo API key for your partner account (for the native approach) — ask your Turneo contact if you don't have one yet

Overview

Hotel apps are where guests already are: they check in, open the room, order room service. Adding experiences to the same app lets guests browse and book tours, spa treatments and activities without leaving it.

There are two ways to bring Turneo into a mobile app, and they can be combined:

Approach Best for Effort You build
Embedded — render a Turneo Element or Store inside a WebView Getting live quickly with the full booking and checkout flow Days One WebView screen and a hosted HTML page
Native — build your own screens on the Turneo API Apps with their own design system, checkout, or payment (e.g. room charging, loyalty points) Weeks Listing, detail, availability and checkout screens, plus a backend proxy

Most partners start with the embedded approach and move individual screens to native later. A common hybrid is a native experiences list built on the API that hands over to the WebView for the booking and payment step.

One rule for both approaches

Never ship your Turneo API key inside the app binary. API keys identify your partner account and can list and manage every booking you've sold. Anything that needs the key must go through your own backend.


Approach 1: Embedding a Turneo Element in a WebView

A Turneo Element is a JavaScript widget that renders a curated list of experiences with the full detail, availability and checkout flow — including card payments, Apple Pay and Google Pay — inside a <turneo-element> tag. Inside a mobile app, you render it in a WebView.

1. Host a page for the WebView

Don't load the widget from an HTML string inside the app. Host a small page on your own domain (e.g. https://www.yourhotel.com/app/experiences) and point the WebView at it. There are two reasons:

  • Payment providers redirect the guest back to the page they started on after 3-D Secure or wallet authentication. That only works when the page has a real https:// URL.
  • Apple Pay is verified per domain (see Enabling Apple Pay). A page served from your domain can use the verification you already did for your website.

The page itself is minimal — the same two lines you would add to your website, wrapped in a mobile viewport:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <style>
      body { margin: 0; padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom); }
    </style>
  </head>
  <body>
    <turneo-element element-id="<your-element-id>"></turneo-element>
    <script src="https://widget-turneo.vercel.app/turneo-widget.iife.js"></script>
  </body>
</html>

Replace <your-element-id> with the id from the Elements tab. The Element is responsive, so it adapts to the phone's width without further configuration.

2. Configure the WebView

The widget is a regular single-page web app, so it needs the same permissions a modern site does. In particular:

Setting Why
JavaScript enabled The widget is JavaScript-rendered
DOM storage (localStorage) enabled The shopping cart and the guest's in-progress order are stored in the page's local storage
Third-party cookies / cross-origin requests allowed Payment iframes (Stripe) and 3-D Secure pages are served from other origins
target="_blank" links handled The widget opens some links (e.g. organizer terms) in a new tab; a WebView with no popup handler drops them silently — open them in the system browser instead
Back navigation wired to goBack() The widget uses the page's query string (?turneoExperience=...) for navigation, so the platform back gesture should navigate the WebView history, not close the screen

Platform notes:

  • iOS (WKWebView) — set allowsInlineMediaPlayback so experience videos play inline, and implement WKUIDelegate.createWebViewWith to catch new-window requests and open them via UIApplication.open.
  • Android (WebView) — enable javaScriptEnabled, domStorageEnabled, and set setSupportMultipleWindows(true) with a WebChromeClient.onCreateWindow that hands the URL to an Intent.ACTION_VIEW.
  • React Native / Flutterreact-native-webview and webview_flutter expose the same switches (javaScriptEnabled, domStorageEnabled, setSupportMultipleWindows / onCreateWindow). Avoid in-app-browser components that reset storage between opens, or the guest loses their cart.

Apple Pay and Google Pay inside WebViews

Card payments with 3-D Secure work inside an embedded WebView. Wallet buttons are less predictable: Apple Pay on the Web and Google Pay are designed for the system browser, and embedded WKWebView / Android WebView components restrict or block them depending on OS version. If wallets matter to your guests, open the page in an in-app browser tab instead (SFSafariViewController on iOS, Chrome Custom Tabs on Android) — you keep the app context, and the guest gets the full browser payment sheet. Test the exact combination you ship on real devices.

3. Pass what you already know about the guest

Your app usually knows who the guest is. Pass that to the widget so the checkout form is pre-filled — the same query parameters that work on your website:

https://www.yourhotel.com/app/experiences
  ?turneoCheckout_firstName=John
  &turneoCheckout_lastName=Smith
  &turneoCheckout_email=john.smith@example.com
  &turneoCheckout_phone=%2B385992101824
  &turneoCheckout_resellerReference=LOYALTY-48213

turneoCheckout_resellerReference deserves special attention: whatever you pass here (a loyalty id, a PMS reservation number, your own user id) is stored on every booking the guest makes as resellerReference. It is the key you'll use later to find the guest's bookings from your backend (see Showing bookings in your app).

Other useful parameters:

  • language attribute on the tag (de, it, hr, sl, pt) — match the app's locale. You can also render the tag from the host page's lang attribute.
  • turneo_startDate / turneo_endDate — pre-filter the Element to the guest's stay dates.
  • turneo_categories — show only e.g. Wellness,Adventure when opening from a themed section of your app.
  • turneoExperience=<experienceId>_<slug> — deep-link straight into one experience, e.g. from a push notification or a room-service upsell card.

The full list is in Adding Turneo Elements.

4. Knowing when a booking was made

The widget completes the booking and shows the confirmation inside the WebView, and the guest receives the confirmation email as usual. To react in your app (close the screen, show the booking on the home tab, award points), you have two options:

  • Watch the URL. After payment the widget returns to the host page with ?orderId=<uuid> in the query string. Intercept navigation in the WebView and treat that as "an order was just paid". Look the order up from your backend with GET /v2/booking-orders/{id}.
  • Poll from your backend. Query GET /v2/bookings?resellerReference=<guest reference> (or the activity feed) after the WebView closes. This works even if the guest completes payment in an external browser.

Approach 2: Building native screens on the Turneo API

When the experience section should look and feel exactly like the rest of your app, build it on the API. The booking flow is the same one described in Getting Started with Experiences: search, get options and availability, create an order, confirm it.

Architecture: your backend in the middle

Mobile app  ──(your auth)──▶  Your backend  ──(X-API-Key)──▶  api.turneo.co

Your backend holds the Turneo API key and exposes only what the app needs. This gives you three things for free:

  • The key never leaves your servers.
  • You can attach your own guest identity to each request (and set resellerReference on every order from the logged-in user, rather than trusting the app).
  • You can cache the experience catalogue, which changes rarely, while always fetching availability live.

Keep the proxy thin — pass Turneo's camelCase JSON straight through where you can, so the API reference stays your reference.

Step 1: List experiences for the store

Fetch the experiences in the store your app should show and cache the result for a few minutes:

curl -X GET "https://api.turneo.co/v2/experiences?storeId=<storeId>&language=de" \
     -H "Accept: application/json" \
     -H "X-API-Key: <your-api-key>"

Use language to get single-language strings in the app's locale — see Multi-language content. For a "what can I do during my stay" screen, add from and until with the guest's stay dates so only experiences with capacity in that window are returned.

Step 2: Show options and availability

On the detail screen, load the experience's options and paint a calendar from the availability/dates endpoint, then load availability/slots for the tapped day. Availability changes constantly — never cache it in the app, and re-check it right before creating the order.

Step 3: Create the order

When the guest confirms participants and a slot, your backend creates the order with the guest's details and your reference:

curl -X POST "https://api.turneo.co/v2/booking-orders" \
     -H "Content-Type: application/json" \
     -H "X-API-Key: <your-api-key>" \
     -d '{
           "travelerInformation": {
             "firstName": "John",
             "lastName": "Smith",
             "email": "john.smith@example.com",
             "phone": "+385992101824"
           },
           "resellerReference": "LOYALTY-48213",
           "bookings": [
             {
               "variantId": "<variantId>",
               "availabilitySlotId": "<availabilitySlotId>",
               "participantCounts": { "<participantTypeId>": 2 }
             }
           ]
         }'

Bookings start ON_HOLD, which reserves the slot while the guest pays. The hold is short, so move straight on to payment.

Step 4: Take payment and confirm

This is where a native integration differs most from the widget, because you decide how the guest pays:

Payment model What you do
Room charge / pay at hotel Confirm the order immediately with POST /v2/booking-orders/{id}/confirm. Post the charge to the folio yourself, or let Turneo do it if your property has a PMS integration.
Your own payment provider Charge the guest in your app, then confirm the order. Record the payment result on the order so it shows correctly in Turneo Hub.
Turneo collects the payment Call POST /v2/booking-orders/{id}/payment-link to email the guest a hosted payment page, or open the Turneo checkout for that order in a WebView. Confirming happens automatically once payment succeeds.

After confirmation each booking moves to ACCEPTED (instant confirmation) or PENDING (the organizer has to accept within their response window). Show PENDING honestly in the app — "Requested, awaiting confirmation" — and update it when it changes.

Step 5: Handle the lifecycle

Bookings can change after the guest leaves the screen: the organizer accepts or rejects, offers an alternative slot, the guest cancels from the confirmation email. Your app should read status from Turneo rather than storing its own copy:

  • GET /v2/bookings/{id} — a single booking, including bookingStatus, alternativeSlots, and the voucher.
  • POST /v2/bookings/{id}/cancel — cancel from within the app. Check the experience's refundPolicy and cutOffTime first and show the guest what they'll get back.
  • POST /v2/bookings/{id}/accept-alternative — let the guest accept an alternative slot the organizer offered.

Approach 3: Hybrid — native list, embedded detail and checkout

This is the most common setup among hotel apps today. The app owns the part guests see first — the experiences list on the home screen or in a "Things to do" tab, built natively from the API — and hands over to the Element in a WebView the moment a guest taps an experience. The detail page, availability, participants and payment are then all handled by Turneo.

It works because the widget can open directly on one experience. Append turneoExperience=<experienceId>_<slug> to the URL of your host page (the one from Approach 1) and the Element skips its own listing and shows that experience's detail page:

https://www.yourhotel.com/app/experiences
  ?turneoExperience=8efde4f9-916b-4612-b927-4a4b898183a9_Wine-tasting
  &turneoCheckout_firstName=John
  &turneoCheckout_email=john.smith@example.com
  &turneoCheckout_resellerReference=LOYALTY-48213

The experienceId is the id you got from GET /v2/experiences; the slug after the underscore is cosmetic and can be any URL-safe version of the name. The Element you embed must include the experience — the simplest way is one Element that lists the same store your app fetches from, so the two can never disagree.

In practice the flow is:

  1. Your backend fetches GET /v2/experiences?storeId=<storeId>&language=<locale> and caches it for a few minutes. Add the guest's stay dates as from and until if you only want to show what's bookable during their stay.
  2. Your app renders native cards from the response — name, the first of images, the lowest option price, duration and category — in your own design, mixed with whatever else the screen shows.
  3. On tap, the app opens the WebView on the host page with turneoExperience and the guest's prefill parameters. The guest sees the full detail page in the hotel's Element branding and books there.
  4. After booking, use the techniques in Knowing when a booking was made to close the WebView and refresh the guest's itinerary.

Why partners pick this over a fully native build: the list is where the app's own look, personalisation and placement matter most, while the detail and checkout are where the complexity lives — availability rules, participant types, add-ons, payment methods, vouchers, cancellation emails — and all of that keeps working as Turneo ships changes, with nothing to update in the app.


Showing the guest's bookings in your app

Both approaches end with bookings in Turneo that you'll want to display in the app's "My stay" or itinerary screen. From your backend:

curl -X GET "https://api.turneo.co/v2/bookings?resellerReference=LOYALTY-48213&bookingStatus=PENDING,ACCEPTED&ordering=time" \
     -H "Accept: application/json" \
     -H "X-API-Key: <your-api-key>"

resellerReference is the reference you passed at checkout (via turneoCheckout_resellerReference in the widget, or resellerReference on the order in the API). If you didn't set one, travelerEmail matches the guest's email exactly.

Each booking includes everything an itinerary card needs: experience.name and experience.images, slotStartTime in the experience's local time, meetingPoint, totalParticipants, finalPrice, bookingCode and the voucher (with QR code, where the organizer uses tickets).

Keeping your app in sync

Rather than polling every booking, poll the activity feed and react only to what changed:

curl -X GET "https://api.turneo.co/v2/activities?category=booking&since=2026-09-01T12:00:00Z" \
     -H "Accept: application/json" \
     -H "X-API-Key: <your-api-key>"

Pass the occurredAt of the last event you processed as since. Events cover bookings created, confirmed, cancelled and rejected, each with a snapshot of the booking at that moment — enough to update the itinerary card or send a push notification without a second request.

Checklist before you ship

  • API key lives only on your backend, never in the app
  • resellerReference is set on every booking so you can find it again
  • WebView allows JavaScript, DOM storage, and opens _blank links in the system browser
  • Host page is served over HTTPS from a domain verified for Apple Pay
  • Prefill parameters are URL-encoded (note the + in phone numbers → %2B)
  • PENDING bookings are shown as awaiting confirmation, not confirmed
  • Cancellation shows the refund policy before the guest confirms
  • Tested on a real device with 3-D Secure test cards, not only in the simulator

See the API reference for the full request and response shapes of every endpoint used above.