Hubby API (2.1.0)

Welcome to the Hubby API documentation. This API enables partners to seamlessly integrate eSIM booking and management capabilities into their applications.

Key Features:

  • Create and manage eSIM bookings for your customers
  • Access our global package catalog with country-specific offerings
  • Track booking statuses and package activations
  • Support for multiple package types (starter, data-limited, unlimited)

Package Types

The API supports four types of packages:

  • Starter packages: Hybrid packages that are both data and time limited. They provide a small data allowance within a short time period (default: 2 days). Perfect for first-time users and trials.

  • Data-limited packages: Traditional packages with a specific data allowance that expires after a certain period (default: 365 days). This is the primary package type and the default for most use cases.

  • Unlimited packages: Packages that provide unrestricted data usage for a specified duration. Subject to fair use policy. Ideal for heavy data users and digital nomads.

  • Time-limited packages (deprecated): Packages that provide a fixed data allowance for a specific duration with full-speed access. They expire when either the data limit or time limit is reached. This package type is deprecated and should not be used for new integrations — use a data-limited package with a short package_duration instead.

Note: Top-ups are always data-limited packages, regardless of the original package type.

Authentication: All API requests must include the following headers:

  • x-api-key: Your public API key
  • x-timestamp: Current Unix timestamp in milliseconds
  • x-signature: HMAC-SHA256 signature

The HMAC signature must be generated for each request using:

  1. Concatenate: timestamp + HTTP method + request path + query string (if any) The request path is the full path after the domain and must include the /api prefix. Example: "1678901234GET/api/bookings"
  2. Generate HMAC-SHA256 using your secret key
  3. Convert to hex string

Note: Swagger UI cannot be used to test the API directly as each request requires a unique HMAC signature. Please implement the authentication in your client application.

Example Node.js Implementation:

const cryptoJs = require('crypto-js');

// Configuration values that would normally come from environment
const secretKey = "YOUR_API_SECRET";
const publicKey = "YOUR_API_KEY";

// Generate headers for an API request.
// `url` is the full request URL, e.g. "https://api.hubbyesim.com/api/bookings?perPage=10"
function generateApiHeaders(method, url) {
    // Timestamp is in milliseconds e.g. 1715558400000
    const timestamp = Math.floor(Date.now()).toString();

    const parsed = new URL(url);

    // The signed path is everything after the domain, INCLUDING the /api
    // prefix, plus the query string if present.
    const pathWithQuery = parsed.pathname + parsed.search;

    // Sample payload: 1715558400000GET/api/bookings?perPage=10
    const payload = timestamp + method + pathWithQuery;

    // Generate the HMAC signature
    const signature = cryptoJs.HmacSHA256(payload, secretKey).toString(cryptoJs.enc.Hex);

    return {
        'x-timestamp': timestamp,
        'x-signature': signature,
        'x-api-key': publicKey,
        'Accept': 'application/json'
    };
}

Webhook Delivery Authentication

When Hubby delivers a webhook to your endpoint, the request carries your API key in a header. By default this is x-api-key: <your-key>. If your endpoint expects the key under a different header, the header name is configurable per partner (for example Authorization) with an optional value prefix (for example Bearer , producing Authorization: Bearer <your-key>). The default remains x-api-key with no prefix — contact support@hubbyesim.com to change it.

Signature verification

When a signing secret is configured, every webhook is signed so you can verify it originated from Hubby and was not altered in transit. Each delivery carries these headers:

  • x-hubby-signature — one or more comma-separated sha256=<hex> values. Verify by recomputing HMAC-SHA256(secret, "{timestamp}.{body}"), hex-encoding the result, and accepting the request if it matches any entry in the list. Signatures are lowercase hex. Treat this as a list from day one: today we send a single signature, but the list form lets us add a second signature during a future secret rotation without any change on your side.
  • x-hubby-timestamp — the signing timestamp, in epoch seconds. This is the {timestamp} in the signed string above.

The signed string is the timestamp, a literal ., then the exact raw request body as received. Verify against the raw bytes — do not re-serialize the JSON first, as key reordering or whitespace changes will break the match.

Reject the request if now - x-hubby-timestamp exceeds your tolerance window (we recommend ~5 minutes). This is your protection against replayed requests.

A secret stays valid until it is regenerated; there is no scheduled expiry. Regeneration is a hard cutover — update your stored secret at the same time. You receive separate secrets for staging and production.

Idempotency & event identity

Two identifiers accompany every delivery, both as headers and inside the JSON body (in case reading custom headers is awkward in your stack):

  • x-hubby-event-id / event_id — a stable identifier for the logical event. It stays identical across retries and across any replay of the same event, so deduplicate on it: processing the same event_id more than once should be a no-op on your side.
  • x-hubby-delivery-id / delivery_id — identifies a single delivery. Quote it when contacting support about a specific delivery. A replay of an event carries the same event_id but a new delivery_id.

Delivery, retries & replay

  • A delivery is considered successful when your endpoint returns any 2xx status.
  • If your endpoint returns 5xx or 429, times out, or is unreachable, Hubby retries with exponential backoff over a few hours (up to 12 attempts total).
  • A 4xx (other than 429) is treated as a permanent rejection of the content and is not retried. Return 2xx for "received" and reserve 4xx for genuinely malformed requests.
  • Every delivery is recorded and any past event can be replayed to you on request. A replay carries the same event_id (so your dedup keeps it safe) with a fresh signature and timestamp.
  • Deliveries are independent and ordering is not guaranteed; rely on event_id for deduplication rather than on arrival order.

Need Help?

  • Technical Support: support@hubbyesim.com
Download OpenAPI description
Languages
Servers
Mock server
https://docs.hubbyesim.com/_mock/apis/openapi/
Production server
https://api.hubbyesim.com/api/
Staging server
https://api-staging.hubby.dev/api/

Booking

Operations

Destination

Operations

PromoCode

Operations

eSIM

Operations

WebView

Operations

Create a redirect token

Request

Generates a short-lived redirect token that the partner app uses to open the Hubby WebView. The token is valid for 5 minutes and is single-use.

When to call: Every time the traveler opens the eSIM section in your app. Do not cache the returned token — generate a fresh token on every open, on app resume, and if the 5-minute window expires.

Authentication: HMAC-SHA256 (same as all partner endpoints).

Bodyapplication/jsonrequired
Any of:

Identify the traveler with either external_user_id or email. When both are supplied, external_user_id takes precedence.

external_user_idstringrequired

The partner's own user identifier — your internal ID for this traveler. This is not Hubby's user_id from the booking API. Use this for travelers created via the WebView / universal flow.

Example: "partner_user_456"
emailstring(email)

Alternative to external_user_id for partners without a stable per-user identifier (e.g. classic-grant / email-keyed bookings). Resolves the traveler by email, scoped to your Partner. If the resolved user has no external_user_id yet, Hubby assigns one so subsequent calls remain stable.

Example: "traveller@example.com"
redirect_tokenstring(uuid)

Optional. If you pre-generate the token, pass it here. Otherwise Hubby generates one.

Example: "b3864ab6-c13b-42b5-9f69-935eb9c34aba"
curl -i -X POST \
  https://docs.hubbyesim.com/_mock/apis/openapi/redirect-tokens/create \
  -H 'Content-Type: application/json' \
  -d '{
    "external_user_id": "partner_user_456",
    "email": "traveller@example.com",
    "redirect_token": "b3864ab6-c13b-42b5-9f69-935eb9c34aba"
  }'

Responses

Redirect token created successfully

Bodyapplication/json
successboolean
Example: true
dataobject
Response
application/json
{ "success": true, "data": { "redirect_token": "1b80dfe9-0202-4151-a26f-ceac52ca3b34", "expires_in": 300 } }

Exchange redirect token for session JWT

Request

Exchanges a redirect token for a short-lived session JWT. The JWT is used as a Bearer token for all subsequent WebView API calls.

Called automatically by the Hubby WebView — partners do not call this endpoint directly. Documented for transparency and to aid debugging of WebView network traffic.

Authentication: None (the redirect token itself is the credential).

Bodyapplication/jsonrequired
redirect_tokenstring(uuid)required

The redirect token returned by /redirect-tokens/create.

Example: "1b80dfe9-0202-4151-a26f-ceac52ca3b34"
external_user_idstringrequired

The partner's own user identifier. Must match the external_user_id used when creating the redirect token.

Example: "123"
curl -i -X POST \
  https://docs.hubbyesim.com/_mock/apis/openapi/webapp/auth/exchange \
  -H 'Content-Type: application/json' \
  -d '{
    "redirect_token": "1b80dfe9-0202-4151-a26f-ceac52ca3b34",
    "external_user_id": "123"
  }'

Responses

Token exchanged successfully

Bodyapplication/json
successboolean
Example: true
dataobject
Response
application/json
{ "success": true, "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 1209600 } }

Set marketing/tracking consent

Request

Sets or revokes the authenticated traveler's marketing and/or tracking consent. Either field may be omitted to leave it untouched; at least one must be present.

Setting a field to true records the current time as its _given_at timestamp, but only if it is not already set — re-sending true while already consented is a no-op and preserves the original consent date. Setting a field to false always clears its timestamp to null; a later true then records a fresh consent date.

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

Bodyapplication/jsonrequired
marketing_consentboolean

When true, records marketing_consent_given_at if unset. When false, clears it to null. Omit to leave untouched.

tracking_consentboolean

When true, records tracking_consent_given_at if unset. When false, clears it to null. Omit to leave untouched.

curl -i -X POST \
  https://docs.hubbyesim.com/_mock/apis/openapi/webapp/me/consent \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "marketing_consent": true,
    "tracking_consent": true
  }'

Responses

Consent updated (or already in the requested state)

Bodyapplication/json
successboolean
Example: true
dataobject

The full updated user document (snake_case).

Response
application/json
{ "success": true, "data": {} }

Get user dashboard

Request

Returns the authenticated traveler's dashboard data: active packages, data usage, unclaimed packages, and available actions.

Called automatically by the Hubby WebView — partners do not call this endpoint directly. Documented for transparency and to aid debugging of WebView network traffic.

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

curl -i -X GET \
  https://docs.hubbyesim.com/_mock/apis/openapi/webapp/me/dashboard \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>'

Responses

Dashboard data

Bodyapplication/json
successboolean
Example: true
dataobject

Dashboard payload including active packages, usage, and unclaimed packages. Structure may evolve — the WebView renders this automatically.

Response
application/json
{ "success": true, "data": { "unclaimed_packages": [], "active_packages": [], "expired_packages": [] } }

Get Help Centre context

Request

Returns the traveler's real-time context for the in-app Help Centre: device, eSIM state, packages (webapp single-eSIM model), trip destination, and flags. The help web app maps this to its decision engine to route the traveler to the most likely resolution.

Reads live data (refreshes data_left from the provider). ?destination=<ISO3> overrides the current destination (else the eSIM's).

Called automatically by the Hubby Help WebView — partners do not call this directly.

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

Query
destinationstring

ISO-3166 alpha-3 code of the traveler's current destination (overrides the eSIM's).

curl -i -X GET \
  'https://docs.hubbyesim.com/_mock/apis/openapi/webapp/me/help-context?destination=string' \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>'

Responses

Help context

Bodyapplication/json
successboolean
Example: true
dataobject

HelpContext (snake_case). Every field nullable; the engine degrades per missing field.

Response
application/json
{ "success": true, "data": { "source": "app", "partner_id": "string", "locale": "string", "first_name": "string", "device": {}, "trip": {}, "esim": {}, "packages": [], "booking": {}, "flags": {} } }

Start a Help Centre support chat (Intercom)

Request

Opens an Intercom conversation for the traveler, carrying the help-app context AND server-side support fields the app never sees (partner, promocode, iccid, hubby_user_id — read from the user/eSIM). The agent/Fin skips discovery.

The traveler's help context (from /webapp/me/help-context) plus the stuck-step info is sent as the body; support fields are attached server-side.

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

Requires: INTERCOM_ACCESS_TOKEN configured, and the custom attributes pre-created in Intercom (see helpChat/README.md).

Bodyapplication/json
entry_statestring or null
pathstring or null

The path the traveler was on when they asked for a person — a guided flow (e.g. connect_triage), or one of the screens the Help Centre resolves to instead of a flow (esim_shop, human_handoff)

surfacestring or null
Enum"native""webapp"
variantstring or null
step_stuckstring or null

The step id the traveler got stuck on

steps_shownArray of strings
capturedobject or null

Anything the traveler typed into a form step (e.g. booking_ref, email), appended to the conversation body so the agent skips the screenshot loop

Example: {"booking_ref":"ABC123"}
correctionsobject or null

What the traveler told the Help Centre it had wrong about them, keyed by the fact. The Help Centre asserts four facts at the top of every screen (phone, where they are, departure, eSIM state); three of them are correctable, and all three are asked at once in a single sheet. The correction is scoped to that Help session and nothing is written back to the account, so the conversation is where it becomes durable.

Sent as a disagreement, never as a fact — the context attached server-side (phone_os, device_model) still reports what the platform holds. "Traveler says Samsung, our record says iPhone" is worth more to an agent than either alone. Omit the field entirely when the traveler corrected nothing; an empty object reads as "they were asked and said nothing".

Keys: device (iphone | samsung | pixel | xiaomi | other), location (not_there_yet | in_destination | trip_over), esim (not_installed | not_connected | working). Reaches Intercom as the traveller_says conversation attribute.

device names a phone brand rather than a platform because the brand is what decides which instructions and artwork exist — the Samsung, Pixel and Xiaomi Settings paths differ from each other and from Apple's. other means generic Android.

Example: {"device":"samsung"}
curl -i -X POST \
  https://docs.hubbyesim.com/_mock/apis/openapi/webapp/me/help/escalate \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "entry_state": "string",
    "path": "string",
    "surface": "native",
    "variant": "string",
    "step_stuck": "string",
    "steps_shown": [
      "string"
    ],
    "captured": {
      "booking_ref": "ABC123"
    },
    "corrections": {
      "device": "samsung"
    }
  }'

Responses

Conversation created

Bodyapplication/json
successboolean
Example: true
dataobject
Response
application/json
{ "success": true, "data": { "conversation_id": "string", "contact_id": "string" } }

Refresh a user's universal eSIM

Request

Assigns a fresh universal eSIM to a user identified by external_user_id. If the user already has a universal eSIM, it is replaced with a new one. If the user does not yet have one, a new eSIM is provisioned and assigned.

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

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

Bodyapplication/jsonrequired
external_user_idstringrequired

The partner's own user identifier. Must match the external_user_id used during authentication.

Example: "partner_user_456"
curl -i -X POST \
  https://docs.hubbyesim.com/_mock/apis/openapi/webapp/refresh-esim \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "external_user_id": "partner_user_456"
  }'

Responses

Universal eSIM assigned successfully

Bodyapplication/json
successboolean
Example: true
dataobject
Response
application/json
{ "success": true, "data": { "iccid": "8901234567890123456", "qr": "LPA:1$smdp.example.com$ACTIVATION-CODE", "status": "RELEASED" } }

Native eSIM Integration

Operations

Webhooks

Webhooks