Security & data handling
What happens to the data you send us, where it lives, how long it sticks around, and what we never do with it. Written for the engineer who has to answer their legal team's questions.
Overview
When you call POST /v1/ask, your prompt is forwarded to the LLM providers you selected (OpenAI, Anthropic, Google, Perplexity), the normalized response is returned to you, a copy is written to a short-term retrieval archive so you can fetch it via GET /v1/ask/:id for up to 30 days, and a deduplicated cache entry may be stored so identical follow-up calls are cheap and fast. Nothing is sold. Nothing is used to train models. You can revoke an API key at any time.
Where your prompts live
A single /v1/ask call can touch up to three storage locations, all encrypted in transit over TLS and encrypted at rest by the underlying managed service:
- Cloudflare Cache API (L1, per-colo) — the response payload, keyed by a SHA-256 digest of the normalized prompt plus provider list plus params. TTL: 24 hours. Managed by Cloudflare, not queryable by us except through the normal cache-read path.
- Cloudflare KV (L2, global) — the same response payload under the key
ask:<sha256>. TTL: 24 hours. Expires automatically. - Supabase Postgres (
response_archive) — the full normalized response keyed byrequest_id, scoped to youraccount_id. This is what backsGET /v1/ask/:id. Retention: 30 days. Row Level Security on this table restricts dashboard reads to the owning account only.
Access control: the MentionsAPI Worker talks to Supabase over a service-role credential; Row Level Security policies on dashboard tables restrict reads to the authenticated account via its JWT. Transport to every LLM provider is HTTPS. Transport between the Worker and our provider brokers is HTTPS plus a shared-secret header compared in constant time.
Response retention
Every /v1/ask response is archived for 30 days so you can retrieve it later via GET /v1/ask/:id. After that window it's gone.
- A daily cron at 04:00 UTC deletes every
response_archiverow whosecreated_atis older than 30 days. - The
GET /v1/ask/:idhandler also enforces the TTL at read time — if a row survives past the window for any reason, the endpoint returns410 Gonewith{ error: { code: "expired", retention_days: 30 } }. - Once the row is deleted, the prompt text and the normalized provider payload are no longer recoverable from the archive. Cache entries (L1/L2, 24-hour TTL) may still exist separately; those are content-addressed by SHA-256 and never contain your
account_id.
Cleanup runs daily at 04:00 UTC and is enforced at read time by the GET /v1/ask/:id handler, which returns 410 expired for rows past 30 days regardless of cron-timing.
Is my data used to train models?
No — not by us, and not by default at the upstream providers.
- MentionsAPI never trains on your prompts or responses. We don't run models. The only reason we store responses is so you can fetch them back within the 30-day retrieval window.
- Upstream providers receive your prompt subject to their API terms. We call OpenAI, Anthropic, Google, and Perplexity through their paid API endpoints. Each of those providers publishes a default-no-training policy for paid API traffic (as of April 2026). We don't opt you into any training or sharing toggle on your behalf. For legal precision, treat each provider's current API data policy as authoritative — we simply pass your request through.
- The cache and archive are for retrieval, not modeling. Cache entries are content-addressed (SHA-256 of the normalized prompt) and accessed only when an identical request comes in; the archive is indexed by
request_idand scoped to your account.
API key storage
API keys are stored as SHA-256 hashes — we never keep the plaintext token after you receive it.
- A fresh key has 32 bytes (256 bits) of entropy from
crypto.getRandomValues()and is formatted aslvk_live_<secret>(orlvk_test_…). - On creation we store only the first 12 characters of the key (
key_prefix, used for O(1) lookup) andkey_hash = SHA-256(token)as lowercase hex. The plaintext is returned to you once and only once at creation time and never written to any log. - At request time the Worker re-hashes the presented bearer token and compares it against the stored hash in constant time.
- The dashboard displays only the 12-character prefix plus the key's name and last-used timestamp. We cannot retrieve a lost key — you revoke and mint a new one.
- Revocation is instant: it flips
statuson the DB row, invalidates the 60-second KV auth cache for that prefix, and further requests with that token return 401.
Note: we hash with SHA-256 rather than a slow password hash because the secret is a 256-bit random token, not a user-chosen password. Moving to argon2id behind a Supabase Edge Function is on the roadmap for enterprise deployments.
LLM provider key isolation
This is the part most people get wrong. Our MentionsAPI Worker holds zero LLM provider keys. Not OpenAI, not Anthropic, not Google, not Perplexity.
Every provider call goes through a dedicated Supabase Edge Function broker whose secrets are scoped to that one function:
Your request
│
▼
Cloudflare Worker (secrets: Supabase URL + service role + shared broker secret)
│ (NO OpenAI / Anthropic / Google / Perplexity keys on this edge)
│ HTTPS + x-mentionsapi-worker (constant-time compared)
▼
Supabase Edge Function broker (one per provider)
│ openai-proxy ─ holds OPENAI_API_KEY only
│ anthropic-proxy ─ holds ANTHROPIC_API_KEY only
│ gemini-proxy ─ holds GOOGLE_GEMINI_API_KEY only
│ perplexity-proxy─ holds PERPLEXITY_API_KEY only
▼
Provider (OpenAI / Anthropic / Google / Perplexity)What each broker enforces before the key is ever used:
- Shared-secret authentication on the
x-mentionsapi-workerheader, constant-time compared. Missing or wrong secret returns 401 before any provider call. - A strict per-provider model allowlist — text-generation models only. Image, audio, video, vision, embedding, TTS, and moderation model names are rejected with HTTP 403 by pattern match as a second line of defense.
- API-key pattern scrubbing across all providers in every log and error response, so a cross-contaminated error cannot leak a key.
- Prompt length cap (20 KB) and max output token cap.
- Output field allowlist — the broker returns only
{ model, content, citations, tokens }.
Practical consequence: if the Worker is ever compromised, an attacker still cannot exfiltrate a provider key, and cannot call an image-generation model — the two most expensive failure modes for an AI gateway.
Caching behavior
Our cache is content-addressed. Keys are SHA-256(normalized_prompt | providers | params) where normalization lowercases, collapses whitespace, strips trailing punctuation, sorts the provider list, and JSON-encodes params with sorted keys.
- Two tiers. L1 is Cloudflare's per-colo Cache API (single-digit ms reads, 24-hour TTL). L2 is Cloudflare KV (global, 24-hour TTL). An L2 hit is asynchronously promoted back to L1.
- Shared vs. private scope. Default scope is
shared— identical prompt-plus-provider-plus-params tuples dedupe across customers, because the underlying provider response is the same either way. Pass"cache_scope": "private"to namespace the key with youraccount_idso your cached entries are isolated to your account. - Cheaper cached calls. Cache hits cost less than fresh calls (that's the whole point). The per-provider upstream cost drops to zero because we don't re-invoke the LLM; you still pay a small serving fee. See Caching for the full economics.
- Opt out of shared caching. Set
"cache_scope": "private"so your cached entries are isolated to your account. If you need stronger guarantees (e.g. a compliance requirement that prompts never share a cache line with any other customer), this is the standard answer; contact us for a fully dedicated cache layer on Enterprise. - Cache entries never contain your
account_id,request_id, or API key material — they're keyed by the SHA-256 digest of the normalized inputs.
Rate limiting & abuse prevention
Every API key has a token-bucket rate limit enforced per-tier at the edge in a Cloudflare Durable Object, plus a per-account credit balance that gates fanout calls. See Rate limits for per-tier numbers and how to handle 429s.
Transport security
- Public API endpoint:
https://api.mentionsapi.com. HTTPS only. TLS is managed by Cloudflare at the edge (modern cipher suites; TLS 1.3 supported, TLS 1.2 is the floor). - Dashboard:
https://mentionsapi.com. HTTPS only, same edge. - Internal: Worker-to-broker calls go over HTTPS with an additional constant-time shared-secret check. Broker-to-provider calls go over HTTPS to the provider's published API endpoint.
Compliance posture
- GDPR. Our infrastructure providers — Cloudflare and Supabase — both publish GDPR-compliant DPAs and operate EU data regions. We act as a processor of your prompts and responses while they're in flight and in the 30-day retrieval archive. If you need a DPA or a sub-processor disclosure letter for your legal team, email [email protected].
- SOC 2. Not yet certified. This is on the roadmap for the enterprise tier. We're documenting controls now and plan to pursue a Type I report once the enterprise tier has its first design partners. In the meantime we can share architecture documentation, incident runbooks, and access-review practices under NDA — email [email protected].
- HIPAA. Not supported. Do not send Protected Health Information through MentionsAPI. We do not sign BAAs.
- Sub-processors. The current list:
- Cloudflare — API edge, TLS termination, Cache API, KV.
- Supabase — Postgres (accounts, API keys, usage events, response archive), Edge Functions (provider brokers).
- OpenAI — LLM provider, invoked only when you list
openaiin your providers array. - Anthropic — LLM provider, invoked only when you list
anthropic. - Google — LLM provider (Gemini), invoked only when you list
gemini. - Perplexity — LLM provider, invoked only when you list
perplexity. - Stripe — billing and payment processing. Receives only what it needs to bill you (customer, amount, metered events); never receives prompt content.
Reporting security issues
Email [email protected]. Include a proof-of-concept if possible and a suggested CVSS vector. We acknowledge reports within two business days.
We follow a 90-day responsible-disclosure window: please give us up to 90 days to investigate, remediate, and notify affected users before public disclosure. We will coordinate a disclosure timeline with you and credit you in the advisory unless you prefer to remain anonymous. Please do not run denial-of-service tests, pivot to other customers' data, or access data you aren't entitled to.