All Vennio API errors follow RFC 9457 Problem Detail format:
{
"type": "https://api.vennio.app/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Email is required",
"request_id": "req_abc123",
"errors": {
"customer_email": "Required field missing"
}
}
| Field | Description |
|---|---|
type |
URI identifying the error type — stable across API versions |
title |
Short human-readable summary |
status |
HTTP status code |
detail |
Specific explanation for this error instance |
request_id |
Trace ID — include this when contacting support |
errors |
Field-level validation errors (400 only) |
| Status | Meaning | Retryable? |
|---|---|---|
| 400 | Bad request — invalid or missing parameters | No — fix the request |
| 401 | Unauthorized — missing or invalid API key | No — check your key |
| 403 | Forbidden — API key lacks required scope | No — use a key with correct scopes |
| 404 | Not found — resource doesn't exist, or calendar not connected | No |
| 409 | Conflict — slot no longer available | No — re-query availability |
| 422 | Unprocessable — valid request that cannot be executed (e.g. past time) | No — fix the request |
| 429 | Too many requests — rate limit exceeded | Yes — wait for reset |
| 500 | Internal server error | Yes — retry with backoff |
| 503 | Service unavailable | Yes — retry with backoff |
{
"type": "https://api.vennio.app/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Missing or invalid API key",
"request_id": "req_abc123"
}
Fix: Check that your Authorization: Bearer vennio_sk_live_... header is present and the key is active. Verify at vennio.app/api-keys.
{
"type": "https://api.vennio.app/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "This API key doesn't have permission for this action",
"request_id": "req_abc123",
"key_type": "publishable",
"available_scopes": ["read:availability", "create:booking"]
}
Fix: Publishable keys (vennio_pk_live_*) are limited to read:availability and create:booking. Use a secret key for all other operations.
{
"type": "https://api.vennio.app/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Invalid request parameters",
"request_id": "req_abc123",
"errors": {
"customer_email": "Invalid email format",
"start_time": "Must be a future time"
}
}
Fix: The errors object maps field names to specific validation messages. Fix each field before retrying.
{
"type": "https://api.vennio.app/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "No connected calendar found for this business. Connect a calendar first.",
"request_id": "req_abc123"
}
Fix: The business account needs at least one connected calendar. Direct the user to connect Google or Microsoft via the OAuth flow. See Integration Patterns.
{
"type": "https://api.vennio.app/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "The requested time slot is no longer available",
"request_id": "req_abc123"
}
Fix: The slot was booked by someone else between your availability query and booking attempt. Re-query GET /v1/availability/slots and present updated options. Do not retry the same time — it will keep failing.
{
"type": "https://api.vennio.app/errors/unprocessable",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Booking start_time must be in the future",
"request_id": "req_abc123"
}
Fix: Ensure start_time is at least a few minutes in the future. Account for clock skew between your server and the API.
{
"type": "https://api.vennio.app/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 2026-04-11T10:15:00Z",
"retry_after": "2026-04-11T10:15:00Z",
"request_id": "req_abc123"
}
Fix: Wait until retry_after before retrying. Check the rate limit headers on every response:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2026-04-11T10:15:00.000Z
| Plan | Limit |
|---|---|
| Free | 100 requests / hour |
| Premium | 1,000 requests / hour |
| Enterprise | Custom |
Rate limits apply per API key.
Retry 429 and 5xx errors with exponential backoff. Never retry 4xx errors — they indicate problems with the request itself.
async function callWithRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn()
} catch (err) {
const status = err.status || err.response?.status
if (status === 429 || status >= 500) {
// Honor retry_after if provided
const retryAfter = err.response?.data?.retry_after
const delay = retryAfter
? new Date(retryAfter) - Date.now()
: Math.pow(2, attempt) * 1000 + Math.random() * 500
await new Promise(r => setTimeout(r, Math.max(delay, 0)))
continue
}
throw err // Don't retry 4xx
}
}
throw new Error('Max retries exceeded')
}
For 409 Conflict, do not retry the same request — the slot is unavailable. Re-query availability and let the user choose a new time.
For POST requests that create resources, pass a unique Idempotency-Key header to safely retry on network failures without creating duplicates:
curl -X POST https://api.vennio.app/v1/bookings \
-H "Authorization: Bearer $VENNIO_API_KEY" \
-H "Idempotency-Key: booking-$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ ... }'
Duplicate requests with the same key return the original response within 24 hours.
Include the request_id from the error response when contacting support — it lets us trace exactly what happened server-side.