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

# Testing & Rate Limits

> Sandbox mode, idempotency keys, and rate limits.

## Sandbox Mode

Use a test API key (`pms_test_` prefix) to validate your integration without writing any data to the hotel's system.

When the hotel admin generates an API key, they receive **both a live key and a test key** as a pair — just like Stripe. Ask the hotel admin for the test key to start development.

Test keys run the **exact same validation** as live keys — the only difference is that no data is persisted.

| Behavior                    | Live (`pms_live_`) | Test (`pms_test_`)    |
| --------------------------- | ------------------ | --------------------- |
| Payload validation          | Full validation    | Full validation       |
| Data written to database    | Yes                | **No**                |
| Real-time dashboard updates | Yes                | **No**                |
| Response format             | Real IDs           | Fake IDs (`test_xxx`) |
| Rate limits                 | Normal             | Same limits           |
| Request logging             | Yes                | Yes (marked as test)  |

### Test Response Example

```json theme={null}
{
  "status": "created",
  "guestId": "test_abc123",
  "pmsGuestId": "RES-2026-001",
  "roomNumber": "301",
  "requestId": "req_test_a1b2",
  "_sandbox": true
}
```

The `_sandbox: true` flag confirms you're in test mode and no data was written. This flag is never present in live responses.

### What to Test

Use sandbox mode to verify:

1. **Payload format** — Confirm all your field formats are correct (dates, phone numbers, etc.)
2. **Error handling** — Send intentionally invalid data to test your error handling code
3. **Idempotency** — Verify that duplicate requests return cached responses
4. **Rate limit handling** — Confirm your code handles `429` responses gracefully

<Tip>
  **Recommended workflow:** Develop and test with a `pms_test_` key first. Once your integration passes all test cases, switch to the `pms_live_` key for production. The only change needed is the API key — all endpoints and request formats are identical.
</Tip>

## Idempotency

All POST endpoints require an `Idempotency-Key` header. This prevents duplicate operations when network issues cause retries — for example, if your system sends a check-in request but doesn't receive the response due to a timeout, you can safely resend the same request.

### How It Works

1. You include an `Idempotency-Key` header with every POST request
2. RecepAI processes the request and stores the response, keyed to your idempotency key
3. If you send another request with the **same key** within 24 hours, RecepAI returns the cached response **without re-processing**

### Rules

| Rule         | Detail                                                                                                                                               |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Required** | All POST endpoints require this header. Omitting it returns `400`.                                                                                   |
| **Length**   | 1–255 characters                                                                                                                                     |
| **Scope**    | Keys are scoped to your API key. Different hotels can use the same idempotency key safely.                                                           |
| **TTL**      | Keys expire after **24 hours**. After that, the same key can be reused.                                                                              |
| **Caching**  | Only **2xx responses** are cached. If a request fails with 4xx, the key is NOT consumed — you can retry with a corrected payload using the same key. |

### Recommended Key Format

Use a structured format that's unique per operation:

```
{operation}_{pmsGuestId}_{date}
```

**Examples:**

| Operation                        | Idempotency Key               |
| -------------------------------- | ----------------------------- |
| Check in guest RES-001 on Feb 18 | `checkin_RES001_20260218`     |
| Update guest RES-001 room change | `update_RES001_room_20260218` |
| Check out guest RES-001          | `checkout_RES001_20260218`    |
| Morning sync on Feb 19           | `sync_20260219_morning`       |

### Duplicate Response

When a cached response is returned, it includes an `_idempotent` flag:

```json theme={null}
{
  "status": "created",
  "guestId": "abc123-def456",
  "pmsGuestId": "RES-2026-001",
  "roomNumber": "301",
  "requestId": "req_a1b2c3d4",
  "_idempotent": true
}
```

<Info>
  The `_idempotent: true` flag tells you this is a cached replay, not a new operation. The response body, HTTP status, and `requestId` are all identical to the original response.
</Info>

### Important: Failed Requests Don't Consume Keys

If your request returns a **4xx error** (validation failure, room conflict, etc.), the idempotency key is **not consumed**. This means you can fix the issue and retry with the **same key**:

```bash theme={null}
# First attempt — has a date format error
curl -X POST .../guests/checkin \
  -H "Idempotency-Key: checkin_RES001_20260218" \
  -d '{"checkOutDate": "18.02.2026"}'  # Wrong format!
# Returns 422 INVALID_DATE_FORMAT — key NOT consumed

# Second attempt — fixed payload, same key
curl -X POST .../guests/checkin \
  -H "Idempotency-Key: checkin_RES001_20260218" \
  -d '{"checkOutDate": "2026-02-18"}'  # Correct format
# Returns 200 — key now consumed with this response
```

## Rate Limits

Each endpoint has its own rate limit. Limits are per API key (not per hotel or per IP).

### Limits by Endpoint

| Endpoint                | Limit                |
| ----------------------- | -------------------- |
| `POST /guests/checkin`  | 60 requests / minute |
| `POST /guests/update`   | 60 requests / minute |
| `POST /guests/checkout` | 60 requests / minute |
| `GET /guests`           | 30 requests / minute |
| `POST /guests/sync`     | 10 requests / hour   |

### Response Headers

Every response includes rate limit headers:

```http theme={null}
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1708180260
```

| Header                  | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window       |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets          |

### Rate Limited Response (429)

When you exceed the limit:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 45
Content-Type: application/json
```

```json theme={null}
{
  "status": "error",
  "code": "RATE_LIMITED",
  "message": "Rate limit exceeded. Retry after 45 seconds.",
  "retryAfter": 45,
  "requestId": "req_u1v2w3x4"
}
```

### Handling Rate Limits

Implement exponential backoff in your integration:

```javascript theme={null}
async function callWithRetry(requestFn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await requestFn();

    if (response.status !== 429) {
      return response;
    }

    // Use the server's Retry-After value
    const data = await response.json();
    const waitSeconds = data.retryAfter || Math.pow(2, attempt);
    console.log(`Rate limited. Waiting ${waitSeconds}s before retry...`);
    await new Promise(r => setTimeout(r, waitSeconds * 1000));
  }

  throw new Error('Max retries exceeded');
}
```

<Tip>
  **For the sync endpoint (10/hour):** Schedule your daily sync at a fixed time (e.g., 08:00). One sync per day is sufficient. For real-time updates throughout the day, use the individual check-in, update, and checkout endpoints which have much higher limits (60/minute).
</Tip>

## Request Logging

Every API request is logged — including test mode requests. Logs include:

* Full request payload
* Response status and body
* IP address and User-Agent
* Processing duration
* Idempotency key (if provided)

Logs are retained for **90 days** and are accessible to RecepAI support for debugging. When contacting support, always include the `requestId` from the response.

## Security Notes

* All API traffic is encrypted via **HTTPS** (TLS 1.2+). HTTP requests are rejected.
* API keys are stored as **SHA-256 hashes** — the full key is never stored on our servers.
* PMS API paths are exempt from CSRF middleware (machine-to-machine authentication, no browser cookies).
* All guest changes from the PMS are recorded in an **activity log** with the actor type `pms`, visible alongside staff actions on the Front Desk.
