Last updated

Tiered Branding

This guide demonstrates how to integrate Hubby eSIM API to create bookings that return promo codes tied to starter packages. This guide also covers data-limited packages and tracking paid upgrades.

Overview

In this integration:

  1. You create a booking with a starter package and a specific brand.
  2. The system generates a promo code
  3. We use our optimized email flow to deliver the promo code(s) alongside installation instructions to your customer, i.e. the end user.
  4. The end user redeems the code in the Hubby app, is presented the correct branding and can install an esim and thus connect to the internet for the duration of the selected package.

Prerequisites

  • A Hubby Partner account with API access
  • Your API credentials (API key and secret key)
  • Ability to make HTTP POST requests from your system

API Endpoints

Hubby provides two API endpoints for different environments:

Production Environment

  • Base URL: https://api.hubbyesim.com
  • Use for: Live applications and production integrations
  • Full endpoint: https://api.hubbyesim.com/api/bookings

Staging Environment

  • Base URL: https://api-staging.hubby.dev
  • Use for: Testing, development, and integration testing
  • Full endpoint: https://api-staging.hubby.dev/api/bookings

When to Use Each Environment

Use Staging (api-staging.hubby.dev) when:

  • Developing and testing your integration
  • Verifying authentication and request formats
  • Testing booking creation workflows
  • Debugging integration issues

Use Production (api.hubbyesim.com) when:

  • Your integration is complete and tested
  • You're ready to create real bookings for customers
  • You're running in a live/production environment

Important Notes:

  • Both environments use the same authentication method
  • Each environment has its own API credentials (staging and production keys are separate)
  • Data created in staging is separate from production
  • Always test thoroughly in staging before using production

Authentication

Hubby API uses HMAC-SHA256 authentication. Every request must include three headers:

Required Headers

  1. x-api-key: Your public API key
  2. x-timestamp: Current timestamp in milliseconds (Unix epoch time)
  3. x-signature: HMAC-SHA256 signature of the request

How Authentication Works

The signature is computed by creating an HMAC-SHA256 hash of a message string using your secret key. For every API route /api is fixed therefor POST https://api.hubbyesim.com/api/bookings results in this message.

{timestamp}{HTTP_METHOD}{path}

Which in turn looks like 1704067200000POST/api/bookings

Authentication Implementation

Here's how to generate the authentication headers:

const crypto = require('crypto');

function generateAuthHeaders(method, path, apiKey, apiSecret) {
  // Get current timestamp in milliseconds
  const timestamp = Date.now().toString();
  
  // Create the message to sign e.g. 1704067200000POST/api/bookings
  // Note: path must include /api (e.g., '/api/bookings')
  const message = `${timestamp}${method}${path}`;
  
  // Generate HMAC-SHA256 signature
  const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(message)
    .digest('hex');
  
  // Return headers
  return {
    'x-api-key': apiKey,
    'x-timestamp': timestamp,
    'x-signature': signature,
    'Content-Type': 'application/json'
  };
}

Important Authentication Notes

  • Timestamp must be current: The timestamp must be within 5 minutes of the server time
  • Use exact path: The path in the signature must include /api (e.g., /api/bookings). The route /bookings does not exist on its own
  • Method must be uppercase: Use GET, POST, PUT, DELETE (uppercase)
  • Keep your secret secure: Never expose your API secret key in client-side code or public repositories

Creating a Booking

Endpoint

Production:

POST https://api.hubbyesim.com/api/bookings

Staging (for testing):

POST https://api-staging.hubby.dev/api/bookings

Request Body

For a starter package booking, you need:

{
  "booking_id": "YOUR-BOOKING-123",
  "departure_date": "2025-06-15T00:00:00.000Z",
  "return_date": "2025-06-22T00:00:00.000Z",
  "departure_location": "AMS", 
  "first_name": "John",
  "custom_branding": "EBSilver", 
  "package_specifications": [
    {
      "package_type": "starter",
      "destination": "Cabo Verde"
    }
  ]
}

Field Descriptions

  • booking_id (required if email is not provided): Any unique reference you want to store with us for this booking. This is not the customer-facing booking ID, but rather an internal identifier you can use to correlate bookings in your system with Hubby records. Either booking_id or email must be provided
  • departure_date (required): Travel start date in ISO 8601 format (e.g., "2025-06-15T00:00:00.000Z")
  • return_date (optional): Travel end date in ISO 8601 format (e.g., "2025-06-22T00:00:00.000Z")
  • departure_location (optional): The customer's departure airport code (IATA 3-letter code, e.g., "AMS", "JFK", "LHR"). Useful for analytics and localized communications
  • email (required if booking_id is not provided): Customer email address. If provided, Hubby sends eSIM installation instructions directly to the customer. Either email or booking_id must be provided; both can be supplied
  • first_name (optional): Customer first name
  • custom_branding (optional): Your brand identifier that includes the membership tier level. The tier is incorporated into the brand name (e.g., "EBSilver", "EBGold"). This allows you to track bookings per brand/tier combination and customize the customer experience accordingly
  • package_specifications (required): Array with at least one package specification. Each entry is resolved by package_id (direct reference) or by other properties (destination, size, etc.); when package_id is set, other fields in that entry are ignored.
  • package_type (required): The type of package to create. Use "starter" for starter packages or "data-limited" for packages with a specific data allowance
  • destination (optional): The eSIM destination. This field is free-form—you can use country names, ISO codes, or common variations (e.g., "Cabo Verde", "CV", "Cape Verde"). Hubby resolves the destination automatically
  • size (optional, non-starter packages only): The data allowance for the package. Available sizes: "1GB", "3GB", "5GB", "10GB", "20GB". Use this property to increase the data allowance of a promo code for customers who purchase additional data in your sales funnel. Only applicable to data-limited package types
  • paid_price (optional, non-starter packages only): The price in EUR cents that the end user has paid to upgrade from a starter package to a fully working data-limited bundle. This field is used for analytics and reporting purposes, allowing you to track revenue generated from package upgrades. Only applicable to data-limited package types, not starter packages

Package Types

Hubby supports two main package types, each designed for different use cases in your sales funnel:

Starter Packages

Starter packages are lightweight, entry-level eSIM packages typically offered free of charge to customers. A starter package includes 1GB of data valid for 2 days, giving customers enough connectivity to get started and experience the eSIM. They provide:

  • 1GB data allowance: Enough to get the customer connected and experience the service
  • 2-day validity: Short validity period encourages customers to upgrade for extended trips
  • Easy onboarding: Perfect for introducing customers to eSIM technology without friction
  • Upgrade path: Customers can later purchase additional data through your sales funnel

Starter packages are ideal for:

  • Travel booking confirmations where you want to offer a complimentary eSIM
  • Lead generation and customer acquisition
  • Providing a "try before you buy" experience

Data-Limited Packages

Data-limited packages are fully functional eSIM packages with a specific data allowance. These are typically sold to customers who want a complete connectivity solution:

  • Defined data allowance: Specify the exact amount of data ("1GB", "3GB", "5GB", "10GB", or "20GB")
  • Revenue generating: Track the price paid using the paid_price field
  • Premium offering: Suitable for customers who need reliable connectivity during their travels

Data-limited packages are ideal for:

  • Upselling customers who started with a starter package
  • Direct sales of eSIM data bundles
  • Premium travel packages that include connectivity

Multiple Packages in One Booking

You can include multiple packages in a single booking by adding multiple objects to the package_specifications array. This is useful for group bookings or when a customer is traveling to multiple destinations.

{
  "booking_id": "FAMILY-TRIP-001",
  "departure_date": "2025-07-01T00:00:00.000Z",
  "email": "family@example.com",
  "first_name": "Smith",
  "custom_branding": "EBGold",
  "package_specifications": [
    {
      "package_type": "data-limited",
      "destination": "France",
      "size": "5GB",
      "paid_price": 1999
    },
    {
      "package_type": "data-limited",
      "destination": "France",
      "size": "5GB",
      "paid_price": 1999
    },
    {
      "package_type": "starter",
      "destination": "France"
    }
  ]
}

This example creates three promo codes: two 5GB data-limited packages and one starter package, all for France. Each package specification results in a separate promo code in the response.

Brand with Membership Tiers

The membership tier is incorporated into the custom_branding field rather than being a separate property. This allows for flexible tier-based branding and customization.

Example brand values with tiers:

Brand ValueDescription
"EBNonmember"Non-member tier
"EBBasic"Basic tier for entry-level members
"EBSilver"Mid-level tier for regular customers
"EBGold"Premium tier for loyal customers
"EBPandion"Top-tier for VIP customers

These tier-aware brands can be used for:

  • Tracking customer loyalty segments in your analytics
  • Applying tier-specific promotional benefits
  • Customizing claim page branding per tier

Complete Example

Here's a complete example using Node.js:

const crypto = require('crypto');
const https = require('https');

// Your API credentials (store these securely!)
const API_KEY = 'your-api-key-here';
const API_SECRET = 'your-api-secret-here';
// Use staging for development, production for live
const API_BASE_URL = process.env.NODE_ENV === 'production' 
  ? 'https://api.hubbyesim.com'
  : 'https://api-staging.hubby.dev';

function generateAuthHeaders(method, path, apiKey, apiSecret) {
  const timestamp = Date.now().toString();
  const message = `${timestamp}${method}/api${path}`;
  const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(message)
    .digest('hex');
  
  return {
    'x-api-key': apiKey,
    'x-timestamp': timestamp,
    'x-signature': signature,
    'Content-Type': 'application/json'
  };
}

async function createBooking(bookingData) {
  const path = '/bookings';
  const url = `${API_BASE_URL}/api${path}`;
  
  const headers = generateAuthHeaders('POST', path, API_KEY, API_SECRET);
  
  const requestBody = {
    booking_id: bookingData.bookingId,
    departure_date: bookingData.departureDate, // ISO 8601 format
    return_date: bookingData.returnDate,       // ISO 8601 format
    departure_location: bookingData.departureLocation,
    email: bookingData.email,
    first_name: bookingData.firstName,
    custom_branding: bookingData.customBranding, // - includes tier, e.g., 'EBSilver'
    package_specifications: [
      {
        package_type: 'starter',
        destination: bookingData.destination
      }
    ]
  };
  
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      port: urlObj.port || 443,
      path: urlObj.pathname,
      method: 'POST',
      headers: headers
    };
    
    const req = https.request(options, (res) => {
      let data = '';
      
      res.on('data', (chunk) => {
        data += chunk;
      });
      
      res.on('end', () => {
        try {
          const response = JSON.parse(data);
          if (res.statusCode === 200 || res.statusCode === 201) {
            resolve(response);
          } else {
            reject(new Error(`API Error: ${res.statusCode} - ${data}`));
          }
        } catch (e) {
          reject(new Error(`Failed to parse response: ${e.message}`));
        }
      });
    });
    
    req.on('error', (error) => {
      reject(error);
    });
    
    req.write(JSON.stringify(requestBody));
    req.end();
  });
}

// Example usage
async function main() {
  try {
    const result = await createBooking({
      bookingId: 'BOOKING-12345',
      departureDate: '2025-06-15T00:00:00.000Z',
      returnDate: '2025-06-22T00:00:00.000Z',
      departureLocation: 'AMS',
      email: 'customer@example.com',
      firstName: 'John',
      customBranding: 'EBSilver',
      destination: 'Cabo Verde'
    });
    
    console.log('Booking created successfully!');
    console.log('Booking ID:', result.data.id);
    console.log('Promo Code:', result.data.promo_codes[0].promo_code);
  } catch (error) {
    console.error('Error creating booking:', error.message);
  }
}

main();

Using Fetch (Modern JavaScript)

const crypto = require('crypto');

const API_KEY = 'your-api-key-here';
const API_SECRET = 'your-api-secret-here';
// Use staging for development, production for live
const API_BASE_URL = process.env.NODE_ENV === 'production' 
  ? 'https://api.hubbyesim.com'
  : 'https://api-staging.hubby.dev';

function generateAuthHeaders(method, path, apiKey, apiSecret) {
  const timestamp = Date.now().toString();
  const message = `${timestamp}${method}/api${path}`;
  const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(message)
    .digest('hex');
  
  return {
    'x-api-key': apiKey,
    'x-timestamp': timestamp,
    'x-signature': signature,
    'Content-Type': 'application/json'
  };
}

async function createBooking(bookingData) {
  const path = '/bookings';
  const url = `${API_BASE_URL}/api${path}`;
  const headers = generateAuthHeaders('POST', path, API_KEY, API_SECRET);
  
  const requestBody = {
    booking_id: bookingData.bookingId,
    departure_date: bookingData.departureDate, // ISO 8601 format
    return_date: bookingData.returnDate,       // ISO 8601 format
    departure_location: bookingData.departureLocation,
    email: bookingData.email,
    first_name: bookingData.firstName,
    custom_branding: bookingData.customBranding,
    package_specifications: bookingData.packageSpecifications
  };
  
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(requestBody)
  });
  
  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`API Error: ${response.status} - ${errorText}`);
  }
  
  return await response.json();
}

// Example usage with two package types
async function createBookingExamples() {
  const bookings = [
    // Starter package example
    {
      bookingId: 'BOOKING-STARTER-001',
      departureDate: '2025-06-15T00:00:00.000Z',
      returnDate: '2025-06-22T00:00:00.000Z',
      departureLocation: 'JFK',
      email: 'starter@example.com',
      firstName: 'Alice',
      customBranding: 'EBSilver',
      packageSpecifications: [
        {
          package_type: 'starter',
          destination: 'Thailand'
        }
      ]
    },
    // Datalimited package example with 3GB size
    {
      bookingId: 'BOOKING-DATALIMITED-002',
      departureDate: '2025-06-20T00:00:00.000Z',
      returnDate: '2025-06-27T00:00:00.000Z',
      departureLocation: 'LHR',
      email: 'datalimited@example.com',
      firstName: 'Bob',
      customBranding: 'EBGold',
      packageSpecifications: [
        {
          package_type: 'data-limited',
          destination: 'Spain',
          size: '3GB',
          paid_price: 1499 // Price in cents paid by the end user
        }
      ]
    }
  ];

  for (const booking of bookings) {
    try {
      const result = await createBooking(booking);
      console.log(`${booking.custom_branding} booking created!`);
      console.log('  Promo Code:', result.data.promo_codes[0].promo_code);
    } catch (error) {
      console.error(`Error creating ${booking.custom_branding} booking:`, error.message);
    }
  }
}

createBookingExamples();

Response

A successful booking creation returns:

{
  "success": true,
  "message": "Booking created successfully.",
  "data": {
    "id": "xs8gWi1GXHWa7YyV4HVG",
    "booking_id": "YOUR-BOOKING-123",
    "departure_date": "2025-06-15T00:00:00.000Z",
    "return_date": "2025-06-22T00:00:00.000Z",
    "departure_location": "AMS",
    "first_name": "John",
    "custom_branding": "EBGold",
    "promo_codes": [
      {
        "promo_code": "CXZ6ZM9NGA",
        "uuid": "5163cbc9-a96a-4a97-8577-bd3416bd3e4b",
        "package_type": "starter",
        "package_size": "1GB",
        "package_duration": 2,
        "destination": "CV"
      }
    ]
  }
}

Using the Promo Code

The promo code is automatically delivered to the customer via email (when an email address is provided in the booking). You can also extract and store the promo code for your records:

const promoCode = result.data.promo_codes[0].promo_code;
// Store in your database for customer support purposes

Error Handling

Common error responses:

  • 401 Unauthorized: Invalid or missing authentication headers
  • 400 Bad Request: Invalid request data (missing required fields, invalid date format, etc.)
  • 500 Internal Server Error: Server-side error

Always check the response status and handle errors appropriately:

if (!response.ok) {
  const error = await response.json();
  console.error('API Error:', error);
  // Handle error appropriately
}

Using Brand Strategically

Brand with Tier Integration

The custom_branding field combines your brand identity with membership tiers. This allows you to:

  • Track performance: Analyze eSIM adoption rates across different brands and tiers
  • Customize experiences: Different brand-tier combinations can have different claim page branding
  • Reporting: Get separate analytics and billing per brand/tier
// Example: Different brand-tier combinations
const silverMemberBooking = {
  custom_branding: 'EBSilver',  // Silver tier branding
  // ... other fields
};

const pandionMemberBooking = {
  custom_branding: 'EBPandion',  // Pandion tier branding
  // ... other fields
};

Departure Location Analytics

The departure_location field (IATA airport code) helps you understand your customer demographics:

  • Track which departure airports generate the most eSIM bookings
  • Optimize marketing spend based on departure location data
  • Localize communication based on customer departure location

Tier Use Cases

Leverage the tier-based branding to create differentiated experiences, change colors, logos and banners to enhance the experience of the users.

When creating bookings with non-starter packages (such as data-limited), you can include a paid_price field in the package_specifications. This represents the price the end user has paid to upgrade their bundle from a basic starter package to a fully working data-limited bundle with significant data allowances.

Key points about paid_price:

  • Value format: Specify the price in EUR cents (e.g., 1499 for €14.99)
  • Applicable packages: Only for non-starter package types (data-limited). Starter packages do not support this field as they are typically offered free of charge
  • Purpose: Enables accurate revenue tracking and analytics for package upgrades
  • Reporting: Hubby can provide aggregated reports on upgrade revenue per brand/tier combination
// Example: Data-limited package with paid_price
{
  package_type: 'data-limited',
  destination: 'France',
  size: '5GB',
  paid_price: 2499  // €24.99 (EUR cents) paid by the end user for the upgrade
}

This information helps you understand the revenue generated from eSIM package upgrades and correlate it with your booking data.

Best Practices

  1. Store credentials securely: Never hardcode API keys in your source code. Use environment variables or secure configuration management
  2. Handle errors gracefully: Implement proper error handling and retry logic for network issues
  3. Validate input: Validate customer data before sending to the API
  4. Include email addresses: Provide customer email addresses when possible so Hubby can send installation instructions automatically
  5. Store promo codes: Save the promo codes in your database for customer support purposes
  6. Test in development: Use test credentials and test bookings before going to production
  7. Use consistent brand identifiers: Establish a naming convention for brand values that includes the tier level (e.g., EBSilver, EBGold, EBPandion)

Next Steps

  1. Set up your API credentials
  2. Learn about other package types
  3. Implement error handling

Support

For integration support or questions:

  • Email: tech@hubbyesim.com
  • Check the API reference for complete endpoint documentation