API Key Security
An Inodra API key is a bearer credential. Anyone who holds it can spend your credits. This page lists every control you can put on a key. If you ship a key in a browser app, follow the checklist.
| Control | What it stops | Set it |
|---|---|---|
| Scopes | A leaked key reaching webhooks or account data | Per key |
| Allowed origins | A key lifted from your site, replayed elsewhere | Per key |
| Per-visitor-IP limit | One abusive visitor draining your quota | Per key |
| Per-key limit | One integration running away with your quota | Per key |
| Expiration | An old key living forever | At creation |
| Network binding | A mainnet key used against testnet, and the reverse | Automatic |
| Credit quota | An unbounded bill | Your plan |
Scopes
Every key carries scopes that decide which parts of the platform it can reach:
| Scope | Grants access to |
|---|---|
| Data | JSON-RPC, GraphQL, gRPC, and REST read endpoints |
| Streams | Connecting to existing Warp streams (SSE and WebSocket) |
| Manage | Creating/editing/deleting webhooks and Warp streams, usage stats, key info |
New keys get Data only. Change scopes from the Scopes control on any key. Changes take effect within a few seconds.
Keep Manage off any key you ship to users. A Manage key can read your webhook signing secrets and repoint your webhook deliveries.
See API Key Scopes for the full reference.
Allowed origins
You can restrict a key to a list of web origins. When the list is not empty, the request must carry an Origin header that matches an entry. This is the main control for a key in a public web page. Set it from the Origins control on any key, or at creation time.
An entry is [scheme://][*.]host[:port]. Paths are not allowed.
| Entry | Matches |
|---|---|
example.com | example.com on any scheme and any port |
*.example.com | Any subdomain of example.com. Not the apex domain itself |
https://app.example.com | Exactly that origin, HTTPS, default port |
https://*.example.com | Any subdomain, HTTPS only |
http://localhost:3000 | Local development |
203.0.113.10 | An IPv4 host |
Matching rules:
- Entries are case-insensitive. Default ports (
:443for HTTPS,:80for HTTP) are normalized away. - A wildcard covers subdomains only. To allow both, add
example.comand*.example.com. - Bare labels such as
intranetare rejected. A browser never sends them.localhostis the one allowed single-label host. - IPv6 literals are not supported and never match.
- Limits: 20 entries per key, 255 characters per entry.
A restricted key is rejected when the request carries no Origin. Browsers set it on cross-origin fetch, XHR, and WebSocket upgrades, which is what a page calling *.inodra.com does.
Per surface:
- HTTP (JSON-RPC, GraphQL, REST):
403with codeORIGIN_NOT_ALLOWED. - WebSocket: the upgrade is refused and the socket closes with code
4003. - gRPC and gRPC-Web:
PERMISSION_DENIED. gRPC-Web from a browser works normally. A native gRPC client sends noOrigin, so it cannot use a restricted key.
What it does not do: origin restriction stops a key lifted from your web page from being reused on another site, because a browser sets Origin itself and page JavaScript cannot forge it. It does not authenticate a server-side caller: any curl user can send whatever Origin they like. Pair it with a per-visitor-IP rate limit, which applies to everyone.
Rate limits
Each key can carry two optional caps, set from the Limits control:
| Cap | What it bounds |
|---|---|
| Per visitor IP | Requests from a single IP address, for this key |
| Per key | All traffic through this key combined |
The per-IP cap keeps a browser-embedded key affordable. One abusive visitor, or someone who replays a copied key from their own server, is stopped at their own budget. Start around 20 requests per second: offices and mobile networks put many real visitors behind one IP.
Set both. The per-key cap always applies, so it is the one that gives you a hard ceiling. The per-IP cap depends on identifying the visitor, so treat it as a strong guard rather than an absolute one.
The per-IP cap cannot be set higher than the per-key cap. We compare rates, not raw numbers, so 100 requests per second per IP is rejected under a per-key cap of 10 per minute. A per-IP cap that can never fire would tell you that you are protected when you are not.
Rejections return 429 with code RATE_LIMIT_EXCEEDED and a Retry-After header, and do not consume credits. The response does not say which cap fired or how it is set, because your key is readable by anyone using your site.
See Rate Limits for the full reference.
Expiration
You choose an expiration at creation. The default is 30 days. Pick a preset, a custom number of days (up to 10 years), or Never. An expired key is rejected with 401 and code API_KEY_EXPIRED, and shows an Expired badge in the dashboard.
Expiration cannot be changed after creation: create a new key and delete the old one. Give short expirations to keys for tests, demos, or contractors. Give long ones to production keys, where a surprise expiry is an outage.
Project and network
A key belongs to one project, and a project is bound to one network. A mainnet key sent to testnet-api.inodra.com is rejected with 403 and code NETWORK_MISMATCH, and the reverse also fails. A leaked testnet key cannot spend against your mainnet traffic. Use separate projects, and separate keys, per environment. See Projects.
Your quota is a cost ceiling
Your plan's credit quota is the last line of defence. When it is spent, further requests are rejected with 429 instead of billing you. Every response carries the remaining balance:
X-Inodra-Credit-Remaining: 918204
X-Inodra-Credit-Limit: 1000000
X-Inodra-Credit-Replenish-Date: 2026-09-01T00:00:00ZRejections from a per-key or per-IP cap, a scope check, or an origin check do not consume credits. An attacker hammering a restricted key cannot run up your bill.
Watch usage per key in the dashboard. A key whose traffic suddenly changes shape is your earliest signal that it has been copied.
Keys in a browser app
A key in a public web page is readable by anyone who opens dev tools. Assume it will be copied, and make the copy worthless:
- Give it Data only, plus Streams if you use Warp. Never Manage.
- Set allowed origins to your site's domains, plus
http://localhost:3000if you develop against it. - Set a per-visitor-IP cap, so one visitor cannot spend the whole budget.
- Set a per-key cap as your hard ceiling. It has to be at least as high as the per-IP cap.
- Use a key from your mainnet project only for mainnet traffic.
- Keep a separate, unrestricted, Manage-scoped key on your server for webhooks and stream management.
- Prefer the
x-api-keyheader over theapi_keyquery parameter, which lands in access logs and browser history. The query parameter exists for clients that cannot set headers, such asEventSource(see Authentication).
Error reference
| Status | Code | Meaning |
|---|---|---|
401 | MISSING_API_KEY | No x-api-key header |
401 | INVALID_API_KEY | Unknown, revoked, or disabled key |
401 | API_KEY_EXPIRED | The key passed its expiration date |
403 | INSUFFICIENT_SCOPE | The key lacks the scope this surface needs |
403 | ORIGIN_NOT_ALLOWED | The key is origin-restricted and this origin does not match |
403 | NETWORK_MISMATCH | The key's network differs from the endpoint's network |
429 | RATE_LIMIT_EXCEEDED | A per-key or per-visitor-IP cap fired. Not billed |
429 | CU_QUOTA_EXCEEDED | The organization's credit quota is spent |
A rejected WebSocket upgrade closes with 4001 for a 401, 4003 for a 403, and 4029 for a rate limit. A stream already running closes with 1008 and reason QUOTA_EXCEEDED when the quota runs out. On gRPC, these arrive as UNAUTHENTICATED or PERMISSION_DENIED.