Error Handling
This guide explains how to handle errors from the Bundleport API, including error codes, retry strategies, and best practices.
Supplier tracing and sandbox flags
When debugging integration issues with support or your connectivity team:
settings.auditTransactions: set totrueto include supplier-level trace data inauditDataon successful responses (useful to see raw protocol exchanges). Do not store or log full audit payloads in production systems without redaction; never commit them to source control.settings.testMode: set totruein sandbox when supported so downstream handling treats traffic as non-production.tracing.accessSpans: on search and similar calls, inspect per-connection status (success, timeout, error) when one supplier fails and others succeed—partial failures often return200with warnings rather than a top-level error.
See Sandbox & testing for the full sandbox checklist.
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
- Error Codes — Full list of error codes and their meanings
- Status Codes — Booking and search status reference
- Rate Limits Guide — Understand rate limiting in detail
- Webhooks Guide — Set up real-time notifications
- API Reference — Browse all endpoints