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
| Status | Meaning | When It Occurs |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created (booking, entry) |
| 204 | No Content | Delete succeeded |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Missing or invalid token |
| 403 | Forbidden | Valid token but insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource state conflict (already booked) |
| 422 | Unprocessable | Valid syntax but business rule failure |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Internal error (retry may help) |
| 503 | Service Unavailable | Temporary 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;
}
}