Skip to main content

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.

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.

Key format

vapk_live_<48 hex characters>
Total length: 58 characters (10-char prefix vapk_live_ + 48 hex chars from 24 random bytes). The vapk_live_ prefix is intentional and serves three purposes:
  1. Format-distinctive — log scrubbers (Sentry’s beforeSend redactor, log-shipper rules) can pattern-match vapk_* and drop these strings before they reach stored events.
  2. Cheap shape-validation — middleware short-circuits malformed tokens (random Slack pastes, OAuth-flow Bearer carryover) before paying for a hash + DB round-trip.
  3. 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.
Today only one environment tag (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

1

Sign in at app.vorel.ai

The dashboard is Clerk-authed. You need the admin or owner role on your tenant to issue keys.
2

Navigate to Settings → Integrations → API keys

The full path: app.vorel.ai/(dashboard)/settings/integrations/api-keys.
3

Click Create API key

Pick a descriptive name (used in audit logs + the key list view) and select the scopes the integration needs.
4

Copy the key — once

The plaintext secret is shown ONCE. Subsequent reads see only the prefix + last 4 characters. Store it in your secret manager immediately. If you lose it, revoke + reissue.
The audit log records every issuance with the actor’s user id, the requested scopes, and the resulting key id. “Who created this key, when, with what scopes?” is always answerable.

Scopes

Vorel’s API surface is scope-gated. A key with scope read can call any GET endpoint; mutating endpoints require the resource-specific write scope.
ScopeAllows
readEvery GET endpoint (conversations, leads, appointments, offerings, analytics)
leads:writePOST /v1/leads, PATCH /v1/leads/:id, POST /v1/leads/:id/handoff
appointments:writePOST /v1/appointments, PATCH /v1/appointments/:id
offerings:writePOST /v1/offerings, PATCH /v1/offerings/:id
conversations:writePOST /v1/conversations, PATCH /v1/conversations/:id, POST /v1/conversations/:id/send
crm:writePOST /v1/crm/create-record
Issue keys with the minimum scope set the integration needs. A key without the required scope returns 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

curl https://app.vorel.ai/api/v1/conversations \
  -H "Authorization: Bearer vapk_live_..." \
  -H "Content-Type: application/json"
The 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: lastUsedAt is bumped fire-and-forget on every successful verification so a write failure on the bump doesn’t block the request.
  • Revocation: revokeApiKey(id) flips revokedAt to the current timestamp. Subsequent verification calls return 401 unauthorized even if the secret is otherwise valid.
The hash + salt are stored in 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

The lifecycle is:
  1. Issue a new key with the same scope set.
  2. Update your integration to use the new key.
  3. Verify traffic on the new key (the dashboard surfaces “last used” per key).
  4. Revoke the old key.
There’s no in-place rotation API today — issue + cutover + revoke is the canonical flow. The “issue” + “revoke” pair are the only mutations on api_keys.
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.
If a key has leaked (committed to a public repo, shared in a screenshot, etc.):
  1. Revoke immediately — first action.
  2. Audit usagelastUsedAt + the audit log (actor_type='api_key') tells you what the leaked key did during its window.
  3. Reissue with a fresh secret. Update your integration to use it.
  4. Optional: rotate any downstream credentials the leaked key could have read (CRM credentials, webhook secrets) if you’re being thorough.
Revoke first, audit second — minimising the attacker’s window matters more than perfect forensics.

Errors

The error envelope is consistent across every authentication failure mode:
{
  "error": {
    "code": "unauthorized",
    "message": "<context-specific message>"
  }
}
HTTPCodeMessage variants
401unauthorizedmissing or malformed Authorization header · unknown or revoked api key
403forbiddenkey missing required scope '<scope>'
429rate_limitedper-key rate limit exceeded (200 req/min — see Rate limits)
The middleware emits 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 timingSafeEqual against equal-length buffers.
  • Per-issuance audit trail. Every issuance, every revocation, every successful verification with lastUsedAt updates land in audit_log with 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.