Skip to main content

Error Codes & Handling

Complete reference for API error responses and how to handle them.

Error Response Format

All errors follow a consistent structure:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {
"field": "Additional context"
}
},
"meta": {
"timestamp": "2025-01-15T12:00:00Z",
"requestId": "req_abc123"
}
}

HTTP Status Codes

StatusMeaningWhen It Occurs
200OKRequest succeeded
201CreatedResource created (booking, entry)
204No ContentDelete succeeded
400Bad RequestInvalid request parameters
401UnauthorizedMissing or invalid token
403ForbiddenValid token but insufficient permissions
404Not FoundResource doesn't exist
409ConflictResource state conflict (already booked)
422UnprocessableValid syntax but business rule failure
429Too Many RequestsRate limit exceeded
500Server ErrorInternal error (retry may help)
503Service UnavailableTemporary outage

Booking Errors

BOOKING_NOT_FOUND

{ "code": "BOOKING_NOT_FOUND", "message": "Booking not found" }

Cause: The booking ID doesn't exist or was deleted. Solution: Verify the booking ID. Check /players/me/bookings for valid IDs.

SLOT_NOT_AVAILABLE

{ "code": "SLOT_NOT_AVAILABLE", "message": "Tee time slot is no longer available" }

Cause: Another player booked the slot while you were checking out. Solution: Search for alternative tee times and retry.

HOLD_EXPIRED

{ "code": "HOLD_EXPIRED", "message": "Your hold on this tee time has expired" }

Cause: The 5-minute hold period elapsed before payment. Solution: Start the booking process again from search.

INVALID_PLAYER_COUNT

{ "code": "INVALID_PLAYER_COUNT", "message": "Invalid number of players", "details": { "min": 1, "max": 4 } }

Cause: Requested player count outside allowed range. Solution: Adjust player count within the allowed range.

CANCELLATION_NOT_ALLOWED

{ "code": "CANCELLATION_NOT_ALLOWED", "message": "Booking cannot be cancelled", "details": { "reason": "past_cutoff" } }

Cause: Cancellation window has closed per club policy. Solution: Contact the club directly for assistance.

RESCHEDULE_NOT_ALLOWED

{ "code": "RESCHEDULE_NOT_ALLOWED", "message": "Booking cannot be rescheduled" }

Cause: Reschedule not permitted by club policy or booking type. Solution: Cancel (if allowed) and create a new booking.

Payment Errors

PAYMENT_FAILED

{ "code": "PAYMENT_FAILED", "message": "Payment was declined", "details": { "reason": "insufficient_funds" } }

Cause: Card declined by payment processor. Solution: Try a different payment method or contact your bank.

PAYMENT_REQUIRED

{ "code": "PAYMENT_REQUIRED", "message": "Payment is required to complete booking" }

Cause: Attempting to confirm booking without payment. Solution: Complete the payment flow before confirming.

REFUND_NOT_AVAILABLE

{ "code": "REFUND_NOT_AVAILABLE", "message": "Refund not available for this booking" }

Cause: Cancellation policy doesn't allow refunds. Solution: Review club's cancellation policy.

Authentication Errors

UNAUTHORIZED

{ "code": "UNAUTHORIZED", "message": "Authentication required" }

Cause: No token provided or token malformed. Solution: Include a valid Authorization: Bearer <token> header.

TOKEN_EXPIRED

{ "code": "TOKEN_EXPIRED", "message": "Access token has expired" }

Cause: Token's validity period has ended. Solution: Refresh the token using your refresh token.

INVALID_TOKEN

{ "code": "INVALID_TOKEN", "message": "Token is invalid" }

Cause: Token is corrupted or was revoked. Solution: Re-authenticate to obtain a new token.

FORBIDDEN

{ "code": "FORBIDDEN", "message": "Insufficient permissions" }

Cause: Token valid but lacks required scope/role. Solution: Ensure the token has the required permissions.

Player Errors

PLAYER_NOT_FOUND

{ "code": "PLAYER_NOT_FOUND", "message": "Player not found" }

Cause: Player ID doesn't exist. Solution: Verify the player ID or use /players/me.

BUDDY_ALREADY_EXISTS

{ "code": "BUDDY_ALREADY_EXISTS", "message": "Player is already in your buddy list" }

Cause: Attempting to add an existing buddy. Solution: No action needed; buddy relationship exists.

MEMBERSHIP_NOT_VERIFIED

{ "code": "MEMBERSHIP_NOT_VERIFIED", "message": "Golf association membership not verified" }

Cause: Membership linking failed or is pending. Solution: Retry membership linking or contact support.

Rate Limiting

RATE_LIMIT_EXCEEDED

{ "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests" }

Cause: Exceeded your rate limit quota. Solution: Wait until the reset time and retry.

Rate limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705320000

Retry Strategy

For rate-limited requests:

async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);

if (response.status === 429) {
const resetTime = response.headers.get('X-RateLimit-Reset');
const waitMs = (resetTime * 1000) - Date.now() + 1000;
await sleep(Math.min(waitMs, 60000));
continue;
}

return response;
}
throw new Error('Max retries exceeded');
}

Validation Errors

VALIDATION_ERROR

{
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"errors": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "phone", "message": "Phone must be in E.164 format" }
]
}
}

Cause: Request body or parameters failed validation. Solution: Fix the fields listed in the errors array.

Server Errors

INTERNAL_ERROR

{ "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" }

Cause: Server-side error. Solution: Retry after a few seconds. If persistent, contact support with the requestId.

SERVICE_UNAVAILABLE

{ "code": "SERVICE_UNAVAILABLE", "message": "Service temporarily unavailable" }

Cause: Planned maintenance or temporary outage. Solution: Retry after a few minutes. Check status page for updates.

Best Practices

Always Check Response Status

const response = await fetch('/api/bookings', { method: 'POST', body });

if (!response.ok) {
const error = await response.json();

switch (error.error.code) {
case 'SLOT_NOT_AVAILABLE':
// Redirect to search for alternatives
break;
case 'PAYMENT_FAILED':
// Show payment error UI
break;
default:
// Show generic error
}
}

Log Request IDs

Always log the requestId from error responses. This helps support teams diagnose issues:

if (error.meta?.requestId) {
console.error(`API Error [${error.meta.requestId}]:`, error.error.message);
}

Implement Exponential Backoff

For transient errors (500, 503), use exponential backoff:

const delays = [1000, 2000, 4000, 8000];

for (let i = 0; i < delays.length; i++) {
try {
return await makeRequest();
} catch (error) {
if (error.status >= 500 && i < delays.length - 1) {
await sleep(delays[i]);
continue;
}
throw error;
}
}