Skip to content

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.

ControlWhat it stopsSet it
ScopesA leaked key reaching webhooks or account dataPer key
Allowed originsA key lifted from your site, replayed elsewherePer key
Per-visitor-IP limitOne abusive visitor draining your quotaPer key
Per-key limitOne integration running away with your quotaPer key
ExpirationAn old key living foreverAt creation
Network bindingA mainnet key used against testnet, and the reverseAutomatic
Credit quotaAn unbounded billYour plan

Scopes

Every key carries scopes that decide which parts of the platform it can reach:

ScopeGrants access to
DataJSON-RPC, GraphQL, gRPC, and REST read endpoints
StreamsConnecting to existing Warp streams (SSE and WebSocket)
ManageCreating/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.

EntryMatches
example.comexample.com on any scheme and any port
*.example.comAny subdomain of example.com. Not the apex domain itself
https://app.example.comExactly that origin, HTTPS, default port
https://*.example.comAny subdomain, HTTPS only
http://localhost:3000Local development
203.0.113.10An IPv4 host

Matching rules:

  • Entries are case-insensitive. Default ports (:443 for HTTPS, :80 for HTTP) are normalized away.
  • A wildcard covers subdomains only. To allow both, add example.com and *.example.com.
  • Bare labels such as intranet are rejected. A browser never sends them. localhost is 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): 403 with code ORIGIN_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 no Origin, 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:

CapWhat it bounds
Per visitor IPRequests from a single IP address, for this key
Per keyAll 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:

text
X-Inodra-Credit-Remaining: 918204
X-Inodra-Credit-Limit: 1000000
X-Inodra-Credit-Replenish-Date: 2026-09-01T00:00:00Z

Rejections 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:

  1. Give it Data only, plus Streams if you use Warp. Never Manage.
  2. Set allowed origins to your site's domains, plus http://localhost:3000 if you develop against it.
  3. Set a per-visitor-IP cap, so one visitor cannot spend the whole budget.
  4. Set a per-key cap as your hard ceiling. It has to be at least as high as the per-IP cap.
  5. Use a key from your mainnet project only for mainnet traffic.
  6. Keep a separate, unrestricted, Manage-scoped key on your server for webhooks and stream management.
  7. Prefer the x-api-key header over the api_key query parameter, which lands in access logs and browser history. The query parameter exists for clients that cannot set headers, such as EventSource (see Authentication).

Error reference

StatusCodeMeaning
401MISSING_API_KEYNo x-api-key header
401INVALID_API_KEYUnknown, revoked, or disabled key
401API_KEY_EXPIREDThe key passed its expiration date
403INSUFFICIENT_SCOPEThe key lacks the scope this surface needs
403ORIGIN_NOT_ALLOWEDThe key is origin-restricted and this origin does not match
403NETWORK_MISMATCHThe key's network differs from the endpoint's network
429RATE_LIMIT_EXCEEDEDA per-key or per-visitor-IP cap fired. Not billed
429CU_QUOTA_EXCEEDEDThe 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.

Next steps

The full-stack Sui data layer.