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.
AI agents connect to the MCP server with the same keys. A few operations, such as creating API keys, need a workspace owner signed in to the app. The reference marks those Owner.
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, so an AI agent can read your mentions, triage them and run checks itself.
- Send an API key as
Authorization: Bearer ss_…. With a read-only key (ss_ro_…), only the read tools are listed. EU keys work the same way. - Start with
get_brief: what needs attention today, most urgent first, each post with its one-line reason. sniff_brandchecks any brand, including ones the workspace doesn't track, on the free platforms (10 a day per workspace).- One-click sign-in for ChatGPT and Claude.ai connectors (OAuth) is next. Until then, use a client that can send a header.
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 --header "Authorization: Bearer $SNIFFINGTON_API_KEY"
Cursor, Windsurf, VS Code and other clients configured with JSON:
{
"mcpServers": {
"sniffington": {
"url": "https://sniffington.com/mcp",
"headers": {
"Authorization": "Bearer ss_ro_…"
}
}
}
}