Skip to main content

Error Handling

This guide explains how to handle errors from the Bundleport API, including error codes, retry strategies, and best practices.

Error Response Format

All API errors follow a consistent format:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
"field": "Additional context about the error"
}
}
}

HTTP Status Codes

Status CodeMeaningAction
200SuccessProcess the response normally
400Bad RequestFix the request payload and retry
401UnauthorizedCheck your API key
403ForbiddenVerify API key scopes
404Not FoundCheck the endpoint URL and resource ID
429Too Many RequestsImplement exponential backoff and retry
500Internal Server ErrorRetry with exponential backoff
502, 503, 504Gateway/Service UnavailableRetry with exponential backoff

Common Error Codes

Authentication Errors

UNAUTHORIZED

Invalid or missing API key.

{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}
}

Solution: Verify your Authorization header format: ApiKey YOUR_KEY

FORBIDDEN

API key doesn't have required permissions.

{
"error": {
"code": "FORBIDDEN",
"message": "API key does not have required scope: hotels:book"
}
}

Solution: Update your API key scopes in the dashboard

Validation Errors

VALIDATION_ERROR

Request payload doesn't match the API schema.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request payload",
"details": {
"field": "stay.checkIn",
"reason": "Date must be in YYYY-MM-DD format"
}
}
}

Solution: Fix the validation errors and retry

INVALID_DESTINATION

Destination code is invalid or not supported.

{
"error": {
"code": "INVALID_DESTINATION",
"message": "Destination code 'XXX' is not valid"
}
}

Solution: Use valid destination codes (IATA codes for airports, city codes, etc.)

Business Logic Errors

NO_AVAILABILITY

No hotels available for the search criteria.

{
"error": {
"code": "NO_AVAILABILITY",
"message": "No hotels found matching your criteria"
}
}

Solution: Adjust search criteria (dates, destination, filters)

PRICE_CHANGED

Price changed between quote and booking.

{
"error": {
"code": "PRICE_CHANGED",
"message": "Price has changed. Please recheck availability.",
"details": {
"originalPrice": 150.00,
"newPrice": 165.00
}
}
}

Solution: Recheck availability and present updated price to user

BOOKING_FAILED

Booking could not be completed.

{
"error": {
"code": "BOOKING_FAILED",
"message": "Booking failed: Room no longer available",
"details": {
"provider": "SUPPLIER_1",
"reason": "INVENTORY_EXHAUSTED"
}
}
}

Solution: Search again and try a different option

Rate Limiting

RATE_LIMIT_EXCEEDED

Too many requests in a short period.

{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Retry after 60 seconds."
}
}

Solution: Implement exponential backoff (see below)

Retry Strategies

Exponential Backoff

For transient errors (5xx, 429), implement exponential backoff:

async function makeRequestWithRetry(url, options, maxRetries = 3) {
let lastError;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);

// Success
if (response.ok) {
return await response.json();
}

// Rate limited - check Retry-After header
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
await sleep(retryAfter * 1000);
continue;
}

// Client errors (4xx) - don't retry
if (response.status >= 400 && response.status < 500) {
const error = await response.json();
throw new Error(error.error?.message || 'Client error');
}

// Server errors (5xx) - retry
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}

} catch (error) {
lastError = error;

// Calculate backoff delay: 2^attempt seconds
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await sleep(delay);
}
}

throw lastError;
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

Retry Logic by Error Type

Error TypeRetry?Strategy
400 Bad Request❌ NoFix the request
401 Unauthorized❌ NoCheck API key
403 Forbidden❌ NoUpdate scopes
404 Not Found❌ NoCheck resource ID
429 Rate Limited✅ YesExponential backoff with Retry-After
500 Internal Error✅ YesExponential backoff (3-5 retries)
502/503/504 Gateway✅ YesExponential backoff (3-5 retries)

Handling Warnings

Some responses include warnings that don't prevent the request from succeeding:

{
"options": [...],
"warnings": [
{
"code": "UNMAPPED_HOTEL",
"message": "Hotel code '12345' not found in catalog",
"severity": "INFO"
}
]
}

Best Practice: Log warnings for monitoring but don't treat them as errors.

Error Handling Best Practices

1. Always Check Response Status

const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'Request failed');
}

2. Log Errors for Debugging

try {
const data = await makeRequest(url, options);
} catch (error) {
console.error('API Error:', {
url,
status: error.status,
code: error.code,
message: error.message,
timestamp: new Date().toISOString(),
});
// Send to error tracking service (Sentry, etc.)
throw error;
}

3. Provide User-Friendly Messages

function getUserFriendlyMessage(error) {
const errorMessages = {
'UNAUTHORIZED': 'Please check your API credentials',
'NO_AVAILABILITY': 'No hotels found. Try different dates or destination.',
'PRICE_CHANGED': 'Price updated. Please review and confirm.',
'BOOKING_FAILED': 'Booking unavailable. Please try another option.',
};

return errorMessages[error.code] || error.message || 'An error occurred';
}

4. Implement Circuit Breaker Pattern

For production systems, consider a circuit breaker to prevent cascading failures:

class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}

async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}

try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}

onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}

Next Steps