Skip to main content
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

Total length: 58 characters (10-char prefix vapk_live_ + 48 hex chars from 24 random bytes). The vapk_live_ prefix is intentional and has three purposes:
  1. Format-distinctive: log scrubbers (our error-reporting 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 uses an authenticated session. 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. 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

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: last used at, last used from which address, and last used by which client are recorded on successful verification, throttled and fire-and-forget so a write failure on the bump doesn’t block the request.
  • Expiry: a key can carry an expiry date, checked after the hash compare. An expired key fails authentication like a revoked one.
  • Revocation: revoking flips the key to revoked. Subsequent verification calls return 401 unauthorized even if the secret is otherwise valid.
  • Budgets: a key can carry a monthly call budget with a soft warning line and an optional hard stop. See Assistant governance.
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. Our error-reporting redactor + log scrubbers pattern-match vapk_* prefixes and drop them as a defence-in-depth.

Rotating + revoking keys

Rotate a key from the dashboard. Rotation issues the replacement and ends the old key in a single transaction, inheriting the old key’s scopes verbatim, so a partial failure cannot leave you with two live keys or none.You choose the grace period. Zero revokes the old key immediately. Any positive value instead stamps an expiry on it, capped at 30 days, so you can cut your integration over without an outage.The dashboard surfaces last used, last used from which address, and last used by which client per key, so you can confirm traffic has moved before the old key lapses.
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 usage. lastUsedAt + 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:
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 plus 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.
  • IP allowlisting. No per-key IP allowlist today. The per-IP-webhook rate limit (500 req/min) is the only IP-shape gate. The address a key was last used from is recorded, so unexpected use is visible after the fact.
  • Per-environment key tags beyond live. Sandbox / test keys would slot into the same shape (vapk_test_*) but aren’t issued today.