The errorCode field is the most reliable signal for programmatic error handling. HTTP status codes indicate broad categories; error codes give you the exact diagnosis.
All error responses share a common envelope:
{
"success": false,
"statusCode": 400,
"errorCode": "TB_AUTH_101",
"message": "Invalid API key",
"fault": "client",
"retryable": true,
"suggestedAction": "Verify your API key and try again",
"reason": "Invalid API key",
"environment": "sandbox",
"apiVersion": "1.0.0"
}
Field Reference
Validation Errors
When a request fails field-level validation — for example, submitting a booking without a passenger email — the reason field becomes an array of objects, each describing a specific field that failed:
{
"success": false,
"status": "error",
"statusCode": 400,
"errorCode": "TB_VAL_302",
"message": "Validation failed",
"fault": "client",
"retryable": true,
"reason": [
{
"field": "email",
"message": "Required"
},
{
"field": "password",
"message": "Required"
}
],
"apiVersion": "1.0.0"
}
Use the reason array to surface field-specific feedback directly in your booking forms. Each object maps precisely to a form field, making inline validation straightforward:
function handleValidationError(error: TravelbaseError) {
if (error.errorCode === "TB_VAL_302" && Array.isArray(error.reason)) {
for (const fieldError of error.reason) {
setFieldError(fieldError.field, fieldError.message);
}
}
}
Error Code Catalog
Error codes follow a structured namespace: TB_<CATEGORY>_<CODE>. This makes it easy to handle entire categories with a single prefix check before falling through to specific codes.
Authentication — TB_AUTH_*
Permissions — TB_PERM_*
Validation — TB_VAL_*
Resources — TB_RES_*
System — TB_SYS_*
Rate & Quota Limits — TB_LIM_*
Payments — TB_PAY_*
Payment errors require careful handling. Always check retryable before attempting a retry — retrying a non-retryable payment error can result in multiple authorisation holds on a traveller’s card.
Tenant Status — TENANT_*
These errors apply when your platform operates in a multi-tenant context. They reflect the operational status of the tenant account making the request.
Handling Errors in Code
The following patterns cover the full error handling lifecycle for a Travelbase integration.
import type { TravelbaseError, FieldError } from "@travelbase/sdk";
async function createBooking(payload: BookingPayload) {
try {
return await travelbase.bookings.create(payload);
} catch (err) {
if (!isTravelbaseError(err)) throw err;
return handleTravelbaseError(err);
}
}
function handleTravelbaseError(error: TravelbaseError) {
const { errorCode, retryable, suggestedAction } = error;
// Authentication — token has lapsed mid-session
if (errorCode === "TB_AUTH_102" || errorCode === "TB_AUTH_104") {
return refreshTokenAndRetry();
}
// Validation — surface field-level errors back to the booking form
if (errorCode === "TB_VAL_302" && Array.isArray(error.reason)) {
for (const field of error.reason as FieldError[]) {
setFormError(field.field, field.message);
}
return;
}
// Payment declined — prompt traveller to use another card
if (errorCode === "TB_PAY_800") {
return showPaymentDeclinedModal();
}
// Rate limit — respect the Retry-After header
if (errorCode === "TB_LIM_700") {
const retryAfter = error.headers?.["retry-after"] ?? 60;
return scheduleRetry(retryAfter * 1000);
}
// Tenant issues — operational block, not a code bug
if (errorCode.startsWith("TENANT_")) {
return showTenantStatusBanner(errorCode);
}
// Server-side retryable errors
if (error.fault === "server" && retryable) {
return retryWithBackoff(createBooking, payload);
}
// Non-retryable client errors — log and surface the suggestion
console.error(`[Travelbase] ${errorCode}: ${suggestedAction}`);
throw error;
}
from travelbase import TravelbaseError
def create_booking(payload: dict):
try:
return travelbase.bookings.create(payload)
except TravelbaseError as err:
return handle_travelbase_error(err)
def handle_travelbase_error(err: TravelbaseError):
code = err.error_code
if code in ("TB_AUTH_102", "TB_AUTH_104"):
return refresh_token_and_retry()
if code == "TB_VAL_302" and isinstance(err.reason, list):
for field_error in err.reason:
set_form_error(field_error["field"], field_error["message"])
return
if code == "TB_PAY_800":
return show_payment_declined()
if code == "TB_LIM_700":
retry_after = int(err.headers.get("retry-after", 60))
return schedule_retry(delay_seconds=retry_after)
if code.startswith("TENANT_"):
return show_tenant_status_banner(code)
if err.fault == "server" and err.retryable:
return retry_with_backoff(create_booking, payload)
raise err
func createBooking(payload BookingPayload) (*Booking, error) {
booking, err := client.Bookings.Create(payload)
if err == nil {
return booking, nil
}
tbErr, ok := err.(*TravelbaseError)
if !ok {
return nil, err
}
switch tbErr.ErrorCode {
case "TB_AUTH_102", "TB_AUTH_104":
return refreshAndRetry(payload)
case "TB_VAL_302":
return nil, formatValidationErrors(tbErr.Reason)
case "TB_PAY_800":
return nil, ErrPaymentDeclined
case "TB_LIM_700":
retryAfter := tbErr.RetryAfterSeconds(60)
time.Sleep(time.Duration(retryAfter) * time.Second)
return createBooking(payload)
}
if strings.HasPrefix(tbErr.ErrorCode, "TENANT_") {
return nil, fmt.Errorf("tenant issue: %s", tbErr.ErrorCode)
}
if tbErr.Fault == "server" && tbErr.Retryable {
return retryWithBackoff(createBooking, payload)
}
return nil, tbErr
}
Retry Strategy
Not every error should be retried. Use retryable: true as your gate, then apply exponential backoff to avoid overwhelming the API during degraded conditions.
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxAttempts = 4,
baseDelayMs = 300
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
const isLast = attempt === maxAttempts;
const isRetryable = isTravelbaseError(err) && err.retryable;
if (isLast || !isRetryable) throw err;
const delay = baseDelayMs * 2 ** (attempt - 1); // 300ms → 600ms → 1.2s → 2.4s
const jitter = Math.random() * 100;
await sleep(delay + jitter);
}
}
throw new Error("Unreachable");
}
Add random jitter (a small random offset on top of the delay) to prevent a thundering herd — many clients retrying in perfect lockstep can amplify an outage rather than recover from it.
Decision Tree
When you receive an error, follow this path to resolution:
Check `fault`
"client" means your request needs to change before retrying. "server" means the problem is on Travelbase’s side and may resolve on its own.
Check `retryable`
If false, do not retry — fix the request first. If true, proceed to backoff.
Match on `errorCode`
Use the catalog above to understand the exact cause. Handle auth, validation, payments, and tenant errors with dedicated logic paths.
Use `suggestedAction`
Surface this field to internal support tooling or operations dashboards. It is written for humans, not machines.
Escalate if needed
For TB_SYS_600, TN_ISE_901, TB_PAY_802, or persistent TENANT_SUSPENDED, open a support ticket with the full error body and your request ID.
Common Mistakes
| Mistake | Consequence | Fix |
|---|
| Retrying non-retryable errors in a loop | Duplicate charges, burnt quota, account lockout | Always check retryable: false before retrying |
Swallowing TB_RES_402 (invalid resource state) | Attempting to cancel a departed flight silently fails | Check the booking’s status field before mutating it |
Ignoring TENANT_SUSPENDED | All bookings for affected travellers are silently blocked | Monitor for tenant codes and alert your operations team immediately |
Using message for programmatic branching | Prone to breaking if copy changes | Always branch on errorCode, never on message |
Not reading the reason array on TB_VAL_302 | Generic “something went wrong” shown to travellers | Map reason fields directly to your booking form for inline feedback |
Retrying TB_PAY_800 automatically | Multiple authorisation holds on the traveller’s card | Payment declines require human action — prompt for a different payment method |
TypeScript Error Code Enum
Copy this enum directly into your project for compile-time safety across all error handling code:
export enum TravelbaseErrorCode {
// Authentication
AUTH_MISSING = "TB_AUTH_100",
AUTH_INVALID = "TB_AUTH_101",
AUTH_EXPIRED = "TB_AUTH_102",
AUTH_BLOCKED = "TB_AUTH_103",
SESSION_EXPIRED = "TB_AUTH_104",
// Permissions
ACCESS_DENIED = "TB_PERM_200",
ACTION_NOT_ALLOWED = "TB_PERM_201",
// Validation
INVALID_INPUT = "TB_VAL_300",
FIELD_FORMAT_INVALID = "TB_VAL_301",
VALIDATION_ERROR = "TB_VAL_302",
DUPLICATE_RESOURCE = "TB_VAL_303",
// Resources
RESOURCE_NOT_FOUND = "TB_RES_400",
RESOURCE_EXISTS = "TB_RES_401",
RESOURCE_STATE_INVALID = "TB_RES_402",
// Bad Request
BAD_REQUEST = "TB_BR_403",
// System
INTERNAL_ERROR = "TB_SYS_600",
UNSUPPORTED_ACTION = "TB_SYS_602",
INTERNAL_SERVER_ERROR = "TN_ISE_901",
// Limits
RATE_LIMIT_EXCEEDED = "TB_LIM_700",
QUOTA_EXCEEDED = "TB_LIM_701",
TOO_MANY_ATTEMPTS = "TB_LIM_702",
// Payments
PAYMENT_FAILED = "TB_PAY_800",
PAYMENT_METHOD_UNSUPPORTED = "TB_PAY_801",
REFUND_FAILED = "TB_PAY_802",
// Tenant
TENANT_PENDING_APPROVAL = "TENANT_PENDING_APPROVAL",
TENANT_REJECTED = "TENANT_REJECTED",
TENANT_SUSPENDED = "TENANT_SUSPENDED",
TENANT_UNAVAILABLE = "TENANT_UNAVAILABLE",
}