Claim Links
This guide demonstrates how to integrate the Hubby eSIM API to create bookings with destinationless starter packages, where a claim link is generated for every promo code created on the booking.
This is perfect for simple integrations where customers can choose their destination later, and you can simply send them a link that takes them straight to redemption.
Overview
In this integration:
- You create a booking with a destinationless starter package
- The system creates one or more promo codes for that booking
- For each promo code, the system also generates a
claim_url - You send the claim link(s) to your customer
- The customer clicks the link, redeems the eSIM, and selects a destination during redemption
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
- Your API credentials work in both environments
- 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. The message format is:
{timestamp}{HTTP_METHOD}/api{path}For example, for a POST request to /api/bookings:
- If timestamp is
1704067200000 - The message would be:
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
const message = `${timestamp}${method}/api${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 match the actual API path (e.g.,
/bookingsnot/api/bookingsin the message) - 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 (and Claim Links)
Endpoint
Production:
POST https://api.hubbyesim.com/api/bookingsStaging (for testing):
POST https://api-staging.hubby.dev/api/bookingsRequest Body
For a destinationless starter package booking, you need:
{
"booking_id": "YOUR-BOOKING-123",
"departure_date": "2025-06-15",
"first_name": "John",
"last_name": "Doe",
"package_specifications": [
{
"package_type": "starter",
"size": "500MB",
"package_duration": 2
}
]
}Field Descriptions
booking_id(optional but recommended): Your internal booking referencedeparture_date(required): Travel date inYYYY-MM-DDformatfirst_name(required): Customer first namelast_name(required): Customer last namepackage_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: "starter": Creates a destinationless starter package (customer selects destination later)size(required for all types except unlimited): Data allowance for the packagepackage_duration(required for starter, time-limited, and unlimited): Duration in days
Complete Example (Node.js)
This example creates a booking and then prints all promo codes and their claim URLs.
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,
email: bookingData.email ?? null,
first_name: bookingData.firstName,
last_name: bookingData.lastName,
package_specifications: bookingData.packageSpecifications ?? [
{ package_type: 'starter', size: '500MB', package_duration: 2 }
]
};
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
};
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', reject);
req.write(JSON.stringify(requestBody));
req.end();
});
}
// Example usage
async function main() {
try {
const result = await createBooking({
bookingId: 'YOUR-BOOKING-123',
departureDate: '2025-06-15',
firstName: 'John',
lastName: 'Doe',
// Optional: specify multiple packages (=> multiple promo codes => multiple claim links)
// packageSpecifications: [
// { package_type: 'starter', size: '500MB', package_duration: 2 },
// { package_type: 'starter', size: '500MB', package_duration: 2 }
// ]
});
console.log('Booking created successfully!');
console.log('Booking ID:', result.data.id);
// Loop all promo codes and their links
for (const pc of result.data.promo_codes ?? []) {
console.log('---');
console.log('Promo Code:', pc.promo_code);
console.log('UUID:', pc.uuid);
console.log('Claim URL:', pc.claim_url);
console.log('Deep Link URL:', pc.deep_link_url);
}
} catch (error) {
console.error('Error creating booking:', error.message);
}
}
main();Response
A successful booking creation returns the booking plus an array of promo codes. Each promo code includes a claim_url.
{
"success": true,
"message": "Booking created successfully.",
"data": {
"id": "RKI8w2e09kImpBnJ2iHH",
"title": null,
"first_name": "John",
"last_name": "Doe",
"full_name": "John",
"pax": 1,
"email": null,
"phone": null,
"booking_id": "YOUR-BOOKING-123",
"external_id": "agentnumber1",
"return_date": "2025-06-15T00:00:00.000Z",
"departure_date": "2025-06-15T00:00:00.000Z",
"gender": "M",
"locale": "de-DE",
"created_at": "2026-01-12T14:55:04.724Z",
"promo_codes": [
{
"promo_code": "WSTZXS9HK7",
"uuid": "f18f9757-ffff-ffff-ffff-6b2c8af66b2c",
"claim_url": "https://platform.hubbyesim.com/claim-promocode/bronze?uuid=f18f9757-1365-426d-bd32-6b2c8af66b2c",
"deep_link_url": "https://hubby-esim.web.app/starter/?promoCode=WSTZXS9HK7",
"package_id": "",
"package_size": "",
"destination": "Türkei",
"iso3": "TUR",
"package_type": "starter",
"package_duration": 2
}
],
"updated_at": "2026-01-12T14:55:05.302Z",
"status": "COMPLETED"
}
}Using the Links (Recommended)
You generally don’t need to display the raw promo code anymore. Instead, you can send the claim link or deep link to the traveler.
Each promo code in the response includes two links:
claim_url: A web link where the user enters their email to receive the promo codedeep_link_url: A mobile deep link that opens the Hubby app and auto-applies the promo code
How it works
- Extract the links from the response:
result.data.promo_codes[i].claim_urlresult.data.promo_codes[i].deep_link_url
- Include the link(s) in your booking confirmation email, SMS, or itinerary
- The traveler clicks the link and redeems the eSIM on the Hubby platform
- The traveler continues the flow (including destination choice for starter packages)
Example (single promo code):
const claimUrl = result.data.promo_codes[0].claim_url;
const deepLinkUrl = result.data.promo_codes[0].deep_link_url;
// Include in email: "Activate your eSIM: <claimUrl>"
// Or for mobile: "Open in app: <deepLinkUrl>"Example (multiple promo codes):
const links = (result.data.promo_codes ?? []).map(p => ({
claim: p.claim_url,
deepLink: p.deep_link_url,
}));
// Render these in your UI/email as separate "Activate eSIM" buttons/links.Why these links are better
- Less friction: no copy/paste code needed
- Easier support: you can store the UUID/claim URL and look it up later
- Better follow-up options: claim flow allows capturing traveler contact details (when applicable)
- Mobile-optimized: deep links open the Hubby app directly for one-tap redemption
Option: Still show the Promo Code
If you want a fallback, you can still show:
promo_codefor manual entryuuidfor internal support and reconciliation
But for the best experience, send the claim link.
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
}Best Practices
- Store credentials securely: Use environment variables or a secrets manager
- Store claim URLs and UUIDs: Save them for support and re-sending to the customer
- Send claim URLs by default: It’s the simplest flow for travelers
- Handle multiple promo codes: Loop over
promo_codes[]and send one claim link per promo code - Test in staging first: Validate signing, payloads, and parsing logic before production
Next Steps
Support
For integration support or questions:
- Email: tech@hubbyesim.com
- Check the API reference for complete endpoint documentation