Errors and refusals
The shape
Every answer that is not a card has ok: false and an error object:
{
"ok": false,
"error": {
"code": "unknown_key",
"message": "That key is not one of ours.",
"docs": "https://www.sniffopotamus.dev/partners/errors#unknown_key"
}
}
| Field | What it is |
|---|---|
error.code | A short, stable code from the list below. Write your code against this. |
error.message | A plain-English sentence for your logs. It may be reworded, so never match on it. |
error.docs | A link to this page's explanation of the code. |
error.resets_at | over_allowance only: when your monthly allowance comes back, in UTC. |
status | in_review and not_found only: the same word as error.code, kept so code written for the first version still works. |
Every refusal is sent with Cache-Control: no-store. Never keep a refusal as
though it were a card.
Answers from before 29 September 2026
The first version of the door sent error as a plain word
("error": "unknown_key"), and sent in_review and not_found with only a
status field. This reads both:
function refusalCode(body) {
if (body && typeof body.error === "object") return body.error.code;
return body?.error ?? body?.status ?? "unavailable";
}
Every code
| Code | HTTP | Costs a call? | Retry? | In short |
|---|---|---|---|---|
no_key | 401 | No | No, fix it | No key was sent |
unknown_key | 401 | No | No, fix it | The key is not one of ours |
revoked | 401 | No | No, fix it | The key was stopped |
suspended | 403 | No | No, contact us | Your business record is suspended |
not_entitled | 403 | No | No, contact us | The partner API is not switched on for your business |
over_allowance | 429 | No | When the month turns over | This month's cards are used up |
rate_limited | 429 | No | After Retry-After | A ceiling was reached |
invalid_id | 400 | No | No, fix it | That is not a fragrance id |
in_review | 200 | No | Later | The perfume is held back while it is checked |
not_found | 404 | No | No | We hold no public perfume with that id |
card_failed | 500 | No | Yes, shortly | We could not build the card |
unavailable | 503 | No | Yes, shortly | Something on our side could not be checked |
use_key_tool | 410 | No | No | The admin screen no longer creates keys |
keys_by_code | 409 | No | No | New keys are handed out by Sniffopotamus for now |
Only a card that is served costs a call, so no refusal ever does.
no_key
401. The request had no Authorization: Bearer <key> header, or it was not
in that form.
What to do: send the header exactly as Authorization: Bearer sniffo_pk_….
Check that your server actually read the secret: an empty environment variable
sends Bearer with nothing after it. Each of these counts toward the failed
attempts allowed from your network address (30 a minute).
unknown_key
401. The key is not one we issued: a typo, a missing character, a space or a line break copied with it, or a key from another service.
What to do: copy the key again into your server's secret. Do not retry in a loop: each attempt counts toward the 30 failed attempts a minute your network address is allowed, and after that every request from that address waits, even ones with a good key. If you have lost the key, create a new one in the Builder Console; we store only a fingerprint of it and cannot show it again.
revoked
401. The key was stopped, by your business's owner or by us. A stopped key is refused from its very next request.
What to do: use your current key, or have the owner create a new one in the Builder Console and put it in your server's secret. See Getting started.
suspended
403. Your business record is suspended.
What to do: email sniff@sniffopotamus.com. Meanwhile keep serving stored copies up to 7 days old, as on Limits and caching.
not_entitled
403. Your key is valid, but the partner API is not switched on for your business. A key on its own never grants access; the switch does. Today that switch sits on the owner's own Sniffopotamus account, so this can also mean the key belongs to a business whose owner's account has changed.
What to do: email us.
over_allowance
429. Your business has used this month's allowance of cards. It comes back
at 00:00 UTC on the first day of next month, which error.resets_at also
gives.
What to do: stop asking for cards until then and keep serving your stored
copies up to 7 days old. whoami and capabilities are free, so you can check
where you stand. If this happens every month, your allowance is too small;
tell us.
rate_limited
429. One of the ceilings was reached:
| Ceiling | Limit | Counted per |
|---|---|---|
Failed key attempts (no_key, unknown_key) | 30 a minute | your network address |
| Card requests | 600 a minute | your business |
| Card requests | 50,000 in any 24 hours (a sliding window, not a calendar day) | your business |
whoami and capabilities together | 60 a minute | your business |
| Requests carrying one key | 1,200 a minute | that key |
The last is set above anything your business's own ceilings let you reach, so you only meet it if a copy of your key is being used somewhere else. Rotate the key if you do.
What to do: wait the number of seconds in the Retry-After header, then
carry on. The X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset headers say where you stand. For the 24-hour ceiling,
Retry-After counts down to when enough of the requests from 24 hours earlier
have fallen out of the window; it is not "tomorrow". If you meet the minute
ceiling in normal use, you are probably asking for the same card again and
again: keep a stored copy. See Limits and caching.
invalid_id
400. The id in the address is not a fragrance id. A fragrance id is a UUID,
such as 03c188b7-74aa-469d-ae40-caa8c45673b7: the last part of the perfume's
address on sniffopotamus.com.
What to do: check where your code gets the id from. This is free.
in_review
200, not an error status. We hold this perfume, but it is held back while something in it is checked, so none of its data is given out. It is the same answer our own app gives.
{
"ok": false,
"status": "in_review",
"error": {
"code": "in_review",
"message": "This fragrance is still being checked, so its card is not shown yet.",
"docs": "https://www.sniffopotamus.dev/partners/errors#in_review"
}
}
What to do: drop any stored copy you hold for this id at once, and show your own page without the card. Ask again another day. Free.
Today every live perfume in the catalogue is public (checked 29 September
2026), so no id answers in_review at the moment. Your code must still handle
it, because the catalogue team can hold a perfume back at any time.
not_found
404. We hold no public perfume with that id. The id may be wrong, or the perfume may have been removed or joined into another record.
What to do: drop any stored copy at once, show your page without the card, and flag the link on your side for a person to check. Do not ask again for the same id in a loop. Free.
card_failed
500. Your key was fine, but we could not build the card. Ours, not yours.
What to do: keep serving a stored copy up to 7 days old if you have one, and try again in a minute or so. Nothing was charged.
unavailable
503. We could not check your key, or could not count the call against your allowance, because something on our side did not answer. Ours, not yours. It is never a sign that your key is wrong.
What to do: retry after a short wait, backing off if it keeps happening, and
keep serving stored copies up to 7 days old meanwhile. Nothing was charged. If
it lasts more than a few minutes, capabilities tells you whether card reads
are switched on for accounts.
use_key_tool
410. The admin screen no longer creates or replaces keys. New keys are created by Sniffopotamus with its own key tool and stored in a secure keychain on our Mac, so a key is never shown in a web browser.
What to do: nothing on your side. This answer is for our own team, and a partner's server should not meet it. If you need a new key, email sniff@sniffopotamus.com and we will arrange it with you.
keys_by_code
409. New keys are handed out by Sniffopotamus for now, so the owner screen cannot create one for you.
What to do: email sniff@sniffopotamus.com to ask for a key. You can still stop a key at once from the owner screen if you think it has been seen by someone it should not have been.
Testing each answer
Use these to see each answer from your own server:
| To see | Ask for |
|---|---|
| A card | 03c188b7-74aa-469d-ae40-caa8c45673b7 (Casafutura) |
not_found | 00000000-0000-4000-8000-000000000000, a well-formed id we do not hold |
invalid_id | not-a-fragrance-id |
no_key | any card, with no Authorization header |
unknown_key | any card, with Authorization: Bearer sniffo_pk_test |
in_review | no id answers it today (see in_review); test your handling with a stub that returns the example above |
Keep the unknown_key and no_key tests to a handful: they count toward your
address's 30 failed attempts a minute.
Not partner codes
Two answers come from the web server rather than the partner door, and have no
error.code:
- 404 with a web page: the address is wrong. Check the path, for example
/api/partner/v1/fragrances/<id>/card. - 405: only
GETis answered. Do notPOST.
Telling us about a problem
Email sniff@sniffopotamus.com with the time
(with its time zone), the address you called, the HTTP status and the
error.code. Never send your key.