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:

2. Which networks to use

All 2 networks this deployment serves are in scope: mina-mainnet, mina-devnet.

Guidance, not enforcement. Every network this deployment serves is in scope as of v1.0 — mainnet included. This remains guidance rather than a restriction the service enforces: keys are not scoped to a network, so any active network will answer a valid key. Treat the scope above as the supported surface: it is what we watch, size and answer questions about.

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/keys

The 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 partner

The 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.

Not 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/info

5. Rate limits

Limits are per key, per minute, set by the key's tier unless the operator gave it an explicit limit:

TierDefault limit
free120 requests/minute
partner600 requests/minute
any other tier120 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_ID

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

StatusBodyMeans
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).
406empty body, no content-typeYour 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.

← Migration guide · Compatibility matrix