> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thecleanlife.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> Retry strategy, timeouts, idempotency, logging, security, and performance.

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:

| Scenario                           | Retry? | Strategy                        |
| ---------------------------------- | ------ | ------------------------------- |
| Network timeout                    | Yes    | Exponential backoff             |
| `500 INTERNAL_ERROR`               | Yes    | Exponential backoff with jitter |
| `429 RATE_LIMIT_EXCEEDED`          | Yes    | Wait 60+ seconds                |
| `400 VALIDATION_ERROR`             | No     | Fix the request first           |
| `401 UNAUTHORIZED`                 | No     | Fix the API key                 |
| `403 FORBIDDEN`                    | No     | Request the permission          |
| `404 NOT_FOUND`                    | No     | The resource does not exist     |
| `409 DUPLICATE_EXTERNAL_REFERENCE` | No     | Use a different reference       |
| `422` business errors              | No     | Read the error code and handle  |

### Exponential Backoff

```javascript theme={null}
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

```javascript theme={null}
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 — use the `bookingId` you stored from the original response, or treat the conflict as confirmation that creation succeeded.
   * If you receive `422 BOOKING_CREATION_FAILED`, the booking failed — retry with the same `externalReference`.
   * If you receive no response, retry only if you have not yet received a `409`; otherwise poll `GET /partners/bookings/:bookingId/status` using a stored `bookingId`.

```javascript theme={null}
async function createBookingIdempotently(bookingData) {
  const externalReference = bookingData.externalReference; // pre-generated

  try {
    const response = await client.post('/partners/bookings', bookingData);
    await db.orders.save({ externalReference, bookingId: response.data.bookingId });
    return response.data;
  } catch (err) {
    if (err.response?.status === 409 &&
        err.response?.data?.error?.code === 'DUPLICATE_EXTERNAL_REFERENCE') {
      const existing = await db.orders.findByExternalReference(externalReference);
      if (existing?.bookingId) {
        return client.get(`/partners/bookings/${existing.bookingId}/status`);
      }
      throw new Error('Booking exists but bookingId was not stored');
    }
    throw err;
  }
}
```

***

## Logging

Log the following for every API call to enable debugging and auditing:

| Data                                                                 | Description                                     |
| -------------------------------------------------------------------- | ----------------------------------------------- |
| Request: method + path                                               | e.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-Id`                                             | Always log this — needed for support escalation |
| Duration (ms)                                                        | For latency monitoring                          |

```javascript theme={null}
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:

```typescript theme={null}
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_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. **Use HTTPS** for all API calls.

3. **Restrict your API Key** to a specific IP range if possible. Contact CleanLife to configure an IP allowlist.

4. **Rotate your API key periodically.**

***

## Performance

### Cache the Service Catalog

Services change infrequently. Cache this response:

| Endpoint                               | Recommended TTL | Notes                                                          |
| -------------------------------------- | --------------- | -------------------------------------------------------------- |
| `GET /partners/services?addressId=...` | 15–60 min       | Cache per address; response is categories with nested services |

### Do Not Cache Timeslots

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

### Pagination

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

***

## Testing

1. Use a distinct `externalReference` prefix for test bookings (e.g., `TEST-`) to identify them in production logs.
2. Test cancellation and payment confirmation flows end-to-end before going live.
