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:
- You create a booking with a starter package and a specific brand.
- The system generates a promo code
- We use our optimized email flow to deliver the promo code(s) alongside installation instructions to your customer, i.e. the end user.
- 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
x-api-key: Your public API keyx-timestamp: Current timestamp in milliseconds (Unix epoch time)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/bookingsdoes 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/bookingsStaging (for testing):
POST https://api-staging.hubby.dev/api/bookingsRequest 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 ifemailis 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. Eitherbooking_idoremailmust be provideddeparture_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 communicationsemail(required ifbooking_idis not provided): Customer email address. If provided, Hubby sends eSIM installation instructions directly to the customer. Eitheremailorbooking_idmust be provided; both can be suppliedfirst_name(optional): Customer first namecustom_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 accordinglypackage_specifications(required): Array with at least one package specification. Each entry is resolved bypackage_id(direct reference) or by other properties (destination, size, etc.); whenpackage_idis 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 allowancedestination(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 automaticallysize(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 todata-limitedpackage typespaid_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 todata-limitedpackage 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_pricefield - 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 Value | Description |
|---|---|
"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 purposesError 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.
Paid Price for Upgraded Packages
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.,
1499for €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
- Store credentials securely: Never hardcode API keys in your source code. Use environment variables or secure configuration management
- Handle errors gracefully: Implement proper error handling and retry logic for network issues
- Validate input: Validate customer data before sending to the API
- Include email addresses: Provide customer email addresses when possible so Hubby can send installation instructions automatically
- Store promo codes: Save the promo codes in your database for customer support purposes
- Test in development: Use test credentials and test bookings before going to production
- Use consistent brand identifiers: Establish a naming convention for brand values that includes the tier level (e.g.,
EBSilver,EBGold,EBPandion)
Next Steps
Support
For integration support or questions:
- Email: tech@hubbyesim.com
- Check the API reference for complete endpoint documentation