For the complete documentation index, see llms.txt.

Rate Limits & Errors

Error response format

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)

HTTP status codes

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

Common errors by scenario

Invalid or missing API key (401)

{
  "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.


Insufficient scope (403)

{
  "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.


Validation error (400)

{
  "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.


Calendar not connected (404)

{
  "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.


Slot conflict (409)

{
  "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.


Past time (422)

{
  "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.


Rate limit exceeded (429)

{
  "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

Rate limits

Plan Limit
Free 100 requests / hour
Premium 1,000 requests / hour
Enterprise Custom

Rate limits apply per API key.


Retry logic

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')
}
409 conflicts need special handling

For 409 Conflict, do not retry the same request — the slot is unavailable. Re-query availability and let the user choose a new time.


Idempotency

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.


Getting help

Include the request_id from the error response when contacting support — it lets us trace exactly what happened server-side.