ResytechResytech Docs

Error Handling & Troubleshooting

Handle API errors, shopping cart validation, gift card issues, network failures, and debug common integration problems.

Every ResytechApi method returns a response object that extends ApiResponseBase. Errors never throw exceptions -- they are returned as response objects with success: false.

ApiResponseBase

All responses include these fields:

interface ApiResponseBase {
  success: boolean;        // true if the request succeeded
  message: string;         // human-readable description
  statusCode: number;      // HTTP status code (0 for network errors, 408 for timeout)
  action: string;          // the API endpoint that was called
  isNetworkError: boolean; // true for network failures and timeouts
}

Error Detection

Check success

The primary way to detect errors:

const response = await api.activity.getActivity('some-uuid');

if (!response.success) {
  console.error(`Error: ${response.message}`);
  return;
}

// Safe to use response.activity

Network errors vs API errors

Use isNetworkError to distinguish between server-side failures and connectivity issues:

const response = await api.cart.createOrUpdateCart(request);

if (!response.success) {
  if (response.isNetworkError) {
    // Connection failed, timeout, or DNS issue
    showMessage('Unable to reach the server. Check your connection and try again.');
  } else if (response.statusCode >= 500) {
    // Server error
    showMessage('Something went wrong on our end. Please try again later.');
  } else {
    // Client error (400, 401, 403, 404, etc.)
    showMessage(response.message);
  }
}

Status codes

CodeMeaningisNetworkError
0Network failure (fetch threw)true
400Bad request / validation errorfalse
401Unauthorized (missing or expired token)false
403Forbiddenfalse
404Resource not foundfalse
408Request timeout (AbortController fired)true
500Internal server errorfalse

Client-side validation errors

Some methods validate input before making the HTTP request. These return immediately with statusCode: 400 and isNetworkError: false:

// This returns an error without making a network request
const response = await api.calendar.getActivityCalendar({
  activity: '',  // empty
  month: 13,     // out of range
  year: 2025,
});

// response.success === false
// response.message === 'Activity, month, and year are required'
// response.statusCode === 400
// response.isNetworkError === false

Shopping Cart Error Codes

When api.cart.createOrUpdateCart() fails, the response includes an errorCode field with a ShoppingCartErrorCode value.

const response = await api.cart.createOrUpdateCart(request);

if (!response.success && response.errorCode !== undefined) {
  switch (response.errorCode) {
    case 4:  // TimeSlotNotAvailable
      showMessage('That time slot is no longer available. Please select another.');
      break;
    case 6:  // EquipmentQuantityExceeded
      showMessage('Not enough equipment available. Please reduce your quantity.');
      break;
    case 9:  // GuestLimitExceeded
      showMessage('Too many guests. Maximum is ' + getGuestLimit());
      break;
    case 19: // CartExpired
      showMessage('Your session has expired. Please start over.');
      resetBooking();
      break;
    default:
      showMessage(response.message);
  }
}

All ShoppingCartErrorCode Values

CodeNameDescriptionSuggested Action
0NoneNo error--
1ActivityNotFoundActivity UUID is invalid or deletedRefresh activity list
2EquipmentNotFoundEquipment UUID is invalid or deletedRefresh activity details
3DurationNotFoundDuration UUID is invalid or deletedRefresh durations
4TimeSlotNotAvailableTime slot was booked by someone elseReload time slots
5EquipmentNotAvailableEquipment is fully booked for this slotShow alternative times
6EquipmentQuantityExceededRequested more units than availableShow max available in UI
7SeatLimitExceededTotal seats exceed equipment capacityReduce guests or add equipment
8GuestMinimumNotMetBelow minimum guest requirementShow minimum in UI
9GuestLimitExceededAbove maximum guest limitShow maximum in UI
10AddonNotFoundAddon UUID is invalidRefresh addon list
11AddonNotAvailableAddon not available for this slotRemove addon from cart
12AddonQuantityExceededToo many addons requestedReduce addon quantity
13InvalidDateDate format is wrong or out of rangeValidate date input
14DateNotAvailableDate is blacked out or past cutoffReload calendar
15InvalidDurationDuration selection is not validRefresh durations
16InvalidTimeSlotTime slot data is malformedReload time slots
17CustomerRequiredCustomer info missing at checkoutPrompt for customer details
18InvalidCustomerCustomer data validation failedCheck email format, required fields
19CartExpiredCart session timed outCreate a new cart
20CartNotFoundCart ID does not existCreate a new cart
21PaymentFailedStripe payment was declinedShow payment error, retry
22CouponInvalidCoupon code not recognizedShow "invalid coupon" message
23CouponExpiredCoupon has expiredShow "coupon expired" message
24GeneralErrorCatch-all server errorShow generic error, retry

Cart Removals

When the cart is updated, the server may remove items that are no longer valid. Check response.removals:

const response = await api.cart.createOrUpdateCart(request);

if (response.success && response.removals?.length) {
  response.removals.forEach(removal => {
    switch (removal.type) {
      case 1: // EquipmentUnavailable
        notify(`Equipment is no longer available and was removed.`);
        break;
      case 2: // EquipmentQuantityReduced
        notify(`Equipment quantity reduced to ${removal.maxQuantity}.`);
        break;
      case 3: // AddonUnavailable
        notify(`An addon was removed because it is no longer available.`);
        break;
      // ... handle other types
    }
  });
}

ShoppingCartRemovalType Values

CodeNameDescription
0NoneNo removal
1EquipmentUnavailableEquipment removed
2EquipmentQuantityReducedEquipment quantity reduced to max
3AddonUnavailableAddon removed
4AddonQuantityReducedAddon quantity reduced
5SeatReductionSeats reduced to max available
6DurationUnavailableDuration no longer available
7TimeSlotUnavailableTime slot no longer available
8DateUnavailableDate no longer available
9ActivityUnavailableActivity no longer available

Gift Card Error Codes

When api.cart.applyGiftCard() fails, the response includes a GiftCardErrorCode:

const response = await api.cart.applyGiftCard({
  giftCardCode: userInput,
  cartId: currentCartId,
});

if (!response.success) {
  switch (response.errorCode) {
    case 1:  // InvalidCode
      showError('That gift card code is not valid.');
      break;
    case 2:  // Expired
      showError('This gift card has expired.');
      break;
    case 3:  // NoBalance
      showError('This gift card has no remaining balance.');
      break;
    case 5:  // AlreadyApplied
      showError('This gift card is already applied to your cart.');
      break;
    case 8:  // LocationMismatch
      showError('This gift card cannot be used at this location.');
      break;
    default:
      showError(response.message);
  }
}

All GiftCardErrorCode Values

CodeNameDescription
0NoneNo error
1InvalidCodeCode format is wrong or unrecognized
2ExpiredGift card has expired
3NoBalanceBalance is zero
4InsufficientBalanceBalance is less than expected (informational)
5AlreadyAppliedAlready applied to this cart
6NotFoundGift card does not exist in the system
7DisabledGift cards are disabled for this location
8LocationMismatchGift card belongs to a different location
9GeneralErrorCatch-all server error

Network Error Handling

The ApiController uses the Fetch API with AbortController for timeouts. All network-level errors are caught and returned as structured responses.

Timeout

const api = new ResytechApi({ timeout: 15000 }); // 15 seconds

const response = await api.activity.getActivity('uuid');

if (!response.success && response.statusCode === 408) {
  // Request timed out
  showMessage('The request took too long. Please try again.');
}

Connection failure

When fetch() throws (DNS failure, no internet, CORS block), you get:

{
  success: false,
  message: 'Unable to connect to the server',  // or the actual error message
  statusCode: 0,
  action: '/activity/uuid',
  isNetworkError: true
}

Retry pattern

async function fetchWithRetry<T>(fn: () => Promise<T & ApiResponseBase>, maxRetries = 3): Promise<T & ApiResponseBase> {
  let lastResponse: T & ApiResponseBase;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    lastResponse = await fn();

    if (lastResponse.success) return lastResponse;

    // Only retry network errors
    if (!lastResponse.isNetworkError) return lastResponse;

    // Wait before retrying (exponential backoff)
    if (attempt < maxRetries) {
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }

  return lastResponse!;
}

// Usage
const response = await fetchWithRetry(() => api.activity.getActivity('uuid'));

Timeout Configuration

The default timeout is 30 seconds (30000ms). You can configure it globally or rely on the default.

// Short timeout for health checks
const api = new ResytechApi({ timeout: 5000 });

// Check connectivity
const healthy = await api.health.check();
if (!healthy) {
  showBanner('Service is temporarily unavailable.');
}

The timeout is implemented using AbortController.abort() on the fetch request. When a timeout fires, the response has statusCode: 408 and isNetworkError: true.

Debug Mode

Enable debug mode to log all API requests and responses to the console:

const api = new ResytechApi({ debug: true });

Debug output:

  • [ResytechAPI] POST /cart {...} -- logged before every request
  • [ResytechAPI] Response: {...} -- logged after successful responses
  • [ResytechAPI] Error: {...} -- logged for all error responses
  • [ResytechAPI] Health check failed: ... -- logged for health check failures

Toggle at runtime:

api.setDebug(true);   // enable
api.setDebug(false);  // disable

CORS Considerations

The ResytechApi sends requests with credentials: 'include' to support cookie-based sessions. This means:

  1. The API server must return Access-Control-Allow-Origin with your exact origin (not *).
  2. The API server must return Access-Control-Allow-Credentials: true.
  3. The default API hosts (api.bookingui.com and crm-intake.resytech.com) already have CORS configured for standard use.

If you are proxying through your own server or using a custom baseUrl, ensure your server forwards CORS headers correctly.

CORS error symptoms

  • response.isNetworkError === true with statusCode === 0
  • Browser console shows: Access to fetch at '...' from origin '...' has been blocked by CORS policy

Fixing CORS issues

  • Verify your domain is whitelisted in the Resytech dashboard.
  • If using a custom proxy, pass through all Access-Control-* response headers.
  • Do not set mode: 'no-cors' on fetch requests -- this silently drops the response body.

Common Integration Issues

"Location GUID is required" from blog methods

The blog controller requires a location GUID. It is set automatically by initialization.initialize(). If you are using blog methods without initializing first, set it manually:

api.blog.setLocationGuid('your-location-guid');

401 Unauthorized after some time

The auth token obtained from initialization.initialize() expires. Re-initialize to get a fresh token:

const response = await api.initialization.initialize({ identifier: 'session-id' });
if (response.success) {
  // Token is automatically set on all controllers
}

Cart operations fail after page reload

Cart state (including cartId) is not persisted by the library. Store the cartId in sessionStorage or your app state:

const response = await api.cart.createOrUpdateCart({ cart: myCart });
if (response.success) {
  sessionStorage.setItem('cartId', response.cartId);
}

// On subsequent requests
const cartId = sessionStorage.getItem('cartId');
await api.cart.applyCoupon({ couponCode: 'SAVE10', cartId });

Checkout returns clientSecret but no confirmation

This means Stripe requires additional payment authentication (3D Secure). Use the clientSecret with Stripe.js to handle the next action:

const response = await api.checkout.checkout(request);

if (response.success && response.clientSecret) {
  // 3D Secure or additional auth required
  const { error, paymentIntent } = await stripe.handleNextAction({
    clientSecret: response.clientSecret,
  });

  if (!error && paymentIntent.status === 'succeeded') {
    // Retry checkout with handledNextAction
    const finalResponse = await api.checkout.checkout({
      ...request,
      handledNextAction: paymentIntent.id,
    });
  }
}

Dates come back as Date objects, not strings

The ApiController automatically converts ISO date strings in responses to JavaScript Date objects. This applies to fields like firstAvailableDate, publishedAt, createdAt, etc. If you need strings, call .toISOString() on them.

Blog content renders as raw text

If renderer.render() outputs the JSON string instead of HTML, verify the content is valid version 2 block JSON:

const parsed = JSON.parse(post.content);
console.log(parsed.version); // Should be 2
console.log(Array.isArray(parsed.blocks)); // Should be true

If the content is legacy HTML (not JSON), the renderer passes it through as-is. This is expected behavior for older posts.

On this page