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_… and ss_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 has api_key, shown once, plus the workspace, scope and daily_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" } }
StatusCodeWhen
400bad_request, validation_errorA parameter or field is missing or has the wrong type. The message names it.
401unauthorizedNo credentials, or the key is wrong, expired or revoked.
402plan_limitThe plan's limit on projects, keywords or seats is reached.
403forbiddenThe credential isn't allowed to do this, for example a read-only key trying to write.
404not_foundNothing with that id in this workspace.
409conflictThe request clashes with the current state, for example a name that's already taken.
415unsupported_media_typeA write from a signed-in browser that isn't JSON.
429rate_limitedToo 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-separated v1,<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_…"
      }
    }
  }
}