Skip to main content
Recommendations for building a robust, reliable, and maintainable integration with the CleanLife Partner API.

Retry Strategy

What to Retry

Only retry idempotent operations or those with transient failures:
ScenarioRetry?Strategy
Network timeoutYesExponential backoff
500 INTERNAL_ERRORYesExponential backoff with jitter
429 RATE_LIMIT_EXCEEDEDYesWait 60+ seconds
400 VALIDATION_ERRORNoFix the request first
401 UNAUTHORIZEDNoFix the API key
403 FORBIDDENNoRequest the permission
404 NOT_FOUNDNoThe resource does not exist
409 DUPLICATE_EXTERNAL_REFERENCENoUse a different reference
422 business errorsNoRead the error code and handle

Exponential Backoff

async function retryWithBackoff(fn, maxAttempts = 4) {
  let lastError;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      const status = err.response?.status;

      // Do not retry client errors
      if (status >= 400 && status < 500 && status !== 429) throw err;

      if (attempt < maxAttempts) {
        // Exponential backoff with jitter: 1s, 2s, 4s (+/- 0.5s random)
        const delay = Math.pow(2, attempt - 1) * 1000 + Math.random() * 500;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  throw lastError;
}

Timeouts

Set a reasonable timeout on all HTTP requests. The CleanLife API is not expected to take more than a few seconds, but network conditions vary. Recommended timeout: 30 seconds per request
const axios = require('axios');

const client = axios.create({
  baseURL: 'https://apiv3.thecleanlife.dev/v1',
  timeout: 30_000, // 30 seconds
  headers: { 'x-api-key': process.env.CLEANLIFE_API_KEY },
});

Idempotency (Booking Creation)

Although the Idempotency-Key header is currently disabled, you can achieve safe retries using externalReference:
  1. Before calling POST /partners/bookings, generate a unique reference (e.g., UUID or your internal order ID).
  2. Store this reference in your database with status PENDING.
  3. Call the API with externalReference set to this value.
  4. If the request fails with a network error or timeout:
    • If you receive 409 DUPLICATE_EXTERNAL_REFERENCE, the booking was created successfully — query GET /partners/bookings?externalReference=... to retrieve it.
    • If you receive 422 BOOKING_CREATION_FAILED, the booking failed — retry with the same externalReference.
    • If you receive no response, assume the operation may have succeeded and poll before retrying.
async function createBookingIdempotently(bookingData) {
  const externalReference = bookingData.externalReference; // pre-generated

  try {
    return await client.post('/partners/bookings', bookingData);
  } catch (err) {
    if (err.response?.status === 409 &&
        err.response?.data?.error?.code === 'DUPLICATE_EXTERNAL_REFERENCE') {
      // Booking already exists — fetch it
      const existing = await client.get('/partners/bookings', {
        params: { externalReference }
      });
      return existing.data.data[0]; // first result
    }
    throw err;
  }
}

Logging

Log the following for every API call to enable debugging and auditing:
DataDescription
Request: method + pathe.g., POST /partners/bookings
Request: key fields (not the full body — avoid logging customer PII)e.g., externalReference, serviceId, date
Response: HTTP status code
Response: error.code (if error)
Response: X-Request-IdAlways log this — needed for support escalation
Duration (ms)For latency monitoring
client.interceptors.response.use(
  (response) => {
    console.log({
      method: response.config.method?.toUpperCase(),
      url: response.config.url,
      status: response.status,
      requestId: response.headers['x-request-id'],
      duration: Date.now() - response.config.metadata?.startTime,
    });
    return response;
  },
  (error) => {
    console.error({
      method: error.config?.method?.toUpperCase(),
      url: error.config?.url,
      status: error.response?.status,
      errorCode: error.response?.data?.error?.code,
      requestId: error.response?.headers?.['x-request-id'],
    });
    throw error;
  }
);

Error Handling

Build a structured error handler that distinguishes between error categories:
async function handleCleanLifeError(error: any): Promise<never> {
  const status = error.response?.status;
  const code = error.response?.data?.error?.code;
  const requestId = error.response?.data?.error?.requestId;

  switch (code) {
    case 'DUPLICATE_EXTERNAL_REFERENCE':
      // Recoverable — fetch the existing booking
      throw new DuplicateBookingError(requestId);

    case 'BOOKING_NOT_EDITABLE':
    case 'BOOKING_ALREADY_CANCELLED':
    case 'PAYMENT_ALREADY_CONFIRMED':
      // Terminal — no action needed
      throw new BookingStateError(code, requestId);

    case 'RATE_LIMIT_EXCEEDED':
      // Recoverable — back off
      throw new RateLimitError(requestId);

    case 'INTERNAL_ERROR':
      // Potentially recoverable — retry
      throw new RetryableError(requestId);

    default:
      throw new CleanLifeError(status, code, requestId);
  }
}

Security

  1. Store API Key as a secret, not in code. Use environment variables or a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault, Azure Key Vault).
  2. Store webhook secrets with the same care as API keys. Never log them.
  3. Validate webhook signatures on every delivery. Reject requests that fail validation with 401.
  4. Use HTTPS for all your webhook endpoints. Do not accept webhook deliveries over HTTP.
  5. Restrict your API Key to a specific IP range if possible. Contact CleanLife to configure an IP allowlist.
  6. Rotate secrets periodically — both the API key and webhook secrets.

Performance

Cache the Service Catalog

Services and pricing tiers change infrequently. Cache these responses:
EndpointRecommended TTL
GET /partners/services1 hour
GET /partners/pricing/tiers1 hour

Do Not Cache Timeslots

Available timeslots change in real time as bookings are created. Always query fresh.

Batch Price Calculations

The POST /partners/pricing/calculate-price endpoint accepts up to 10 items in a single request. Use this to calculate prices for multiple service combinations in one call rather than multiple individual calls.

Pagination

Use limit=100 (the maximum) when fetching large lists to minimize the number of API calls.

Webhook Handling

  1. Respond with 200 immediately — do not do slow processing inline.
  2. Use a job queue to process webhook events asynchronously.
  3. Store and deduplicate by deliveryId — events can be delivered more than once.
  4. Build for out-of-order delivery — a booking.appointment_status_changed event may arrive before booking.created in rare retry scenarios.
  5. Monitor for FAILED deliveries — set up alerts to check GET /partners/webhooks/deliveries?status=FAILED periodically.

Testing

  1. Use a distinct externalReference prefix for test bookings (e.g., TEST-) to identify them in production logs.
  2. Test cancellation, update, and payment confirmation flows end-to-end before going live.
  3. Test your webhook signature verification with a known payload before deployment.
  4. Simulate webhook failures by temporarily stopping your webhook handler to verify the retry behavior.