Last updated

Hubby WebView

Hubby provides a fully managed web app that runs inside your mobile app's in-app browser. Your travelers claim packages, install their eSIM, monitor data usage, and buy top-ups — all within a branded experience you control. Your backend makes two API calls. Hubby handles everything else.

This page covers the complete flow: what you build, what Hubby handles, every API call in sequence, how authentication works, and what to do after you go live.


What You Build vs. What Hubby Handles

Your teamHubby
Backend calls to create bookings and redirect tokensPackage provisioning, eSIM assignment, region optimization
WebView container in your app (open a URL)Branded UI: claiming flow, installation instructions, data meter, top-up store
Pass device info as query parameters when opening the WebViewDevice-tailored installation guides, localized UI
Store external_user_id per travelerSession management, token exchange, JWT lifecycle
Map webhook events to push notifications (Phase 2)Webhook event delivery, departure-time CRM triggers

Effort estimate: Backend: 3–5 days. Mobile: 2–3 days. No frontend build required — your app opens a URL.


Architecture

Three actors are involved: your backend, the Hubby API, and the Hubby WebView running inside your app.

Hubby WebView(in Partner App)Hubby APIPartner BackendHubby WebView(in Partner App)Hubby APIPartner BackendPOST /api/bookingsbooking confirmation + package IDsPOST /api/redirect-tokens/createredirect_token (valid 5 min)Open WebView with redirect_tokenPOST /api/webapp/auth/exchangesession JWTGET /api/webapp/me/dashboarduser dashboard dataUser claims packages,installs eSIM, monitors data

The Full Flow

Step 1 — Create a Booking

POST /api/bookings — your backend calls this when a traveler qualifies for a package (e.g., at flight booking time, loyalty reward, or campaign trigger). The Booking entity maps directly to the travel booking you already have — a departure date, one or more destinations, and the bundles (data packages) for each.

Request Fields

FieldTypeRequiredDescription
departure_datestringYesISO 8601 datetime with timezone (e.g., 2026-07-15T14:30:00+02:00). Including the time and timezone enables precise CRM triggers such as push notifications 2 hours before departure
localestringNoWebView language (es, fr, de, etc.). See supported locales
custom_brandingstringNoSelects your branded theme. Partners with multiple sub-brands pass the brand key here
package_specificationsarrayYesOne or more packages to provision

Package Specification Fields

FieldTypeRequiredDescription
external_user_idstringYesRequired for WebView integration. Your own identifier for the traveler receiving this package. This is the key that links the booking to the traveler across all subsequent calls — redirect tokens, WebView auth exchange, and sessions. Use your own internal user ID and pass the same value consistently.
destinationstringYesISO 3166-1 alpha-2 country code (GR, US, JP)
sizestringYesData package size (1GB, 3GB, 5GB). Required when package_type is data-limited or starter.
package_typestringNoOne of data-limited, starter, unlimited. (time-limited is still accepted but deprecated — use data-limited with a package_duration instead.) Defaults to data-limited when destination + size are provided, and to starter (with size=1GB, package_duration=2) when only destination is provided.
package_durationnumberConditionalDays the package is valid for. Required for starter and unlimited packages. Defaults: 365 for data-limited, 2 for starter.
traffic_policystringConditionalRequired for unlimited packages. Hubby sets a sensible default if omitted.

Important: external_user_id is required for the WebView flow. It must be your own stable user identifier, and the same value must be used across all WebView endpoints — bookings, redirect tokens, and the auth exchange. If you are not using the WebView or Native eSIM Integration, do not supply this field — it is ignored in the traditional booking flow and supplying it incorrectly will route the package into the eSIM management flow.

Multi-Destination Bookings

A single booking can contain multiple entries in package_specifications. Hubby automatically optimizes the assignment — for example, a trip covering Greenland, Canada, the US, and Mexico may be fulfilled with a single North America regional package plus individual packages where more cost-effective.

Example

curl --request POST \
  'https://api.hubbyesim.com/api/bookings' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'x-timestamp: 1717689600000' \
  --header 'x-signature: YOUR_HMAC_SIGNATURE' \
  --header 'Content-Type: application/json' \
  --data '{
    "departure_date": "2026-07-15T14:30:00+02:00",
    "locale": "es",
    "custom_branding": "your-brand",
    "package_specifications": [
      {
        "external_user_id": "partner_user_456",
        "destination": "GR",
        "size": "1GB"
      }
    ]
  }'
{
  "success": true,
  "data": {
    "id": "booking_abc",
    "departure_date": "2026-07-15T14:30:00+02:00",
    "locale": "es",
    "partner": "your-partner-id",
    "external_user_id": "partner_user_456",
    "package_queues": [
      {
        "uuid": "b29bd4d7-f058-497d-bc7b-ada1c4fad0dd",
        "destination": "Macedonia",
        "iso3": "MKD"
      }
    ]
  }
}

The WebView booking response surfaces an array of package_queuesnot package_specifications. package_specifications is the input shape on the request body only; on the response it is replaced by the queue projection. Each entry carries:

  • uuid — stable handle you can correlate with subsequent webhook events
  • destination — human-readable destination name (e.g., "Macedonia")
  • iso3 — ISO 3166-1 alpha-3 country code (e.g., "MKD")

Persist the uuid on your side if you need to map a package back to your internal booking — it is what the WebView and webhooks reference. The traditional booking flow returns promo_codes instead; WebView bookings (any booking created with external_user_id in the package_specifications) return package_queues and no promo_codes.

Note: external_user_id is required for the WebView flow. Always supply your own consistent user identifier — it is the key that ties the booking, redirect token, and WebView session together.


Step 2 — Create a Redirect Token

POST /api/redirect-tokens/create — your backend calls this right before the traveler opens the eSIM section in your app. The response contains a short-lived redirect_token that your app uses to open the Hubby WebView.

Request Fields

Supply either external_user_id or email to identify the traveler — at least one is required. When both are sent, external_user_id takes precedence.

FieldTypeRequiredDescription
external_user_idstringConditionalYour own user identifier — the same value you used in the booking's package_specifications. Preferred for the WebView/universal flow.
emailstringConditionalAlternative lookup for partners without a stable per-user identifier (e.g. travelers created via the email-keyed classic-grant flow). Resolves the traveler by email, scoped to your partner. If that user has no external_user_id yet, Hubby assigns one and the redirect token (and resulting session) is keyed on it — so the rest of the WebView flow is unchanged.

Whichever you send, the resulting redirect_token always carries the traveler's external_user_id; the token exchange and WebView session remain external_user_id-based.

Hubby generates the redirect_token server-side using crypto.randomUUID(). You cannot pre-generate or supply your own token.

Token Lifetime

The redirect token is valid for 5 minutes and is single-use — once consumed by the WebView token exchange it cannot be reused. This prevents URL forwarding and sharing. If the token expires or has already been consumed before the traveler opens the WebView, request a fresh one from the same endpoint.

Important: Do NOT cache the redirect_token. Generate a new token every time the traveler opens the eSIM section in your app — on every open, on app resume if the WebView was killed by the OS, and if the 5-minute window expires.

Error Responses

StatusCause
400Neither external_user_id nor email was supplied, or the partner could not be resolved from your API credentials
404No user exists for the supplied external_user_id (or email) + partner combination. The user is created when the booking is processed — if you call this endpoint before a booking has been provisioned for that traveler, you will get a 404. Create the booking first, then mint the redirect token.

Example

curl --request POST \
  'https://api.hubbyesim.com/api/redirect-tokens/create' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'x-timestamp: 1717689600000' \
  --header 'x-signature: YOUR_HMAC_SIGNATURE' \
  --header 'Content-Type: application/json' \
  --data '{
    "external_user_id": "partner_user_456"
  }'

Or identify the traveler by email instead:

curl --request POST \
  'https://api.hubbyesim.com/api/redirect-tokens/create' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'x-timestamp: 1717689600000' \
  --header 'x-signature: YOUR_HMAC_SIGNATURE' \
  --header 'Content-Type: application/json' \
  --data '{
    "email": "traveller@example.com"
  }'

Both return the same response shape:

{
  "success": true,
  "data": {
    "redirect_token": "1b80dfe9-0202-4151-a26f-ceac52ca3b34",
    "expires_in": 300
  }
}

Step 3 — Open the WebView

Your app opens the Hubby WebView in an in-app browser using the redirect_token returned in Step 2. Construct the URL as https://app.hubbyesim.com/?t={redirect_token} and append device information as additional query parameters so the WebView can tailor eSIM installation instructions to the traveler's exact device.

Device Query Parameters

ParameterRequiredDescription
phone_osRecommendedOperating system (iOS or Android)
phone_modelRecommendedDevice model (iPhone 15 Pro, Samsung Galaxy S24)
os_versionRequiredOS version (17.4, 14). The WebView uses this to decide between QR-code, Universal Link, and direct-provisioning installation flows — installation guidance is wrong without it
phone_brandRecommendedDevice manufacturer (Apple, Samsung, Google)
localeRequiredBCP 47 language-region code (en-US, es-ES, nl-NL, de-DE, fr-FR, pt-BR, …) for the WebView UI. Passing this on every open ensures the traveler always sees the WebView in the language of their current app session, even if their booking was created with a different locale. Use the exact regional code — fr-CA for Canadian French, not just fr. See supported locales for the full list
esim_supportedConditionaltrue or false — whether the traveler's device supports eSIM. Required when your app surfaces the eSIM option regardless of device compatibility, so the WebView can show an unsupported-device screen instead of an installation flow that the device cannot complete. Omit only if your app already hides the eSIM entry point for incompatible devices. See Detecting eSIM Compatibility below

The WebView also performs browser-based device detection as a fallback, but values passed from the native app are always more accurate — especially for device model, brand, and eSIM support, which browsers cannot reliably detect.

Example

https://app.hubbyesim.com/?t=1b80dfe9-0202-4151-a26f-ceac52ca3b34&phone_os=iOS&phone_model=iPhone%2015%20Pro&os_version=17.4&phone_brand=Apple&locale=es-ES&esim_supported=true

Tip: URL-encode parameter values. These parameters are read client-side by the WebView — they are never sent to the Hubby backend.

Detecting eSIM Compatibility

If your app shows the eSIM offering to every traveler — regardless of whether their device can install one — you must pass esim_supported so the WebView can render the right screen. Detect eSIM support natively, before opening the WebView, and forward the result as the query parameter.

Use CTCellularPlanProvisioning from the CoreTelephony framework. supportsCellularPlan() returns true only on devices with an active eSIM-capable modem and a region that permits eSIM provisioning.

import CoreTelephony

func isESIMSupported() -> Bool {
    return CTCellularPlanProvisioning().supportsCellularPlan()
}

// When opening the WebView:
let esimSupported = isESIMSupported()
let url = "https://app.hubbyesim.com?t=\(token)"
    + "&phone_os=iOS"
    + "&os_version=\(UIDevice.current.systemVersion)"
    + "&locale=\(Locale.current.identifier.replacingOccurrences(of: "_", with: "-"))" // e.g. "es-ES"
    + "&esim_supported=\(esimSupported)"

Available since iOS 12. Returns false on iPhone XS / XR and older, on iPhones sold in mainland China without eSIM hardware, and on devices where eSIM has been disabled by carrier policy.

Note: These checks confirm that the device can install an eSIM. They do not guarantee a carrier will permit activation, nor do they check available eSIM slots. The WebView still handles the post-install flow — esim_supported only controls which entry-point screen the traveler sees.

What the Traveler Sees

The WebView adapts to the traveler's state:

Traveler has unclaimed packages → A congratulations screen appears ("You got 1GB for Greece!"). The traveler can:

  • Accept — starts the eSIM installation flow, tailored to their exact device and OS version
  • Skip — moves to the next unclaimed package

After all packages are claimed (or skipped), the traveler sees the data meter.

No unclaimed packages → The data meter screen shows:

  • Active package status and remaining data
  • Option to buy more data
  • If all packages have expired, a "Get a package" prompt

Session Behaviour

  • Closing the WebView ends the session. The next time the traveler opens it, your app should generate a new redirect token (Step 2) and re-open.
  • Auth token persists across navigations within the same WebView session — no re-authentication while the browser instance is alive.
  • If the OS kills the WebView in the background, generate a new redirect token on app resume.

eSIM Installation URL Handling

When the traveler taps "Install eSIM" inside the WebView, the web app navigates to a system-level provisioning URL. These look like normal https:// links but are handled by the OS, not by a browser. A WebView will silently fail to load them unless your native app intercepts and hands them off to the OS.

URLs the web app navigates to:

PlatformURL
Androidhttps://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA:1$<smdp-address>$<matching-id>
iOShttps://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$<smdp-address>$<matching-id>

The carddata query parameter contains the GSMA LPA activation code in the format LPA:1$<SM-DP+ address>$<matching ID>.

Why this is needed: These URLs are not real websites. On Android, esimsetup.android.com is an Android App Link registered by the system eSIM provisioning service — it only works via startActivity with ACTION_VIEW. On iOS, esimsetup.apple.com is handled by the iOS Cellular Setup service — it only works via UIApplication.open(_:). If your WebView does not intercept these URLs, the navigation fails silently and the user sees nothing happen.

Your app must intercept these URL patterns and launch them externally:

URL PatternPlatformAction
https://esimsetup.android.com/*AndroidLaunch with OS via Intent / ACTION_VIEW
https://esimsetup.apple.com/*iOSLaunch with OS via UIApplication.open
LPA:1$...BothLaunch externally (GSMA standard scheme)

Override shouldOverrideUrlLoading in your WebViewClient:

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView,
        request: WebResourceRequest
    ): Boolean {
        val url = request.url.toString()

        // eSIM provisioning URL — launch with the OS
        if (url.startsWith("https://esimsetup.android.com/")) {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            return true
        }

        // Direct LPA activation code
        if (url.startsWith("LPA:")) {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            return true
        }

        return false
    }
}

Testing: After implementing, create a booking and open the WebView. Tap "Install eSIM" — the device's native eSIM provisioning dialog should appear. If nothing happens, check your debug logs for the intercepted URL.

iOS: Declare Hubby as an App-Bound Domain

Starting with iOS 14, Apple disables service workers and several modern web platform APIs inside WKWebView unless the host app explicitly opts in by declaring the domain as app-bound. The Hubby WebView relies on a service worker to cache installation guides, the data meter UI, and static assets so the experience survives the moment a traveler loses connectivity at the gate or on landing. Without this configuration the WebView still loads, but the service worker never registers — travelers without signal see a blank screen instead of their cached eSIM instructions or remaining data.

To unlock service worker support and offline caching, your iOS app must:

  1. Declare app.hubbyesim.com in WKAppBoundDomains in Info.plist
  2. Set limitsNavigationsToAppBoundDomains = true on the WKWebViewConfiguration used to create the WebView

Info.plist:

<key>WKAppBoundDomains</key>
<array>
    <string>app.hubbyesim.com</string>
</array>

WKWebView setup (Swift):

import WebKit

let config = WKWebViewConfiguration()
config.limitsNavigationsToAppBoundDomains = true

let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: hubbyURL))

Constraints to be aware of:

  • WKAppBoundDomains accepts a maximum of 10 entries per app. Choose carefully — Apple does not allow this list to be extended at runtime.
  • Both pieces are required. Declaring the domain without setting limitsNavigationsToAppBoundDomains = true on the configuration does not enable service workers.
  • If your app has other WKWebView instances that need to navigate to non-listed domains (e.g., generic marketing or support pages), keep them in a separate WKWebViewConfiguration with limitsNavigationsToAppBoundDomains left at its default of false. The opt-in is per-WebView, not app-wide.
  • Android WebView supports service workers natively. This configuration is iOS-only.

Why this matters: travelers most need their installation guide and data meter precisely when they have no connectivity — boarding a plane, landing in a new country, or troubleshooting a failed activation. Skipping this configuration ships a WebView that works in the office but breaks in the field.


Step 4 — Token Exchange (automatic)

Partners do not call this endpoint. It is documented here for transparency and to help debug WebView network traffic.

When the WebView loads with the redirect_token, it automatically calls:

POST /api/webapp/auth/exchange

{
  "redirect_token": "1b80dfe9-0202-4151-a26f-ceac52ca3b34"
}

The only field in the request body is redirect_token. The external_user_id and partner_id are read out of the validated token server-side, not from the request body.

The response carries the session token:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_in": 1209600
  }
}

The token field is the session JWT. It is valid for 14 days (expires_in is in seconds) and is scoped to /webapp/ endpoints only — it cannot be used for partner backend calls and your API keys are never exposed to the WebView. The WebView can refresh the token within its last 24 hours of validity via POST /api/webapp/auth/refresh.


Step 5 — Dashboard Load (automatic)

Partners do not call this endpoint. Documented for transparency.

The WebView uses the session JWT to load the traveler's dashboard:

GET /api/webapp/me/dashboard

curl --request GET \
  'https://api.hubbyesim.com/api/webapp/me/dashboard' \
  --header 'Authorization: Bearer <session_jwt>' \
  --header 'Content-Type: application/json'

The response includes active packages, data usage, unclaimed packages, and available actions. The WebView renders this into the branded UI.


How Authentication Works

The WebView flow uses two separate auth contexts. They never mix.

Your Backend → Hubby API (HMAC-SHA256)

All calls from your backend use HMAC-SHA256 signing. Every request includes:

  • x-api-key — your public API key
  • x-timestamp — current Unix timestamp in milliseconds
  • x-signature — HMAC-SHA256 of {timestamp}{METHOD}{path}

This covers POST /api/bookings, POST /api/redirect-tokens/create, and all other partner-facing endpoints. See the authentication guide for implementation details.

WebView → Hubby API (JWT)

The WebView authenticates with a short-lived JWT obtained through the token exchange (Step 4). This is completely separate from your HMAC credentials — the WebView never has access to your API keys.

Hubby WebViewHubby APIPartner BackendHubby WebViewHubby APIPartner BackendHMAC-SHA256JWTPOST /api/redirect-tokens/createredirect_tokenPOST /api/webapp/auth/exchangesession JWTGET /api/webapp/me/dashboarddashboard data

Security Properties

  • Time-limited tokens — the redirect_token expires in 5 minutes, preventing forwarding/sharing
  • No PII in the URL — the WebView URL contains only the opaque redirect_token and optional device metadata
  • Client-side device info — device parameters are read by the WebView client-side and never sent to the Hubby backend
  • Scoped JWT — valid only for /webapp/ endpoints, tied to the external_user_id

Identifier Reference

A single identifier — external_user_id — ties everything together across the WebView flow.

EndpointHow external_user_id is used
POST /api/bookingspackage_specifications[]Links each package to a specific traveler
POST /api/redirect-tokens/createTies the redirect token (and resulting WebView session) to the traveler. Also accepts email as an alternative lookup (resolving to the same external_user_id). Returns 404 if no user yet exists for the identifier under your partner
POST /api/webapp/auth/exchangeReads external_user_id out of the validated redirect_token and embeds it in the session JWT. Not accepted from the request body

Use your own internal user ID as the external_user_id and pass the same value consistently. It is the through-line of the WebView flow; the booking endpoint requires it (the classic-grant flow accepts email instead), and the redirect-token endpoint accepts either external_user_id or email.


Refresh Universal eSIM

POST /api/webapp/refresh-esim — assigns a fresh universal eSIM to a user. If the user already has a universal eSIM, it is replaced. If they don't have one yet, a new eSIM is provisioned and assigned.

Use this to pre-provision an eSIM before the traveler opens the WebView, or to replace a problematic eSIM with a fresh one.

Authentication: Bearer JWT from /webapp/auth/exchange.

Request Fields

FieldTypeRequiredDescription
external_user_idstringYesYour own user identifier — the same value used in bookings and redirect tokens

Example

curl --request POST \
  'https://api.hubbyesim.com/api/webapp/refresh-esim' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIs...' \
  --header 'Content-Type: application/json' \
  --data '{
    "external_user_id": "partner_user_456"
  }'
{
  "success": true,
  "data": {
    "iccid": "8901234567890123456",
    "qr": "LPA:1$smdp.example.com$ACTIVATION-CODE",
    "status": "RELEASED"
  }
}

Note: This endpoint is idempotent in intent — calling it always results in a fresh eSIM assigned to the user, whether or not they had one before. The previous eSIM (if any) is replaced.


What Comes Next

The WebView gets you live fast. Once it's running, you can progressively enhance the experience:

Communication & Conversion (Weeks 2–4)

Use webhooks to receive real-time events and map them to push notifications:

  • booking.within_cutoff — departure is 7 days away, send an installation reminder
  • booking.about_to_depart — 2 hours before flight (requires time + timezone in departure_date), last-chance install prompt
  • esim.installed / esim.removed — track installation state
  • package.activated — traveler connected at destination
  • package.usage.50_percent / package.usage.80_percent / package.usage.100_percent — per-package usage milestones (data consumed or duration elapsed), drive top-up revenue at 80%

Branding & Segmentation

Use custom_branding to run separate branded themes per sub-brand. Different customer segments can receive different package sizes (e.g., premium members get 3GB, standard members get 1GB) by varying size in package_specifications.

Zero-Rated Traffic

Ensure your core app functionality (boarding passes, booking management, payments) remains accessible to travelers even when their data package is depleted. Provide your critical IP ranges to Hubby and we configure zero-rated traffic policies.

Native API (Optional)

For partners who want to go beyond the WebView, Hubby offers a Native API covering packages, provisioning, installation instructions, usage reporting, and event streams. The WebView and Native API coexist — many partners start with the WebView and selectively move high-traffic features (data meter, top-up) to native over time.


Troubleshooting

"Token expired" error in WebView

The redirect token was not opened within 5 minutes. Generate a new token and re-open.

"Invalid external_user_id" error

The external_user_id in the token exchange does not match the one used when creating the redirect token. Ensure your app passes the same identifier consistently.

Nothing happens when traveler taps "Install eSIM"

Your WebView is loading the provisioning URL (esimsetup.android.com or esimsetup.apple.com) as a webpage instead of launching it externally. Implement the URL interception described in eSIM Installation URL Handling.

WebView shows a blank screen

Check your network inspector for a failed /webapp/auth/exchange call. Common causes:

  • Redirect token already consumed (tokens are single-use)
  • Network timeout during the exchange
  • WebView security settings blocking the request (ensure JavaScript is enabled and cross-origin requests are allowed for app.hubbyesim.com)

Quick-Start Checklist

To go live, your team provides:

  • User identifier format (your internal user ID that becomes external_user_id)
  • Brand assets per sub-brand (logos, colors, copy)
  • Required locales for the WebView
  • Customer segment → package size mapping
  • Technical point of contact

Hubby provides:

  • API keys (staging + production), per sub-brand if needed
  • Branded WebView configured per sub-brand
  • Staging environment with all endpoints
  • Integration support