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 Code | Meaning | Action |
|---|---|---|
200 | Success | Process the response normally |
400 | Bad Request | Fix the request payload and retry |
401 | Unauthorized | Check your API key |
403 | Forbidden | Verify API key scopes |
404 | Not Found | Check the endpoint URL and resource ID |
429 | Too Many Requests | Implement exponential backoff and retry |
500 | Internal Server Error | Retry with exponential backoff |
502, 503, 504 | Gateway/Service Unavailable | Retry 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:
- JavaScript
- Python
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));
}
import time
import requests
from typing import Optional
def make_request_with_retry(
url: str,
method: str = 'POST',
max_retries: int = 3,
**kwargs
) -> dict:
last_error = None
for attempt in range(max_retries):
try:
response = requests.request(method, url, **kwargs)
# Success
if response.status_code < 400:
return response.json()
# Rate limited
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
continue
# Client errors (4xx) - don't retry
if 400 <= response.status_code < 500:
error = response.json()
raise ValueError(error.get('error', {}).get('message', 'Client error'))
# Server errors (5xx) - retry
if response.status_code >= 500:
raise requests.exceptions.HTTPError(f'Server error: {response.status_code}')
except Exception as e:
last_error = e
# Exponential backoff: 2^attempt seconds, max 30s
delay = min(1000 * (2 ** attempt), 30000) / 1000
time.sleep(delay)
raise last_error
Retry Logic by Error Type
| Error Type | Retry? | Strategy |
|---|---|---|
400 Bad Request | ❌ No | Fix the request |
401 Unauthorized | ❌ No | Check API key |
403 Forbidden | ❌ No | Update scopes |
404 Not Found | ❌ No | Check resource ID |
429 Rate Limited | ✅ Yes | Exponential backoff with Retry-After |
500 Internal Error | ✅ Yes | Exponential backoff (3-5 retries) |
502/503/504 Gateway | ✅ Yes | Exponential 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
- Rate Limits Guide - Understand rate limiting in detail
- Webhooks Guide - Set up real-time notifications
- API Reference - Browse all endpoints