Getting started
This page takes you from nothing to a fragrance card on your own page. Every business connects the same way: a sign-in, a business record, a key, and your server asking for cards. There is no private door, and Sniffopotamus's own sister apps connect exactly like this too.
What you need
- A Sniffopotamus sign-in for the person who owns the business. It is the same free account anyone can make at sniffopotamus.com.
- A business record, which we set up for you. Today a person at Sniffopotamus does this by hand, so it is not instant: we email you when it is ready.
- A server you control: a website back end, an edge function or a serverless function. The key lives there and nowhere else.
Once you hold a key, your first card takes a few minutes from a terminal.
Prices are not published yet. The apps owned by Sniffopotamus's owner (ScentSell, NotRealSmart and Underground Parfums) are not charged; every other business is by arrangement for now. Every business has a monthly allowance of cards, whatever it pays.
Who can do what, today:
- Only the owner of the business record (the person who signs in) can see, create, rotate and revoke its keys. Staff accounts come later. If a developer is building for you, the owner creates the key and passes it to them privately.
- Whether your business may use the partner API is switched on by us, and today that switch sits on the owner's own Sniffopotamus account rather than on a separate business record. In practice: the person we set up must stay the one who signs in. If that person changes, email us. Business records with their own switch come later, and nothing you build needs to change when they do.
1. Sign in
Sign in, or make an account, at www.sniffopotamus.com/login, using the email address the business owner will keep using.
2. Ask us to set up your business
Email sniff@sniffopotamus.com with:
- your business's name and website
- the email address you signed in with in step 1
- what you want to build (for example "notes on our product pages")
We set up the business record, switch the partner API on for it, and email you back.
3. Create a key in the Builder Console
Go to www.sniffopotamus.com/dev/console while signed in. Your business appears under Your business connection, with three parts: Keys, Usage and Use it.
Under Keys, in What the new key is for, type where the key will live (such as "Production server" or "Staging") and press Create key.
The key starts with sniffo_pk_ and is shown once. Press Copy key and
put it straight into your server's secret settings (step 4), then press
I have stored it. We keep only a fingerprint of the key, so we cannot show
it again: if you lose it, create a new one and revoke the old one.
You can hold several working keys, for example one per environment. They all share your business's one allowance, and a new key never resets it.
4. Keep the key on your server
Put the key in a server-side secret named SNIFF_PARTNER_KEY:
- on Vercel or any Node host: an environment variable that is not prefixed
NEXT_PUBLIC_ - on Supabase: an Edge Function secret, entered in the dashboard under Edge Functions, Secrets
- on your own server: an environment variable or your secrets manager
Never put it in a browser, a single-page app, a mobile app, a Shopify theme, a storefront script, a tag manager, a repository, a log or a chat. The door sends no CORS headers, so a browser cannot call it anyway: your shopper's browser asks your server, and your server asks us.
5. Check the key: whoami
To try the calls below from a terminal, load the key without it landing in your shell history. Run this, paste the key, and press Enter (nothing shows as you paste):
read -rs SNIFF_PARTNER_KEY
Then:
curl -s https://www.sniffopotamus.com/api/partner/v1/whoami \
-H "Authorization: Bearer $SNIFF_PARTNER_KEY"
{
"ok": true,
"partner": "Your business",
"today": { "calls": 0, "public_hits": 0, "in_review": 0, "not_found": 0, "refused": 0 }
}
That is trimmed to the fields every version of whoami has carried; the answer
also points you on to capabilities and to these pages. whoami is free and
never uses your allowance, so put it in your health check.
If you get 401 with unknown_key instead, the secret was not copied exactly;
see Errors and refusals.
6. See what you can use: capabilities
curl -s https://www.sniffopotamus.com/api/partner/v1/capabilities \
-H "Authorization: Bearer $SNIFF_PARTNER_KEY"
This answer tells your code, or your AI coding assistant, what it needs without reading these pages: your business and plan, how many cards you have used this month, your monthly allowance and when the count starts again, every endpoint and whether it works today, what each card field means, the answer codes, and the rules. Trimmed to its list of endpoints:
{
"ok": true,
"endpoints": [
{ "name": "whoami", "method": "GET", "path": "/api/partner/v1/whoami", "status": "live" },
{ "name": "capabilities", "method": "GET", "path": "/api/partner/v1/capabilities", "status": "live" },
{ "name": "card", "method": "GET", "path": "/api/partner/v1/fragrances/{id}/card", "status": "live" },
{ "name": "search", "status": "planned" },
{ "name": "match", "status": "planned" },
"…"
]
}
live means it works now. planned means it is not built, and there is no
working address for it yet. The full planned list is on
What the partner API offers.
If card reads are not switched on for accounts yet, capabilities says so in
plain words, and the card endpoint answers 503 with unavailable. Your key,
whoami and capabilities still work, and nothing is charged.
capabilities is free and shares a ceiling of 60 a minute with whoami.
7. Fetch your first card
Every perfume has an id. Each strength is its own perfume with its own id, so an Eau de Parfum and an Extrait of the same name have different ids.
Finding ids today. Until search and matching are built, a person finds each
id by hand: search on
www.sniffopotamus.com/explore, open the
perfume, check the strength, and take the last part of its address. The worked
example in these pages is at
https://www.sniffopotamus.com/fragrances/03c188b7-74aa-469d-ae40-caa8c45673b7:
Xerjoff, Casamorati - Casafutura, Eau de Parfum, id
03c188b7-74aa-469d-ae40-caa8c45673b7. Store each id against your own product
once, and your server looks it up from then on. Matching a whole product list
at once is planned.
curl -s https://www.sniffopotamus.com/api/partner/v1/fragrances/03c188b7-74aa-469d-ae40-caa8c45673b7/card \
-H "Authorization: Bearer $SNIFF_PARTNER_KEY"
You get { "ok": true, "status": "public", "card": { … } }. Every field is
explained, with this card in full, on The card answer. This is the
one answer that uses a call.
8. Ask from your own server, and keep a stored copy
In production, the shopper's browser never sends a Sniffopotamus id. It asks your server about your own product or listing, by your own id. Your server looks up which perfume that product is linked to, in your own database, and only then asks us. Otherwise anyone could use your server, and your allowance, to read the whole catalogue.
Your server also keeps a stored copy of each card. The rule, which is the same everywhere in these pages:
- Serve a stored copy for up to 24 hours without asking us.
- If we answer with an error, or your month's allowance is used up, keep serving it for up to 7 days from when you fetched it.
- If we answer
in_reviewornot_found, drop it at once.
The full reasons are on Limits and caching.
A table for the stored copies, in Postgres (use your own database):
create table sniff_card_copies (
fragrance_id uuid primary key, -- the Sniffopotamus id
card jsonb not null, -- the card exactly as we sent it
fetched_at timestamptz not null -- when we sent it
);
-- Only your server reads or writes it.
In Node, as a Next.js route handler (any server works the same way):
// app/api/products/[productId]/notes/route.ts, on YOUR server.
// The browser sends YOUR product id. Never a Sniffopotamus id, never the key.
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!);
const SNIFF = "https://www.sniffopotamus.com/api/partner/v1";
const HOUR = 60 * 60 * 1000;
const FRESH = 24 * HOUR; // serve a stored copy without asking for 24 hours
const FALLBACK = 7 * 24 * HOUR; // on an error or a used-up month, keep serving it up to 7 days
export async function GET(_req: Request, { params }: { params: Promise<{ productId: string }> }) {
const { productId } = await params;
// 1. Your own link, from your own database. Only products a shopper may see.
const [product] = await sql`
select sniff_fragrance_id from products where id = ${productId} and published`;
const sniffId: string | undefined = product?.sniff_fragrance_id;
if (!sniffId) return Response.json({ ok: false, reason: "not_linked" });
// 2. A stored copy under 24 hours old costs nothing.
const [copy] = await sql`
select card, fetched_at from sniff_card_copies where fragrance_id = ${sniffId}`;
const age = copy ? Date.now() - new Date(copy.fetched_at).getTime() : Infinity;
if (copy && age < FRESH) return Response.json({ ok: true, card: copy.card });
// 3. Ask Sniffopotamus.
let body: any = null;
try {
const res = await fetch(`${SNIFF}/fragrances/${sniffId}/card`, {
headers: { Authorization: `Bearer ${process.env.SNIFF_PARTNER_KEY}` },
signal: AbortSignal.timeout(8000),
cache: "no-store",
});
body = await res.json().catch(() => null);
} catch {
body = null; // no answer at all: treated like an error on our side
}
if (body?.ok === true) {
await sql`
insert into sniff_card_copies (fragrance_id, card, fetched_at)
values (${sniffId}, ${sql.json(body.card)}, now())
on conflict (fragrance_id) do update set card = excluded.card, fetched_at = excluded.fetched_at`;
return Response.json({ ok: true, card: body.card });
}
const code = typeof body?.error === "object" ? body.error.code : body?.status ?? "unavailable";
// 4. Still being checked, or gone: drop the stored copy at once.
if (code === "in_review" || code === "not_found") {
await sql`delete from sniff_card_copies where fragrance_id = ${sniffId}`;
return Response.json({ ok: false, reason: code });
}
// 5. Any other answer (a ceiling, a used-up month, your key, our side): keep serving
// a copy up to 7 days old, and tell a person.
console.error("[sniff] card not refreshed", sniffId, code);
if (copy && age < FALLBACK) return Response.json({ ok: true, card: copy.card });
return Response.json({ ok: false, reason: code });
}
In PHP, with PDO and the same table:
<?php
// On YOUR server. $productId is your own product id; the browser never sends a Sniffopotamus id.
function sniff_card(PDO $db, string $productId): ?array {
$q = $db->prepare('select sniff_fragrance_id from products where id = ? and published');
$q->execute([$productId]);
$sniffId = $q->fetchColumn();
if (!$sniffId) return null; // not linked: show your own page
$q = $db->prepare('select card, fetched_at from sniff_card_copies where fragrance_id = ?');
$q->execute([$sniffId]);
$copy = $q->fetch(PDO::FETCH_ASSOC) ?: null;
$age = $copy ? time() - strtotime($copy['fetched_at']) : PHP_INT_MAX;
if ($copy && $age < 24 * 3600) return json_decode($copy['card'], true); // fresh: no call
$ch = curl_init("https://www.sniffopotamus.com/api/partner/v1/fragrances/{$sniffId}/card");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SNIFF_PARTNER_KEY')],
CURLOPT_TIMEOUT => 8,
]);
$raw = curl_exec($ch);
curl_close($ch);
$body = is_string($raw) ? json_decode($raw, true) : null;
if (!empty($body['ok'])) {
$db->prepare('insert into sniff_card_copies (fragrance_id, card, fetched_at) values (?, ?, now())
on conflict (fragrance_id) do update set card = excluded.card, fetched_at = excluded.fetched_at')
->execute([$sniffId, json_encode($body['card'])]);
return $body['card'];
}
$code = is_array($body['error'] ?? null) ? $body['error']['code'] : ($body['status'] ?? 'unavailable');
if ($code === 'in_review' || $code === 'not_found') { // drop it at once
$db->prepare('delete from sniff_card_copies where fragrance_id = ?')->execute([$sniffId]);
return null;
}
error_log("sniff card not refreshed: {$sniffId} {$code}");
return ($copy && $age < 7 * 24 * 3600) ? json_decode($copy['card'], true) : null; // up to 7 days
}
With a stored copy, each perfume you show costs at most about one call a day,
however many shoppers open it, and a slow moment on our side never slows your
page. Before you go live, add a per-address limit to your own route (for
example 60 a minute) so nobody can loop over your products to spend your
allowance, and send the console.error or error_log line to a person.
9. Show it
A simple, good card:
- a heading, such as Notes / Accords
- for each item in
tiers: itsheading(Top, Heart, Base), then each note'spicturewith itsnameunderneath, using the name as the picture'salttext; a note with no picture shows its name only description.textas plain words, with no label under or beside itbrand.name,name,strengthandyearwherever your page already names the perfume
Leave out any block the card does not have. Show the pictures as they are, from the address in the card. See Rules and terms of use for the few things you must not do.
Test with your own app walks through a complete integration, ScentSell's, including the page and the checks to run before you go live.
Changing a key
In the Builder Console, under Keys, press Rotate beside the key and choose:
- Leaked: stop the old key now. For a key that was exposed (pasted into a browser, a repository, a log or a chat). You get a new key, and the old one is refused from its very next request. Put the new key on your server straight away.
- Routine: create a new one, then retire the old. For a planned change. You
get a new key and the old one keeps working. Put the new key on your server,
check it with
whoami, then stop the old key: straight after the rotation its button reads Retire old key; if you have reloaded the page since, it reads Revoke. Both do the same thing.
Revoke stops a key at once without making a new one. Your allowance belongs to the business, so a new key never resets it.
If a key leaks
Rotate it with Leaked: stop the old key now, then put the new key on your server. Do it before anything else: every minute the old key works, someone else can spend your allowance with it.
Only the business's owner can do this today. If the owner cannot be reached, email sniff@sniffopotamus.com with your business's name and the key's label (never the key itself) and we will stop it.
Getting help
Email sniff@sniffopotamus.com. For a problem
with a call, include the time with its time zone, the address you called, the
HTTP status and the error.code. Never send your key.