Documentation
Vallkey docs
Everything you need to protect your API keys: Stripe, PayPal, OpenAI, Anthropic, AWS and any other HTTPS API.
Getting started
Quick start
- Create an account at app.vallkey.com with your email or GitHub, choose where your data lives, and add a passkey for fast, phishing-proof sign-in.
- Click New key, choose the provider, paste your real secret key and review the rules. Start with a test key.
- Copy the ghost key. It is shown only once.
- Put the ghost key in your app's environment instead of the real key, and point the provider's SDK at
api.vallkey.comas shown below. - Deploy, then remove the real key from everywhere else it was stored: CI variables, old .env files, shared notes.
Note
https://api.vallkey.com with the ghost key. We check the request against the key's rules, replace the ghost key with the real one, and forward it to the provider. The response comes straight back.Getting started
Stripe
1import Stripe from 'stripe';2 3// .env: STRIPE_GHOST_KEY=gk_stripe_...4const stripe = new Stripe(process.env.STRIPE_GHOST_KEY, { host: 'api.vallkey.com' });5 6const intent = await stripe.paymentIntents.create({ amount: 2000, currency: 'usd' });1import os, stripe2 3stripe.api_key = os.environ["STRIPE_GHOST_KEY"]4stripe.api_base = "https://api.vallkey.com"5 6intent = stripe.PaymentIntent.create(amount=2000, currency="usd")curl https://api.vallkey.com/v1/customers?limit=3 \ -H "Authorization: Bearer $STRIPE_GHOST_KEY"New Stripe keys start with these rules: read anything (GET /v1/**), create and update customers, create, update and confirm Payment Intents, create Checkout Sessions, and at most 100 requests a minute. Refunds, payouts, transfers and connected accounts are off until you allow them.
Stripe requests must be form-encoded (the official SDKs do this), and POST requests may not carry query parameters, so the parameters we check are exactly the ones Stripe acts on.
Getting started
OpenAI
import OpenAI from 'openai'; // .env: OPENAI_GHOST_KEY=gk_openai_...const openai = new OpenAI({ apiKey: process.env.OPENAI_GHOST_KEY, baseURL: 'https://api.vallkey.com/v1' });import osfrom openai import OpenAI client = OpenAI(api_key=os.environ["OPENAI_GHOST_KEY"], base_url="https://api.vallkey.com/v1")Default rules: chat completions, responses, embeddings and listing models, at most 60 requests a minute. Add allowedModels to stop a leaked key from running expensive models.
Getting started
Anthropic
import Anthropic from '@anthropic-ai/sdk'; // .env: ANTHROPIC_GHOST_KEY=gk_anthropic_...const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_GHOST_KEY, baseURL: 'https://api.vallkey.com' });import osfrom anthropic import Anthropic client = Anthropic(api_key=os.environ["ANTHROPIC_GHOST_KEY"], base_url="https://api.vallkey.com")Default rules: messages, token counting and listing models, at most 60 requests a minute.
Providers
All providers
Every provider gets endpoint rules, rate limits, expiry, the kill switch, alerts and activity. Some get extra checks.
| Provider | Point your client at | Extra checks |
|---|---|---|
| Stripe | https://api.vallkey.com | Spending caps, connected accounts |
| Lemon Squeezy, Paddle | https://api.vallkey.com | — |
| OpenAI, xAI, Mistral | https://api.vallkey.com/v1 | Allowed models |
| Anthropic | https://api.vallkey.com | Allowed models |
| Google Gemini | https://api.vallkey.com | Allowed models (read from the path) |
| Groq | https://api.vallkey.com/openai/v1 | Allowed models |
| OpenRouter | https://api.vallkey.com/api/v1 | Allowed models |
| Resend, SendGrid, Postmark, Mailgun | https://api.vallkey.com | — |
| Twilio | https://api.vallkey.com | — |
| GitHub, Cloudflare, Vercel | https://api.vallkey.com | — |
Paths stay exactly as in the provider's own docs: https://api.resend.com/emails becomes https://api.vallkey.com/emails. Send the ghost key the same way you would send the real key: as a Bearer token, in the provider's key header (such as x-goog-api-key), or as the Basic auth password for Twilio and Mailgun. For Twilio, keep your Account SID as the username.
Providers
Custom APIs
For any other HTTPS API, create a Custom API key. You choose:
- The API address, such as
https://api.example.com. It is fixed for the life of the key, so nobody can redirect your real key to another server. To change it, create a new key. - How the key is sent:
Authorization: Bearer, a header such asx-api-key, Basic auth, or a URL parameter such as?api_key=.
1# Before2curl https://api.example.com/v2/orders -H "x-api-key: $REAL_KEY"3 4# After5curl https://api.vallkey.com/v2/orders -H "x-api-key: $GHOST_KEY"Custom keys forward your request headers except credentials, cookies and connection details. Addresses must be public HTTPS hostnames: IP addresses, internal names and non-standard ports are refused. Supabase works with the preset in the dashboard (the key is sent as both apikey and a Bearer token).
Your app can always send the ghost key wherever it would send the real key, including a URL parameter. Vallkey finds it, removes it, and attaches the real key the way the provider expects. Ghost keys never reach the provider, and URL parameters are never stored in activity.
Which provider does a request go to?
The ghost key decides, never the path. gk_openai_… always goes to OpenAI and gk_stripe_… to Stripe, even when two providers use the same path. Custom and AWS keys go to the address saved in the key.
Providers
AWS
Create an AWS key with an endpoint (such as s3.us-east-1.amazonaws.com), a region and a signing name (s3, dynamodb, bedrock…). Paste the real key as ACCESS_KEY_ID:SECRET_ACCESS_KEY. In your app, use the ghost key as the access key id and any text as the secret: Vallkey throws away your app's signature and signs the request again with the real credentials.
1const s3 = new S3Client({2 region: 'us-east-1',3 endpoint: 'https://api.vallkey.com',4 forcePathStyle: true,5 credentials: { accessKeyId: process.env.AWS_GHOST_KEY, secretAccessKey: 'vallkey' },6});Endpoints must be AWS hostnames. Presigned URLs and chunk-signed uploads are refused. Many AWS services put the action in a header rather than the path, so also limit the real key with an IAM policy: Vallkey adds the kill switch, rate limits and alerts on top.
Services such as DynamoDB, SQS, SNS and STS send every request to the root path. For those, allow POST / in the key's rules; /** covers every path below the root, not the root itself.
Providers
PayPal
Paste the real credentials as CLIENT_ID:CLIENT_SECRET. Point your PayPal SDK at https://api.vallkey.com and use the ghost key as the client secret. When the SDK asks for an access token, Vallkey answers with the ghost key itself; it fetches and caches the real PayPal token on its own, so the real secret and the real token never reach your app.
Spending limits work for orders, refunds, captures and payouts. Amounts are in the smallest unit, as for Stripe. Requests with PayPal-Auth-Assertion or a payee count as connected-account actions.
Providers
WebSockets
WebSocket connections, such as OpenAI's Realtime API, go through Vallkey too. Allow GET /v1/realtime in the key's rules. Servers send the ghost key as a Bearer token; browsers, which cannot set headers, send it in the openai-insecure-api-key. subprotocol. The model in ?model= is checked against allowedModels, and token usage reported during the session counts toward the daily token cap: the session is closed once the cap is reached.
Rules and limits
Rules
Each ghost key has its own rules, edited in the dashboard or through the API. Anything not allowed is refused. null means no limit.
1{2 "allow": [3 { "method": "POST", "path": "/v1/payment_intents" },4 { "method": "GET", "path": "/v1/**" }5 ],6 "ratePerMinute": 100,7 "dailyRequests": 5000,8 "maxAmount": { "usd": 5000 },9 "dailyAmount": { "usd": 50000 },10 "allowConnectedAccounts": false,11 "allowedModels": null,12 "dailyTokens": null,13 "graphql": null,14 "allowedIps": ["203.0.113.0/24"],15 "allowedCountries": ["NL", "DE"],16 "autoKill": { "blockedPerMinute": 20, "onNewCountry": false, "onSpike": true },17 "expiresAt": null18}| Field | Meaning |
|---|---|
allow | Endpoints the key may call. method is GET, POST, PUT, PATCH, DELETE or *. In path, * matches one segment and a final ** matches the rest. |
ratePerMinute | Requests per minute, measured over a sliding window. |
dailyRequests | Requests per UTC day. |
maxAmount | Largest amount per Stripe request, per currency, in the smallest unit (cents). A currency that is not listed is refused. |
dailyAmount | Total amount per UTC day, per currency, in the smallest unit. |
allowConnectedAccounts | Allows the Stripe-Account header and parameters that send money to other accounts, such as transfer_data[destination]. |
allowedModels | For AI providers: the only models the key may use. |
dailyTokens | For AI providers: input plus output tokens per UTC day. |
graphql | For GraphQL APIs: { allowMutations, allowSubscriptions, operations }. See GraphQL below. |
allowedIps | IPv4 or IPv6 addresses and CIDR ranges the key may be used from, up to 100. |
allowedCountries | Two-letter country codes the key may be used from, such as ["NL", "US"]. |
autoKill | Kill the key by itself when it looks leaked: { blockedPerMinute, onNewCountry, onSpike }. See Automatic kill below. |
expiresAt | Time in milliseconds since 1970 after which the key stops working. |
Rules and limits
Spending limits
- Amounts are in the currency's smallest unit:
"usd": 5000means $50.00. - When a key has spending limits, a request that moves money must state its amount and currency. A full refund of unknown size is refused.
- A payment the provider declines does not count toward the daily total. If the provider cannot be reached, the amount stays counted, because the payment may have gone through.
- Retries with the same
Idempotency-Keyare counted once.
Rules and limits
AI token caps
Set dailyTokens to cap what a key can spend on AI, for example 2000000 tokens a day. We count tokens rather than dollars because prices change and differ by model; tokens are what every provider reports. Vallkey reads the usage each response reports, including streamed ones. For OpenAI, Groq and xAI streaming chat completions, it asks for usage with stream_options.include_usage, which adds a final chunk with empty choices.
Usage is only known when a response finishes, so the request that crosses the cap completes and the next one is refused with 429. Combine the cap with allowedModels to keep each request cheap.
Rules and limits
GraphQL
Custom API and GitHub keys can check GraphQL operations. With graphql set, every POST must be readable GraphQL (JSON, a batch, or application/graphql), and each operation in it is checked:
"graphql": { "allowMutations": false, "allowSubscriptions": false, "operations": ["GetProduct", "ListOrders"] }operations: null allows any operation name. Unnamed operations are refused when names are listed. Persisted queries (a hash instead of the query text) cannot be read, so they are refused.
Rules and limits
IP and country limits
If your app runs on servers with fixed addresses, list them in allowedIps. A ghost key that leaks is then useless anywhere else. Use single addresses such as 203.0.113.7 or ranges such as 203.0.113.0/24 and 2001:db8::/48.
allowedCountries refuses requests from outside the countries you list. The country comes from the caller's IP address, so treat it as a strong hint, not proof: VPNs exist. For servers, an address list is the stronger choice.
Every request in Activity shows its IP address, country and client. When a key already has an address list, open a blocked request and choose Allow this address to add it. Allowing more addresses is a looser rule, so it waits an hour like any other.
Rules and limits
Automatic kill
Vallkey can kill a key by itself, the moment it looks leaked, instead of waiting for you to read an alert:
| Setting | Kills the key when |
|---|---|
blockedPerMinute | This many requests are blocked within one minute. Someone trying a stolen key usually hits many blocks quickly. |
onNewCountry | A request comes from a country the key was never used from. |
onSpike | Requests in an hour reach 10 times the usual hour, and at least 100. |
A new key first learns what normal looks like: new countries and spikes are only judged after it has been in use for an hour and handled 20 requests. The request that trips the kill is refused too. Everyone gets an alert, the audit log shows "Vallkey (automatic)" with the reason, and you can revive the key as usual.
Rules and limits
Rotating a ghost key
To get a new ghost key, for example after a teammate leaves, open the key's Connect tab and choose Rotate, or POST /keys/:id/rotate. The rules, limits and real key stay the same.
Choose how long the old ghost key keeps working: not at all, 1 hour, 24 hours or 3 days. During that time the key page shows how often the old one is still used, so you know when every server has switched. Turn it off early with Turn off now.
Rules and limits
Replacing a real key
Rotated a key at the provider? Paste the new one under Real key on the key's page, or PUT /keys/:id/real-key. The ghost key stays the same, so your app needs no change. The switch happens after one hour and everyone gets an alert: otherwise someone in your dashboard could swap in a key from their own account and redirect your payments. Anyone on the team can cancel a pending replacement.
Teams and security
Team and audit log
Invite people from Team. Each invite is a one-time link that expires in 7 days; the person joins by creating their own passkey.
| Role | Can |
|---|---|
| Owner | Everything, including the team, the recovery token, API tokens, the account email, data export and account deletion. |
| Admin | Create and delete keys, change rules, rotate ghost keys, replace real keys, revive keys, and change alerts. |
| Viewer | See keys, activity, the team and the audit log. Kill a key, cancel a real-key replacement or turn off an old ghost key in an emergency. |
Role changes apply at once. Removing someone signs them out everywhere. The Audit log records who created, changed, killed, revived or deleted keys, and every team and security change. Your plan decides how far back you can see it: 30 days on Free, 90 on Pro, a year on Team and Business.
Teams and security
Activity history
For each request, Vallkey records the time, method, path, outcome, status, how long the provider took, the caller's IP address and country, and its user agent. Request and response bodies are never stored.
Each key keeps its requests for 90 days, up to the latest 10,000; your plan decides how far back you can see (7 days on Free, 30 on Pro, 90 on Team and Business). Load older requests on the key's page, or export them as CSV. Cells that a spreadsheet would run as a formula are prefixed with a quote, so an attacker-chosen path cannot run in your spreadsheet.
Rules and limits
Kill switch and slow loosening
Killing a key takes effect everywhere at once. Making rules stricter also applies immediately.
Making rules looser, or reviving a killed key, waits one hour and sends you an alert. If someone gets into your dashboard, they cannot quietly open a key up: you hear about it with time to react, and saving stricter rules cancels the pending change.
Teams and security
Data regions
Each account chooses where its data lives when it is created: European Union, stored and processed only inside the EU, or Global, stored close to where the account is used. Keys, activity, the audit log, the team and the plan all stay in that region.
Nothing changes in your code: ghost keys, the management API and the dashboard work the same in both regions. The region is part of each key's signed id, so requests go straight to the right place. A request from far away to an EU key takes a little longer, about a tenth of a second from the other side of the world.
The region cannot be changed from the dashboard. To move an account, contact us.
Teams and security
Leak detection
Security signals
Each key learns where it is normally used from. After it has learned, Vallkey raises a signal when the key is used from a new country or a new network, when traffic jumps to 10 times the usual hour, or when many requests are blocked at once. Signals appear on the overview and on the key's page, and new countries also send an alert. Turn on Automatic kill to act on them without waiting for you.
Keys pushed to GitHub
Ghost keys and API tokens have a fixed, recognizable format. When one is pushed to a public GitHub repository, GitHub tells Vallkey and we turn it off at once: a ghost key is killed, an old ghost key from a rotation stops working, and an API token is revoked. You get an alert with the link to where it was found, and the audit log shows "GitHub secret scanning".
Teams and security
Alerts
We alert you when a request is blocked, a key is killed (by a person or automatically) or deleted, a key is used from a new country, a ghost key is rotated or found in public, a key reaches its daily token cap, a real-key replacement is asked for, someone joins or leaves the team, a passkey is added or removed, an API token is created or your recovery token is replaced, or looser rules are scheduled. Alerts go to your account email and, if you add one in Settings, a Slack incoming webhook. Repeated blocks of the same kind are grouped to at most one alert every five minutes per key.
Teams and security
Account security
Passkeys
Add a passkey in Settings to sign in with Face ID, Touch ID, Windows Hello or a security key. Passkeys only work on app.vallkey.com, so a look-alike site cannot use them. Once your account has a passkey, the management token can no longer sign in to the dashboard.
Recovery token
The management token you get at sign-up works with the management API and recovers your account if you lose your passkeys. Keep it in a password manager, not in your app.
Replacing the token
If your token may have leaked, replace it in Settings. The old token stops working immediately and every other session is signed out. Ghost keys and API tokens keep working.
Teams and security
API tokens
Give every script and CI system its own token from Settings → API tokens, instead of sharing the recovery token. Each one has a name, shows when it was last used, and can be revoked on its own.
| Access | Can |
|---|---|
| Read only | Read keys, activity and the audit log. Any request that changes something is refused. |
| Admin | Everything an admin can do: create keys, change rules, rotate, kill and revive. |
| Full access | Everything the owner can do through the API. |
API tokens work only with the management API. They cannot sign in to the dashboard. An account can have up to 25.
Reference
Management API
Everything the dashboard does is available at https://api.vallkey.com/_guard/v1 with your management token:
curl https://api.vallkey.com/_guard/v1/keys \ -H "Authorization: Bearer $VALLKEY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"provider":"stripe","real_key":"sk_test_...","label":"CI","rules":{"allow":[{"method":"GET","path":"/v1/**"}]}}'| Request | What it does |
|---|---|
POST /keys | Creates a ghost key: { provider, real_key, label?, rules? }. Returns the ghost key once. |
GET /keys | Lists your keys. |
GET /keys/:id | Rules, pending changes, today's usage and recent activity. |
PUT /keys/:id/rules | Replaces the rules. Answers { applied: "now" } or { applied: "scheduled", activatesAt }. |
POST /keys/:id/kill | Kills the key: { reason? }. |
POST /keys/:id/revive | Schedules the key to come back after the safety delay. |
DELETE /keys/:id | Deletes the key and destroys its encrypted real key. |
PUT /keys/:id/real-key, DELETE /keys/:id/real-key | Schedules a new real key { real_key }, or cancels the pending one. |
POST /keys/:id/rotate, DELETE /keys/:id/previous | Creates a new ghost key { grace_hours: 0 | 1 | 24 | 72 } and returns it once; or turns the old one off early. |
GET /keys/:id/activity, GET /keys/:id/activity.csv | Activity, newest first, paged with ?before=<id>&limit=; or all of it as CSV. |
GET /members, POST /invites, PUT /members/:id/role, DELETE /members/:id | Manages the team (owner only, except listing). |
GET /audit | The audit log, paged with ?before=<id>&limit=. |
GET /account/export, POST /account/delete | Exports your data, or deletes the account with { "confirm": "DELETE MY ACCOUNT" }. |
GET /passkeys, DELETE /passkeys/:id | Lists or removes passkeys, for example to recover a locked-out account. |
POST /token/rotate | Replaces your recovery token and returns the new one once. |
GET /tokens, POST /tokens, DELETE /tokens/:id | Lists, creates { name, scope: "read" | "admin" | "full" } or revokes API tokens (owner only). A new token is returned once. |
GET /billing | Your plan, its limits and this month's usage. |
GET /alerts, PUT /alerts | Reads or sets { webhook_url?, email_alerts? }. |
Reference
Errors
Errors use the same shape as Stripe and OpenAI, so SDKs show our message:
{ "error": { "type": "guard_error", "code": "rule_denied", "message": "POST /v1/refunds is not allowed for this key", "request_id": "..." } }| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid ghost key or token. |
| 403 | rule_denied | The key's rules do not allow this request, for example from this address or country. |
| 403 | key_killed | The key was killed, by someone on the team or automatically. |
| 429 | rate_limited | A per-minute or daily request limit was reached. |
| 429 | quota_exceeded | A Free account reached twice its monthly requests. Requests resume next month, or at once after an upgrade. |
| 402 | plan_limit | The management API refused a change the account's plan does not include, such as a fourth key on Free. |
| 400 | bad_request | The request could not be checked safely, for example duplicated amounts. |
| 502 | upstream_error | The provider could not be reached. |
Every response carries an x-guard-request-id header. Include it when you contact us.
Reference
If Vallkey is down
The status page shows the live state of every part of Vallkey and any incident. If we are unreachable, requests through Vallkey fail. Prepare a way out before you need it:
- In your provider's dashboard, create a backup key with only the permissions your app needs (in Stripe, a restricted key).
- Store it in your password manager. Never put it in your app or CI until you need it.
- If we are down: set the backup key as your app's key, remove the
api.vallkey.comoverride, and redeploy. - When we are back: switch to the ghost key again and roll the backup key in your provider's dashboard, since it has now been used.
Reference
Plans and billing
Each plan sets how many ghost keys, requests a month, team members and API tokens an account has, and how far back activity and the audit log go. See the pricing page for the numbers. Billing in the dashboard shows this month's usage.
- Requests are those Vallkey forwards to a provider. Blocked requests are not counted, so a leaked key cannot use up your plan. The count restarts on the first day of each month, UTC.
- Going over never stops a paid account: you get an email at 80% and 100%. A Free account keeps working up to twice its limit, then its requests are refused with
quota_exceededuntil the next month or an upgrade. - Moving to a smaller plan deletes nothing. Keys, members and rules you have keep working; you only cannot add more than the new plan allows. Address limits, country limits and auto-kill a key already has stay in force and can be removed, but not added or changed, on Free.
- Payments are handled by Paddle.com, our merchant of record, which adds sales tax or VAT and sends invoices. Open Billing → Invoices and payment for receipts, your card and cancellation.
Reference
Limits
- Request bodies: 1 MB for Stripe, 25 MB for the AI providers (32 MB for Anthropic), 50 MB for AWS, 10 MB for everything else.
- Paths must start with the provider's usual prefix (such as
/v1/). Percent-escapes are allowed except encoded slashes, backslashes and dots. - With
allowedModelsset, file and audio uploads are refused, because their model cannot be read. - JSON bodies are forwarded re-encoded, so integers larger than 2^53 lose precision.
- While a request is being forwarded, the real key is decrypted in memory for that moment only.
Still have a question?
We read every email, usually within a day.