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
{
"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:
- Payload format — Confirm all your field formats are correct (dates, phone numbers, etc.)
- Error handling — Send intentionally invalid data to test your error handling code
- Idempotency — Verify that duplicate requests return cached responses
- Rate limit handling — Confirm your code handles
429 responses gracefully
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.
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
- You include an
Idempotency-Key header with every POST request
- RecepAI processes the request and stores the response, keyed to your idempotency key
- 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. |
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:
{
"status": "created",
"guestId": "abc123-def456",
"pmsGuestId": "RES-2026-001",
"roomNumber": "301",
"requestId": "req_a1b2c3d4",
"_idempotent": true
}
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.
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:
# 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 |
Every response includes rate limit headers:
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/1.1 429 Too Many Requests
Retry-After: 45
Content-Type: application/json
{
"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:
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');
}
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).
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.