Contents

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.

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" } }
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, 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_brand checks 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_…"
      }
    }
  }
}