Get an API key
Keys are admin-minted today. Self-serve signup is future work.
1. Request a key
Contact the operator to request a key. Include the metadata an operator needs to mint one:
- label — a human-readable name for the key (e.g.
acme-prod). It appears in logs and metrics, so make it identify the caller. - owner contact — an email / channel to reach the key owner.
- tier —
freeorpartner(selects the default rate limit; an explicit per-key limit can override it). These two are the only tiers with defined limits — any other value silently falls back to the service default.
2. Which networks to use
All 2 networks this deployment serves are in scope: mina-mainnet, mina-devnet.
3. How an operator mints it
Two paths. Both mint through the same store the running API reads, so a key from either authenticates immediately — pick whichever you can reach.
Admin REST API
curl -sS -X POST \
-H "x-admin-token: $MEA_ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{"label":"acme-prod","owner":"ops@acme.example","tier":"partner"}' \
https://mina-explorer-api.minaprotocol.com/admin/v1/keysThe admin surface (/admin/v1/keys) is mounted only when MEA_ADMIN_TOKEN is configured, and is guarded by the x-admin-token header (never the public x-api-key — an ordinary key is never sufficient). It is operator-only: the token is the single thing standing between the public internet and the ability to mint keys.
CLI (on the pod)
python -m app.keys create --label acme-prod \
--owner ops@acme.example --tier partnerThe verb is create. The CLI follows the same configuration the API does — with MEA_POSTGRES_DSN set it writes Postgres, without it Redis — so it cannot mint into a store the API does not read. Run it in the API's own environment (e.g. kubectl exec into an API pod) so it inherits that configuration.
scripts/seed_key.py. That helper is Redis-only and deliberately refuses to run when MEA_POSTGRES_DSN is set. On a Postgres-backed deployment it would write into a store the API does not read, so the key would look minted and then be rejected with 401 on every request.The plaintext key is printed exactly once, at mint. It is stored only as a SHA-256 hash, so it cannot be recovered or re-displayed later — copy it then, and if it is lost, revoke and mint a new one.
4. Use the key
Send it as the x-api-key header on every request:
curl -H "x-api-key: $YOUR_KEY" \
https://mina-explorer-api.minaprotocol.com/mina-mainnet/v1/info5. Rate limits
Limits are per key, per minute, set by the key's tier unless the operator gave it an explicit limit:
| Tier | Default limit |
|---|---|
free | 120 requests/minute |
partner | 600 requests/minute |
| any other tier | 120 requests/minute (the service default) |
Over the limit, a request is answered 429 with {"error": "Too many requests"}. The limit is a fixed per-minute window, so a burst that exhausts it recovers at the top of the next minute. If your workload needs more, ask the operator to raise the key's limit rather than minting extra keys — limits are per key, and a raise is a one-line change.
6. Rotate or revoke
Revoking is by key id (returned at mint, and listed by GET /admin/v1/keys — which never returns plaintext or hashes):
curl -sS -X DELETE \
-H "x-admin-token: $MEA_ADMIN_TOKEN" \
https://mina-explorer-api.minaprotocol.com/admin/v1/keys/$KEY_ID
# or, on the pod:
python -m app.keys revoke --id $KEY_IDRevocation takes effect within 30 seconds, not instantly: each replica caches verification results for at most 30 s, so a just-revoked key can still be accepted for up to that long. Rotating is mint-then-revoke: mint the replacement, cut traffic over, then revoke the old id. The record is kept after revocation for the audit trail — revoked keys never authenticate again.
7. Error responses
Every error is a JSON object with a single error key, served as application/json;charset=UTF-8:
| Status | Body | Means |
|---|---|---|
| 401 | {"error": "Invalid API Key"} | Missing, malformed, unknown or revoked x-api-key. |
| 404 | {"error": "Network not found"} | The mina-{network} path segment is not served here. |
| 405 | {"error": "Handler is not instance of HandlerMethod"} | Unknown path (upstream's wording, kept for compatibility). |
| 406 | empty body, no content-type | Your Accept header cannot be satisfied by JSON — see the migration guide. |
| 429 | {"error": "Too many requests"} | Over the key's per-minute limit. |
| 503 | {"error": "Network temporarily unavailable"} | The network is configured but its upstream is down or dormant. Retry. |
Self-serve signup
Future work. A self-serve signup portal is planned but not part of launch. Until then, all keys are admin-minted via the flow above.