/

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

  1. 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.
  2. Click New key, choose the provider, paste your real secret key and review the rules. Start with a test key.
  3. Copy the ghost key. It is shown only once.
  4. Put the ghost key in your app's environment instead of the real key, and point the provider's SDK at api.vallkey.com as shown below.
  5. Deploy, then remove the real key from everywhere else it was stored: CI variables, old .env files, shared notes.

Note

Your app talks to 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

Node.js
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' });
Python
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
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

Node.js
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' });
Python
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

Node.js
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' });
Python
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.

ProviderPoint your client atExtra checks
Stripehttps://api.vallkey.comSpending caps, connected accounts
Lemon Squeezy, Paddlehttps://api.vallkey.com—
OpenAI, xAI, Mistralhttps://api.vallkey.com/v1Allowed models
Anthropichttps://api.vallkey.comAllowed models
Google Geminihttps://api.vallkey.comAllowed models (read from the path)
Groqhttps://api.vallkey.com/openai/v1Allowed models
OpenRouterhttps://api.vallkey.com/api/v1Allowed models
Resend, SendGrid, Postmark, Mailgunhttps://api.vallkey.com—
Twiliohttps://api.vallkey.com—
GitHub, Cloudflare, Vercelhttps://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 as x-api-key, Basic auth, or a URL parameter such as ?api_key=.
A custom key for an API at https://api.example.com
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.

AWS SDK for JavaScript v3
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.

rules.json
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}
FieldMeaning
allowEndpoints the key may call. method is GET, POST, PUT, PATCH, DELETE or *. In path, * matches one segment and a final ** matches the rest.
ratePerMinuteRequests per minute, measured over a sliding window.
dailyRequestsRequests per UTC day.
maxAmountLargest amount per Stripe request, per currency, in the smallest unit (cents). A currency that is not listed is refused.
dailyAmountTotal amount per UTC day, per currency, in the smallest unit.
allowConnectedAccountsAllows the Stripe-Account header and parameters that send money to other accounts, such as transfer_data[destination].
allowedModelsFor AI providers: the only models the key may use.
dailyTokensFor AI providers: input plus output tokens per UTC day.
graphqlFor GraphQL APIs: { allowMutations, allowSubscriptions, operations }. See GraphQL below.
allowedIpsIPv4 or IPv6 addresses and CIDR ranges the key may be used from, up to 100.
allowedCountriesTwo-letter country codes the key may be used from, such as ["NL", "US"].
autoKillKill the key by itself when it looks leaked: { blockedPerMinute, onNewCountry, onSpike }. See Automatic kill below.
expiresAtTime in milliseconds since 1970 after which the key stops working.

Rules and limits

Spending limits

  • Amounts are in the currency's smallest unit: "usd": 5000 means $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-Key are 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:

Text
"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:

SettingKills the key when
blockedPerMinuteThis many requests are blocked within one minute. Someone trying a stolen key usually hits many blocks quickly.
onNewCountryA request comes from a country the key was never used from.
onSpikeRequests 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.

RoleCan
OwnerEverything, including the team, the recovery token, API tokens, the account email, data export and account deletion.
AdminCreate and delete keys, change rules, rotate ghost keys, replace real keys, revive keys, and change alerts.
ViewerSee 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.

AccessCan
Read onlyRead keys, activity and the audit log. Any request that changes something is refused.
AdminEverything an admin can do: create keys, change rules, rotate, kill and revive.
Full accessEverything 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:

Create a key
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/**"}]}}'
RequestWhat it does
POST /keysCreates a ghost key: { provider, real_key, label?, rules? }. Returns the ghost key once.
GET /keysLists your keys.
GET /keys/:idRules, pending changes, today's usage and recent activity.
PUT /keys/:id/rulesReplaces the rules. Answers { applied: "now" } or { applied: "scheduled", activatesAt }.
POST /keys/:id/killKills the key: { reason? }.
POST /keys/:id/reviveSchedules the key to come back after the safety delay.
DELETE /keys/:idDeletes the key and destroys its encrypted real key.
PUT /keys/:id/real-key, DELETE /keys/:id/real-keySchedules a new real key { real_key }, or cancels the pending one.
POST /keys/:id/rotate, DELETE /keys/:id/previousCreates 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.csvActivity, newest first, paged with ?before=<id>&limit=; or all of it as CSV.
GET /members, POST /invites, PUT /members/:id/role, DELETE /members/:idManages the team (owner only, except listing).
GET /auditThe audit log, paged with ?before=<id>&limit=.
GET /account/export, POST /account/deleteExports your data, or deletes the account with { "confirm": "DELETE MY ACCOUNT" }.
GET /passkeys, DELETE /passkeys/:idLists or removes passkeys, for example to recover a locked-out account.
POST /token/rotateReplaces your recovery token and returns the new one once.
GET /tokens, POST /tokens, DELETE /tokens/:idLists, creates { name, scope: "read" | "admin" | "full" } or revokes API tokens (owner only). A new token is returned once.
GET /billingYour plan, its limits and this month's usage.
GET /alerts, PUT /alertsReads or sets { webhook_url?, email_alerts? }.

Reference

Errors

Errors use the same shape as Stripe and OpenAI, so SDKs show our message:

JSON
{ "error": { "type": "guard_error", "code": "rule_denied", "message": "POST /v1/refunds is not allowed for this key", "request_id": "..." } }
StatusCodeMeaning
401unauthorizedMissing or invalid ghost key or token.
403rule_deniedThe key's rules do not allow this request, for example from this address or country.
403key_killedThe key was killed, by someone on the team or automatically.
429rate_limitedA per-minute or daily request limit was reached.
429quota_exceededA Free account reached twice its monthly requests. Requests resume next month, or at once after an upgrade.
402plan_limitThe management API refused a change the account's plan does not include, such as a fourth key on Free.
400bad_requestThe request could not be checked safely, for example duplicated amounts.
502upstream_errorThe 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:

  1. In your provider's dashboard, create a backup key with only the permissions your app needs (in Stripe, a restricted key).
  2. Store it in your password manager. Never put it in your app or CI until you need it.
  3. If we are down: set the backup key as your app's key, remove the api.vallkey.com override, and redeploy.
  4. 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_exceeded until 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 allowedModels set, 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.

Contact support