Responses use camelCase resource fields; pagination/envelope keys like next_cursor stay snake_case. Request bodies accept camelCase (and snake_case is also tolerated). Organization and application scoping is derived from the API key.
Authentication
Create an application-scoped API key in the panel. Send it as a Bearer token on every API request, or as an `X-Api-Key` header if a Bearer token is inconvenient. API keys carry scopes, an application_id, and an organization_id, so the organization and application scope of every call is derived from the key — you never pass them explicitly.
Keys are shown once at creation. Store them server-side only and rotate from the same application that serves production traffic.
curl https://api.hora.to/v1/usage \
-H "Authorization: Bearer $HORATO_API_KEY"
# Equivalent, using the header form:
curl https://api.hora.to/v1/usage \
-H "X-Api-Key: $HORATO_API_KEY"Connector authorization flow
Start an OAuth authorization, redirect the user, then let Horato store the connection credentials. Use capability endpoints before attempting actions that depend on the connected account.
curl -X POST https://api.hora.to/v1/auth/connectors/{provider}/authorize \
-H "Authorization: Bearer $HORATO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scopes":["email.read","calendar.write"],"redirect_uri":"https://app.example.com/oauth/callback"}'Read and mutate resources
Prefer canonical IDs for stored resources and preserve external IDs for reconciliation. Mutating calls should include etags when the resource surface exposes conflict control.
curl "https://api.hora.to/v1/email/messages?connection_id=conn_123&limit=25" \
-H "Authorization: Bearer $HORATO_API_KEY"curl -X POST https://api.hora.to/v1/calendars/cal_primary/events \
-H "Authorization: Bearer $HORATO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"connection_id":"conn_123","title":"Intro call","start":"2026-06-10T16:00:00Z","end":"2026-06-10T16:30:00Z"}'Pagination and idempotency
List endpoints accept `limit` and `cursor` query parameters and return `next_cursor` when more results exist. Page by passing the previous response's `next_cursor` back as `cursor`; a null `next_cursor` means you have reached the end.
Write endpoints accept an `Idempotency-Key` header for retry-safe creation. Reusing the same key with the same request returns the original result instead of creating a duplicate, so a client can safely retry after a network failure.
curl "https://api.hora.to/v1/email/messages?connection_id=conn_123&limit=50&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $HORATO_API_KEY"curl -X POST https://api.hora.to/v1/scheduling/bookings \
-H "Authorization: Bearer $HORATO_API_KEY" \
-H "Idempotency-Key: booking-2026-06-10-abc" \
-H "Content-Type: application/json" \
-d '{"eventTypeId":"evt_123","start":"2026-06-10T16:00:00Z","inviteeEmail":"dana@example.com"}'Rate limits
The API is throttled per API key. Every response carries the current budget so you can pace requests without guessing.
The default budget is 1,000 requests per minute per key. When you exceed it, the API returns `429` with a `rate_limited` error code and a `Retry-After` header (seconds). The limiter fails open on its own infrastructure errors, so a limiter outage never blocks your traffic — only a confirmed over-limit is rejected.
- `x-ratelimit-limit`: the per-minute ceiling for the key.
- `x-ratelimit-remaining`: requests left in the current window.
- On `429`, wait for `Retry-After` seconds, then retry with backoff.
HTTP/1.1 200 OK
x-ratelimit-limit: 1000
x-ratelimit-remaining: 984
x-request-id: req_1a2b3cAPI key security
Harden a key by restricting where it can be used. A key can carry an allowlist of domains (matched against the request Origin/Referer) and an allowlist of IP addresses or CIDR ranges. A request from outside the allowlist is rejected with `forbidden` before any work runs.
Combine allowlists with least-privilege scopes: issue a narrow, domain-locked key for browser-adjacent use and a separate server key for backend workers.
- Domain allowlist: exact hosts, e.g. `app.example.com`.
- IP allowlist: individual addresses or CIDR ranges, e.g. `203.0.113.10` or `203.0.113.0/24`.
- An empty allowlist means no restriction on that dimension.