Authentication
API key types, scopes, origin allowlists, rotation, and how to read rate-limit and quota headers.
Every request to https://wohno.de/api/v1/* is authenticated with an API key
sent in the X-API-Key header. There are two key types — choosing the right one
is the most important authentication decision you make.
Publishable vs. secret keys
| Prefix | Type | Use where | Allowed scopes |
|---|---|---|---|
sk_live_* | Secret | Server-to-server | Any scope (incl. write / delete / *) |
pk_live_* | Publishable | Browser / embed | Read-only whitelist (see below) |
sk_test_* / pk_test_* exist for sandbox setups.
When to use which
- Secret (
sk_) — anywhere the key can stay secret: backends, cron jobs, CRM sync, server-to-server integrations. It may read, write and delete. A secret key is stored only as a bcrypt hash and returned in cleartext exactly once — copy it at creation time. - Publishable (
pk_) — anywhere the key lands in a browser: embed widgets, public HTML, client-side JS. It may only carry the read-only whitelist and must have an origin allowlist. Publishable keys are deliberately browser-safe: their security comes from the scope whitelist plus the origin allowlist, not from secrecy.
Rule of thumb: if a value ever reaches a browser, it must be a
pk_key. Never put a secret key in a frontend bundle.
Scopes
Scopes follow the resource:action format (e.g. listings:read) and are
hierarchical:
*grants full access.resource:*grants all actions on that resource.deleteimplieswrite, andwriteimpliesread.
A missing scope returns 403 INSUFFICIENT_SCOPE. The full scope registry lives
in the conventions reference.
Publishable whitelist
A pk_ key may carry only these read scopes:
organizations:readlistings:readembed:readappointments:readappointments:book
This is enforced twice (defense-in-depth): in the application layer and by a database trigger.
Origin and IP allowlists
- Origin allowlist is required for
pk_keys and optional forsk_keys.- Subdomain wildcards like
https://*.example.commatch exactly one subdomain level. - HTTPS is mandatory (except
http://localhost). - Browsers send the
Originheader automatically. If it is missing on apk_call, the request is malformed and returns403 ORIGIN_REQUIRED. - An origin that matches no allowlist entry returns
403 ORIGIN_NOT_ALLOWED.
- Subdomain wildcards like
- IP allowlist only makes sense for
sk_keys (IPv4 + IPv4 CIDR, max. 10 entries). Recommended for production server-to-server keys. A request from an IP outside the list returns403 IP_NOT_ALLOWED.
Key rotation
Rotate a key without downtime using an overlap period:
- A new key is created with the same profile (type, scopes, allowlists); its raw value is returned once.
- The old key gets a
rotation_expires_at = now() + overlapDays(default 7 days, 1–30 allowed). - Both keys validate during the overlap window — deploy the new key, then retire the old one.
- After the window, the old key returns
401 KEY_ROTATED_OUTand is revoked by a nightly cleanup job.
Recommendation: rotate every 90 days; a 7-day overlap is enough for one deploy.
Rate-limit and quota headers
Every /api/v1/* response carries informative headers so you can self-throttle.
Rate limits (per key, 1000 req/h)
| Header | Meaning | Example |
|---|---|---|
X-RateLimit-Limit | Max requests in the window | 1000 |
X-RateLimit-Remaining | Requests left in the current window | 950 |
X-RateLimit-Reset | ISO time when the window resets | 2026-06-03T11:00:00.000Z |
Retry-After | Seconds until the next allowed request (429 only) | 42 |
Exceeding the limit returns 429 RATE_LIMITED. Use exponential backoff and
respect Retry-After.
Quotas (per organization, monthly)
| Header | Meaning | Example |
|---|---|---|
X-Quota-Limit | Monthly plan limit or unlimited | 10000 |
X-Quota-Used | Current monthly counter or unknown | 3450 |
X-Quota-Reset | ISO time of the monthly reset (Europe/Berlin) | 2026-07-01T00:00:00.000Z |
X-Quota-Status | ok / soft-warning / unavailable | soft-warning |
soft-warning is set at ≥ 80 % usage. Quotas are tracking-only by default; once
enforced, an overage returns 429 QUOTA_EXCEEDED.
Authentication errors
| HTTP | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | X-API-Key header missing entirely |
| 401 | INVALID_API_KEY | Key unknown, revoked or expired |
| 401 | KEY_ROTATED_OUT | Rotated key used after its overlap window |
| 403 | INSUFFICIENT_SCOPE | Key lacks the required scope |
| 403 | ORIGIN_REQUIRED | pk_ key called without an Origin header |
| 403 | ORIGIN_NOT_ALLOWED | Origin not in the key's allowlist |
| 403 | IP_NOT_ALLOWED | Client IP not in the key's allowlist |
Branch on the stable error.code string, never on the human-readable message.
Reference
For the canonical, code-verified details on authentication, scopes, error formats and headers, see the API conventions reference.