The Vorel public API uses bearer-token authentication with scoped, per-tenant API keys. Each key is hashed at rest with scrypt; the plaintext is shown to the issuer once at creation and never again.Documentation Index
Fetch the complete documentation index at: https://docs.vorel.ai/llms.txt
Use this file to discover all available pages before exploring further.
Key format
vapk_live_ + 48 hex chars from 24 random bytes).
The vapk_live_ prefix is intentional and serves three purposes:
- Format-distinctive — log scrubbers (Sentry’s
beforeSendredactor, log-shipper rules) can pattern-matchvapk_*and drop these strings before they reach stored events. - Cheap shape-validation — middleware short-circuits malformed tokens (random Slack pastes, OAuth-flow
Bearercarryover) before paying for a hash + DB round-trip. - Indexed lookup prefix — the first 22 chars of every issued key are stored in DB un-hashed (
vapk_live_<12 hex>) so verification is a single indexed query plus an scrypt compare.
live) ships. Future per-environment tags would slot into the same shape (e.g. vapk_test_* for sandbox keys) without breaking the format contract.
Issuing a key
Sign in at app.vorel.ai
The dashboard is Clerk-authed. You need the
admin or owner role on your tenant to issue
keys.Navigate to Settings → Integrations → API keys
The full path:
app.vorel.ai/(dashboard)/settings/integrations/api-keys.Click Create API key
Pick a descriptive name (used in audit logs + the key list view) and select the scopes the
integration needs.
Scopes
Vorel’s API surface is scope-gated. A key with scoperead can call any GET endpoint; mutating
endpoints require the resource-specific write scope.
| Scope | Allows |
|---|---|
read | Every GET endpoint (conversations, leads, appointments, offerings, analytics) |
leads:write | POST /v1/leads, PATCH /v1/leads/:id, POST /v1/leads/:id/handoff |
appointments:write | POST /v1/appointments, PATCH /v1/appointments/:id |
offerings:write | POST /v1/offerings, PATCH /v1/offerings/:id |
conversations:write | POST /v1/conversations, PATCH /v1/conversations/:id, POST /v1/conversations/:id/send |
crm:write | POST /v1/crm/create-record |
403 forbidden — the route doesn’t fall through to a less-privileged behaviour.
A key with read only is the right choice for analytics/digest workflows; issue separate keys
for each integration so revoking one doesn’t take down the others.
Making a request
Authorization header carries the full plaintext secret. We extract the prefix, look up the
hashed row, and run an scrypt compare against the candidate. Constant-time hash compare via
timingSafeEqual so an attacker probing for prefix collisions can’t side-channel a partial match.
Storage + hashing
API key storage and verification:- Hashing algorithm: scrypt (Node’s native
crypto.scrypt), N=2^14, r=8, p=1, 64-byte derived key. ~50 ms verification time on an M-class laptop — slow enough to make offline brute-force infeasible on a stolen DB, fast enough to live in the per-request middleware. - Salt: 16 random bytes per key, stored alongside the hash as
<salt>:<hash>. - Last-used tracking:
lastUsedAtis bumped fire-and-forget on every successful verification so a write failure on the bump doesn’t block the request. - Revocation:
revokeApiKey(id)flipsrevokedAtto the current timestamp. Subsequent verification calls return401 unauthorizedeven if the secret is otherwise valid.
api_keys.key_hash. The plaintext secret is never persisted —
no log line, no metric tag, no error envelope contains it. Sentry’s redactor + log scrubbers
pattern-match vapk_* prefixes and drop them as a defence-in-depth.
Rotating + revoking keys
Rotation
Rotation
The lifecycle is:
- Issue a new key with the same scope set.
- Update your integration to use the new key.
- Verify traffic on the new key (the dashboard surfaces “last used” per key).
- Revoke the old key.
Revocation
Revocation
Revoke from the dashboard or via the admin API. The mutation is idempotent — re-revoking
an already-revoked key is a no-op (no audit log noise, no error).Once revoked, every call carrying the key returns
401 unauthorized with code='unauthorized'
and message='unknown or revoked api key'. The revoked row stays in DB for forensic purposes;
it’s not purged.Suspected compromise
Suspected compromise
If a key has leaked (committed to a public repo, shared in a screenshot, etc.):
- Revoke immediately — first action.
- Audit usage —
lastUsedAt+ the audit log (actor_type='api_key') tells you what the leaked key did during its window. - Reissue with a fresh secret. Update your integration to use it.
- Optional: rotate any downstream credentials the leaked key could have read (CRM credentials, webhook secrets) if you’re being thorough.
Errors
The error envelope is consistent across every authentication failure mode:| HTTP | Code | Message variants |
|---|---|---|
| 401 | unauthorized | missing or malformed Authorization header · unknown or revoked api key |
| 403 | forbidden | key missing required scope '<scope>' |
| 429 | rate_limited | per-key rate limit exceeded (200 req/min — see Rate limits) |
api_v1.verify_failed log lines on every 401 with the request id (no
key plaintext). Operators can investigate via the audit log + log search.
Security properties (in plain language)
- DB compromise alone doesn’t yield working keys. Hashes leak, but the work factor + 24 bytes of randomness make offline guessing infeasible at any reasonable scale.
- Stolen-key amplification is bounded. A leaked key gets its own per-API-key rate-limit bucket (200 req/min); it can’t burn through other tenants’ quotas.
- Timing-safe verification. Hash compare uses
timingSafeEqualagainst equal-length buffers. - Per-issuance audit trail. Every issuance, every revocation, every successful verification
with
lastUsedAtupdates land inaudit_logwith the actor + the key id (never the plaintext).
What’s NOT supported today
- OAuth-style end-customer auth. All API keys are tenant-scoped; we don’t ship an end-customer OAuth flow (“let your customers grant access to your data”). The product model is operator-side.
- Hierarchical / sub-keys. No “child” keys with scoped subsets of the parent’s scope. Issue separate top-level keys per integration.
- Time-bounded keys. Keys don’t auto-expire. Rotate manually per your security policy.
- IP allowlisting. No per-key IP allowlist today. The per-IP-webhook rate limit (500 req/min) is the only IP-shape gate.
- Per-environment key tags beyond
live. Sandbox / test keys would slot into the same shape (vapk_test_*) but aren’t issued today.
Related docs
- API introduction — the high-level surface
- Rate limits — per-key + tenant + IP layered stack
- SDKs —
@vorel/sdktyped client - Security overview — broader posture, RLS, encryption