Authentication
API key format, the two header forms, the six causes of a 401, the suspended-workspace 403, and how to roll or revoke a key.
Every call to /api/v1 except the OpenAPI document carries an API key. There is one credential type and one place to get it: Developers → API keys in the console, which only owners and admins can open (on every plan). Six different reasons produce a 401, so read details.reason before you start debugging. The Developers page walks through creating, rolling, and revoking keys screen by screen.
Authentication also enforces workspace billing state. A valid key from a suspended workspace returns 403 permission_denied with details.reason: workspace_suspended on every route except the OpenAPI document, /ping included. Restoring billing re-enables the same keys; they do not need to be recreated.
Sending the key #
Two header forms are accepted. Use whichever your HTTP client makes cleaner.
curl https://services.amomic.in/api/v1/ping \-H "Authorization: Bearer amk_live_7Fk2xQ..."
They are equivalent. If you send both, X-Api-Key wins — it is checked first and the Authorization header is never read. That matters when a proxy or gateway in front of your service injects its own Authorization header: put your key in X-Api-Key and it will not be shadowed.
The Bearer prefix is matched case-insensitively and the token is trimmed, so bearer amk_live_… and a stray trailing space both work. Anything else in the Authorization header is treated as no key at all.
Send neither and you get:
{"error": {"code": "unauthenticated","message": "Provide your API key as `Authorization: Bearer amk_…` or `X-Api-Key`.","details": { "reason": "missing_api_key" },"request_id": "req_01JQRS4T8XZ0AB1CD2EF3GH4","correlation_id": "cor_01JQRS4T8XZ0AB1CD2EF3GH4"}}
Every 401 also carries a WWW-Authenticate: Bearer response header, so standards-aware HTTP clients recognise it as an authentication challenge rather than a generic failure.
Key format #
amk_live_7Fk2xQ9pLm4RtY8vN3wZ0aB6cD1eF5gH7iJ2kL4mN6o│ │ └─ secret: 32 random bytes, base64url, padding stripped — 43 characters│ └────── environment: exactly "live" or "test"└────────── product prefix: always "amk"
Each part earns its place:
amkis a constant so that secret scanners — GitHub's, your own pre-commit hook, your log scrubber — can find a leaked Amomic-AI key by pattern. A key that looks like generic base64 is a key nobody notices in a diff.live/testmakes the environment visible at a glance, in a log line, in a.envfile, in a screenshot on a support call. It is also enforced server-side; see environments below.- The secret is 256 bits from a cryptographic RNG. Nothing about it is derived from your workspace, your email, or the time it was created.
A token that is not exactly three underscore-separated parts, whose second part is not live or test, or whose secret is shorter than 20 characters is rejected as malformed before any lookup happens. A truncated key pasted from a terminal fails instantly with malformed_api_key, not with a vague "invalid".
We store a hash, not your key #
When a key is created, the raw token is generated, returned to you, and then discarded. What is stored is a SHA-256 hash of the secret plus a six-character display hint for the masked form you see in the console (amk_live_7Fk2xQ…).
The secret is shown exactly once
There is no second look. Support cannot retrieve it. An engineer with database access cannot retrieve it — the value is not there. Put the key in your secret manager before you close the creation dialog.
Lost it? Do not create a second key and leave the first one live. Roll it: you get a new secret and the old one keeps working for 24 hours, so you can deploy without a gap. See Rolling a key.
SHA-256 rather than bcrypt or Argon2 is a deliberate choice, and the reasoning is worth understanding because it is the opposite of the right answer for passwords. Password hashing is slow on purpose because human passwords are guessable and an attacker with the hash file can grind them. An API key secret is 256 bits from a CSPRNG — there is nothing to grind. Brute force is not the threat model, so the design optimises for what is: verification is a single fast lookup, which is what lets a revoked key stop working in seconds instead of minutes.
Environments: test versus live #
There is one production API. The live / test distinction is not a mode you toggle per request — it is baked into the key at creation and checked against the host it is used on.
- A key issued in production is a live key. It works against
services.amomic.in. Real messages leave the building, real calls are dialled, real money is spent. - A key issued in a non-production environment is a test key, and it only works there.
Cross them and you get a 401 that names the problem exactly:
{"error": {"code": "unauthenticated","message": "That is a test key; this environment expects a live key.","details": { "reason": "wrong_environment" },"request_id": "req_01JQRS4T8XZ0AB1CD2EF3GH4","correlation_id": "cor_01JQRS4T8XZ0AB1CD2EF3GH4"}}
wrong_environment is not a bad secret
This is the single most misread error on the API. wrong_environment means the key is genuine, unrevoked, and correctly formatted — it is simply the wrong side of the environment boundary. Nobody needs to re-issue anything. Your staging deployment is pointed at the production host, or your production deployment picked up the staging secret. Check which AMOMIC_API_KEY your process actually loaded before you touch the console.
Note it is a 401, not a 403. The key is not recognised as a credential here, which is an authentication outcome, not a permission one.
The validation order #
Checks run in a fixed order, and each one produces a different error. Knowing the order tells you what has already been proven true by the time you see a given failure.
- 1
Is a key present?
Neither header set, or an
Authorizationheader that is not a bearer token. →401unauthenticated,reason: missing_api_key - 2
Does it parse?
Three parts,
amkfirst,liveortestsecond, a secret of at least 20 characters. No datastore lookup happens at this stage — a malformed token never touches storage. →401unauthenticated,reason: malformed_api_key, message"That doesn't look like an Amomic-AI API key." - 3
Does the secret match a real key?
The record is looked up and the hash compared. →
401unauthenticated,reason: invalid_api_key, message"That API key is not valid." - 4
Has it been revoked?
→
401unauthenticated,reason: revoked_api_key, message"That API key has been revoked." - 5
Has it expired?
Only rolled keys expire, when their 24-hour grace window closes. →
401unauthenticated,reason: expired_api_key, message"That API key was rolled and its grace period has ended." - 6
Is it the right environment?
→
401unauthenticated,reason: wrong_environment - 7
Is the workspace in good standing?
A suspended workspace's keys stop working until billing is restored. →
403permission_denied,reason: workspace_suspended, message"This workspace is suspended. Restore billing to use its API keys." - 8
Is the key within its rate limit?
→
429rate_limited. See Rate limits. - 9
Does it have the scope this route needs?
→
403permission_denied,reason: missing_scope. See Scopes.
Read that backwards when you are debugging. A 403 with missing_scope proves your key is valid, unrevoked, in the right environment, in a workspace in good standing, and under its rate limit: the only thing wrong is what it is allowed to do. A 429 proves everything except the scope check. And an invalid_api_key proves the token was well-formed, which usually means you are holding a real key from another workspace or another environment's database, not a typo.
The six causes of a 401 #
details.reason |
Message | What it actually means |
|---|---|---|
missing_api_key |
Provide your API key as Authorization: Bearer amk_… or X-Api-Key. |
No header reached us. Check for a proxy stripping Authorization. |
malformed_api_key |
That doesn't look like an Amomic-AI API key. | Truncated paste, wrapped line, quotes included, or a placeholder that was never substituted. |
invalid_api_key |
That API key is not valid. | Well-formed but unknown. Usually a key from a different workspace, or one character altered. |
revoked_api_key |
That API key has been revoked. | Someone revoked it in the console. Permanent — mint a new key. |
expired_api_key |
That API key was rolled and its grace period has ended. | The old half of a roll, more than 24 hours later. Deploy the new secret. |
wrong_environment |
That is a <env> key; this environment expects a <env> key. |
Right key, wrong host. Not a credential problem. |
All six are code: "unauthenticated" and HTTP 401. Branch on details.reason, never on the message text — messages are written for humans and may be reworded.
The five-second verification cache #
A verified key record is cached in-process for 5 seconds. This is why the API can afford a single hash lookup per request without a datastore round-trip on every call.
Two consequences you will actually notice:
- Changes take effect within seconds, not instantly everywhere. Rolling or revoking a key takes effect immediately on the server that handled the change, and everywhere else within 5 seconds. If you revoke a leaked key and a request slips through 2 seconds later, that is this window — not a failure of the revoke.
last_used_atin the console is coarse. It is written at most once a minute per key, so a key that fired 4,000 requests in the last minute may show alast_used_atup to a minute stale. Use it to answer "is this key still in use at all", not "when exactly was the last call". For per-request truth, use the request log under Developers → Logs.
Rolling a key without downtime #
Rolling is how you rotate a secret without a deployment window. In the console, on the key's row, click Roll. There is no confirmation step: the replacement is created straight away.
What happens:
- A new key is minted with the same name, the same scopes, and the same rate limit. Its secret is shown once, in the Copy your API key dialog. This is the only moment other than creation that a secret is shown.
- The old key's row shows Expires soon, and the key expires 24 hours from that moment.
- Both keys authenticate for those 24 hours.
So the safe sequence is: roll, copy the new secret into your secret manager, deploy, confirm traffic is flowing on the new key, and let the old one lapse. You never have a moment where no valid key exists.
After the grace window closes, the old secret returns 401 with reason: expired_api_key. Nothing is deleted before then, and nothing needs to be cleaned up after.
Rolling is the answer to more than rotation
Lost the secret, a laptop went missing, a contractor left, the key showed up in a CI log — all of these are rolls, not revokes. Revoke is for when you need the old secret dead now and can accept the outage. Roll is for everything with a deploy in front of it.
A revoked key cannot be rolled: its row has no Roll or Revoke button.
Rolling copies the scopes exactly, so it cannot change what a key can do. To grant a different set of scopes, see Scopes.
Revoking a key #
Revoking is immediate and permanent. In the console, click Revoke on the key's row, then Revoke key in the Revoke this key? dialog. Within 5 seconds every request carrying it fails with 401 and reason: revoked_api_key.
The key is not deleted from the list. Its row stays, with a Revoked status and no actions, so you can still see what it was called and which scopes it held.
There is no un-revoke. If you revoke the wrong key, you create a new one and redeploy — the secret is genuinely gone.
Revoke, rather than roll, when the secret is known to be compromised and you would rather break your own integration than leave a window open. That is the whole trade: roll keeps you running, revoke stops the bleeding.
Key hygiene that actually helps #
These are not ceremony. Each one exists because of a specific bad hour it prevents.
- 1
One key per system that calls us
Not one per workspace, not one shared across every service. The order service, the CRM sync, and the analytics exporter get three keys.
The payoff arrives when something goes wrong: you can revoke the leaked one without taking down the other two, the request log's Key column shows which system is generating the errors, and you can scope each one to exactly what it needs — the analytics exporter never gets
messages:send. - 2
Name the key after the system, not the person or the date
Order serviceandZoho sync (prod)are good names.Vinit's key,test, andkey 2are not. At 3 a.m., when one key is throwing 429s, the name is the only thing that tells you which deployment to look at — and the person who created it may have left the company. - 3
Give it the narrowest scope that works
The create dialog starts with two read scopes ticked (Read conversations and Read agents). Tick the write scopes this system needs before you create the key: a key's scopes cannot be changed afterwards. If a request later fails with
missing_scope, create a replacement key with the right scopes, deploy it, and revoke the old one. See Scopes. - 4
Keep it out of source control, images, and logs
The
amk_prefix exists so scanners can catch it — enable secret scanning on your repositories and add the pattern to your log scrubber. Load it from your secret manager at runtime; do not bake it into a container image, where it survives in every layer that ever held it. - 5
Roll on staff change and on schedule
When someone who could have copied a key leaves, roll every key they could have copied. Absent that, a quarterly roll costs one deploy and bounds how long any undetected leak stays useful.
- 6
Point every key at the right host
The environment is in the key. Load
amk_live_…in production andamk_test_…in staging, and make your config loader assert the prefix matches the environment it thinks it is running in. That one assertion turns a confusingwrong_environmentin production into a startup failure in staging.
Verify all of this in one call #
GET /ping is the fastest way to find out what the API thinks of your key. No side effects, no scope required beyond a valid key.
curl https://services.amomic.in/api/v1/ping \-H "Authorization: Bearer $AMOMIC_API_KEY"
{"ok": true,"workspace_id": "ten_01JQRS4T8XZ0AB1CD2EF3GH4","key_name": "Order service","environment": "live","scopes": ["agents:read", "conversations:read", "messages:send"]}
That response answers four questions at once: the key is valid, it is pointed at the right workspace, it is the environment you expected, and it holds the scopes you think it does. Run it in your integration's startup health check.
Next #
Scopes
What each scope unlocks, the read-only default, and the endpoint matrix.
Errors
The full error contract — nine codes, one envelope, every details.reason.
Rate limits
120 requests per key per minute, and how to back off without a Retry-After.
Security and data
What we store and for how long, including the request log.