Sniffington
OAuth-protected MCP server for accessing a Sniffington workspace’s public brand mentions and supported actions.
Hosted MCP Server
npx add-mcp 'https://sniffington.com/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
Sniffington API
Read and manage brand mentions over HTTP or MCP.
Overview
Sniffington finds public posts that mention your brand or keywords on Reddit, X, Hacker News, YouTube, TikTok and other platforms. An AI model drops the noise and tags each post with a sentiment and a category. This API reads and changes the same data you see in the app.
Base URL: https://sniffington.com/api/v1. Requests and responses are JSON. The OpenAPI 3.1 spec is at /openapi.json, and AI agents can start from /llms.txt.
Authentication
Create an API key in the app under Integrations → API keys, then send it as a bearer token with every request:
Authorization: Bearer ss_sk_…
ss_sk_…keys have full access and can read and write.ss_ro_…keys are read-only. A write with one returns 403.- Workspaces stored in the EU have keys that start
ss_sk_eu_…andss_ro_eu_…. They work the same way.
A key belongs to one workspace, so requests never name a workspace. The app shows a key only once. If one leaks, revoke it there and create a new one.
MCP clients such as ChatGPT and Claude can sign in with OAuth instead of using a key (see MCP). A few operations, such as creating API keys, need a workspace owner signed in to the app. The reference marks those Owner.
Agent sign-up
An AI agent can start its own account. A person still owns it: they approve once, by email, and the agent collects an API key. There's no password and no browser step for the agent.
1. The agent asks for an account for a person's email address:
curl -X POST https://sniffington.com/api/agent/signup -H "content-type: application/json" \
-d '{"email":"you@example.com","agent_name":"Claude","scope":"full"}'
scope is read or full (default full). region is us or eu; leave it out and it follows where the request comes from. The reply is 202 with a claim_id, a poll_secret and a poll_url.
2. The person gets an email with a link. They sign in with that address and choose the workspace, whether the agent may change things, and a daily budget. The link works for an hour, and only for the owner of that address.
3. The agent polls until the person has answered:
curl https://sniffington.com/api/agent/signup/<claim_id> -H "Authorization: Bearer <poll_secret>"
- 202
pending: keep waiting, a few seconds apart. - 200
approved: the body hasapi_key, shown once, plus the workspace,scopeanddaily_budget. Store the key now. - 403
denied: the person said no. 410: the link expired or the key was already collected. Start again.
Keys made this way carry a daily budget of 10, 25 or 100 costly actions: create_project, create_keyword, scan_now, scan_project and sniff_brand. Reads are unlimited. Past the budget a call returns 429 with the code key_budget, and the MCP tool whoami shows how many are left. Anyone can set the same cap on a key they create by passing daily_budget to create_api_key. The plan's own limits apply as well.
To move up a plan, an agent with a read and write key calls request_upgrade (POST /v1/billing/upgrade-link). It returns a payment link for the account owner to open. Nothing is charged until they pay, and the plan changes when Polar confirms it.
Sign-ups are limited to 3 a day per email address and 10 a day per IP address.
Quickstart
Keep the key in an environment variable:
export SCOUT_API_KEY=ss_sk_…
List your projects. Each has an id that other calls use:
curl https://sniffington.com/api/v1/projects \
-H "Authorization: Bearer $SCOUT_API_KEY"
Add a keyword to a project. Aliases count as the same keyword, and posts that contain an excluded term are skipped:
curl -X POST https://sniffington.com/api/v1/projects/acme/keywords \
-H "Authorization: Bearer $SCOUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"term": "acme", "aliases": ["acme.com", "@acmehq"], "excluded": ["acme corp"]}'
List negative mentions from Reddit and X that nobody has handled yet, 50 at a time:
curl "https://sniffington.com/api/v1/mentions?project=acme&sources=reddit,x&sentiment=negative&status=open&limit=50" \
-H "Authorization: Bearer $SCOUT_API_KEY"
Mark one as done, using the id from that list:
curl -X PATCH "https://sniffington.com/api/v1/mentions/MENTION_ID" \
-H "Authorization: Bearer $SCOUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "done"}'
The API reference lists every operation with its parameters.
Pagination
List operations return one page of results and a cursor for the next:
{ "data": [ … ], "next_cursor": "WzE3NTg…", "total": 1234 }
Pass next_cursor back as cursor to get the next page. It is null on the last page. limit sets the page size: 25 by default, 100 at most. A cursor marks a position in the list, so posts that arrive while you page don't shift or repeat results.
Errors
Every error has the same shape. code is stable, so branch on it. message is written for people and may change.
{ "error": { "code": "not_found", "message": "no project acme" } }
| Status | Code | When |
|---|---|---|
| 400 | bad_request, validation_error | A parameter or field is missing or has the wrong type. The message names it. |
| 401 | unauthorized | No credentials, or the key is wrong, expired or revoked. |
| 402 | plan_limit | The plan's limit on projects, keywords or seats is reached. |
| 403 | forbidden | The credential isn't allowed to do this, for example a read-only key trying to write. |
| 404 | not_found | Nothing with that id in this workspace. |
| 409 | conflict | The request clashes with the current state, for example a name that's already taken. |
| 415 | unsupported_media_type | A write from a signed-in browser that isn't JSON. |
| 429 | rate_limited | Too many requests. Wait for the number of seconds in Retry-After. |
A 5xx status means the server failed. Reads are safe to retry.
Rate limits
Each credential can make 240 reads (GET) and 60 writes per minute. Calls without a key, such as the public dashboard endpoints, are counted per IP address.
Past the limit you get a 429 with the code rate_limited. Wait a minute and try again.
Webhooks
A webhook sends events, such as a new mention, to your URL as JSON in a POST. Add endpoints in the app or with the webhook operations.
Each delivery is signed the Standard Webhooks way, with these headers:
webhook-id: the message id. Retries reuse it, so use it to skip duplicates.webhook-timestamp: when it was sent, in Unix seconds.webhook-signature: one or more space-separatedv1,<signature>values.
The signature is the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}. The key is your endpoint secret: drop the whsec_ prefix and base64-decode the rest. Check the signature against the raw body before you parse the JSON, and reject timestamps more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: the endpoint's "whsec_…" secret. headers: the request headers. body: the raw body as a string.
export function verifyWebhook(secret, headers, body) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"];
if (!id || !ts || !sigs) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = Buffer.from(createHmac("sha256", key).update(\`${id}.${ts}.${body}\`).digest("base64"));
return sigs.split(" ").some((part) => {
const [version, sig] = part.split(",");
const given = Buffer.from(sig || "");
return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
});
}
Reply with a 2xx status quickly and do slow work afterwards. Any other status, or no reply, counts as a failure, and the delivery is retried with growing gaps for about 90 hours.
MCP
Sniffington is also an MCP server, at https://sniffington.com/mcp over Streamable HTTP.
- ChatGPT, Claude and other clients that support OAuth 2.1 with dynamic client registration only need that URL. They register themselves and ask you to sign in, so there's no key to copy.
- Clients without OAuth can send an API key as
Authorization: Bearer ss_…. With a read-only key, only the read tools work.
Each operation marked MCP tool in the reference is a tool with the same name, and takes that operation's parameters and body fields as arguments.
Claude Code:
claude mcp add --transport http sniffington https://sniffington.com/mcp
A client configured with a key:
{
"mcpServers": {
"sniffington": {
"url": "https://sniffington.com/mcp",
"headers": {
"Authorization": "Bearer ss_ro_…"
}
}
}
}