Developers

PoppyPR API

Read your mentions, alerts and insights from your own tools, and have new alerts pushed to you. The API is read-only: a key can never change or delete anything.

Getting started

API access is part of the Agency & Enterprise plan. An owner or admin creates a key in the app under Integrations. The key is shown once, so store it somewhere safe.

Every address below starts with your PoppyPR site followed by /api/v1. Responses are JSON.

Your first request
curl https://YOUR-POPPYPR-SITE/api/v1/mentions \
  -H "Authorization: Bearer ppr_live_your_key"

Authentication

Send your key in the Authorization header on every request.

Authorization: Bearer ppr_live_your_key

Keys belong to one organisation and only ever return that organisation's data. Keys can only make GET requests; anything else is refused with 403. Revoke a key in the app and it stops working immediately. Treat a key like a password: keep it on your server, never in a web page or mobile app.

Mentions

Everything PoppyPR has matched to your brands, newest first, 30 at a time.

GET/api/v1/mentions
brandOnly this brand's mentions. Use an id from /brands.
scopeLeave out for your own brands. competitors for tracked competitors, all for both.
sentimentpositive, neutral, mixed, negative, or unrated for mentions that haven't been analysed.
platformnews, reddit, x, youtube or reviews.
productOnly mentions tagged with this product name.
statusnew, reviewed or dismissed. Leave out for everything not dismissed; all for everything.
qSearch headlines and publishers.
beforeFor the next page: the next_before value from the previous response.
Response
{
  "mentions": [
    {
      "id": "1fa0d8e0-f77a-45cb-bcd9-9d4fca407d52",
      "brand_id": "8ce6304a-fd69-47f2-a0b6-104dfbb334bf",
      "brand_name": "Loomwell",
      "title": "Loomwell issues recall of heated blankets",
      "url": "https://news.example.com/loomwell-recall",
      "publisher": "Example Wire",
      "author": "Dana Reporter",
      "platform": "news",
      "published_at": "2026-11-10T14:02:00Z",
      "created_at": "2026-11-10T14:09:31Z",
      "status": "new",
      "analysis_state": "done",
      "sentiment": "negative",
      "severity": "high",
      "summary": "Loomwell is recalling its heated blankets after overheating reports.",
      "topics": ["recall", "product safety"],
      "products": ["Heated Blanket"],
      "recommended_action": "Publish clear recall instructions.",
      "competitor": false
    }
  ],
  "next_before": "2026-11-10T14:09:31.414705Z"
}

next_before is null on the last page. analysis_state is done when AI analysis ran; otherwise sentiment is empty and the mention was kept on its name match alone. severity runs none, low, elevated, high, critical.

To download up to 5,000 mentions as a spreadsheet, call /mentions/export with the same filters. It returns CSV.

Alerts

Issues PoppyPR has flagged, open ones first.

GET/api/v1/alerts
statusopen, acknowledged or resolved. Leave out for all.
GET/api/v1/alerts/{id}

One alert, with the mentions behind it in mentions and the drafted response plan in response_plan if one exists.

Response
{
  "alerts": [
    {
      "id": "0b6c1d2e-93a4-4f0e-8d5b-2a1f7c9e4b10",
      "brand_id": "8ce6304a-fd69-47f2-a0b6-104dfbb334bf",
      "brand_name": "Loomwell",
      "kind": "crisis",
      "title": "Loomwell issues recall of heated blankets",
      "summary": "A safety recall is being widely reported.",
      "detected_by": "ai",
      "status": "open",
      "created_at": "2026-11-10T14:09:32Z",
      "mention_ids": ["1fa0d8e0-f77a-45cb-bcd9-9d4fca407d52"],
      "response_plan": null
    }
  ]
}

kind is crisis, rising or notable (good news). detected_by is ai, keywords (alarming words in a headline while AI was unavailable) or volume (negative coverage arriving much faster than usual).

Insights

The figures behind the Insights page for a period: totals, sentiment, sources, topics, products, a day-by-day series, and every brand side by side including competitors.

GET/api/v1/insights
days7, 30 or 90. Defaults to 30.
brandLimit the figures to one of your brands.
Response (shortened)
{
  "days": 30,
  "stats": {
    "total": 131,
    "sentiment": { "positive": 40, "neutral": 61, "mixed": 8, "negative": 22, "unrated": 0 },
    "previous_total": 97,
    "previous_negative": 9,
    "alerts": { "crisis": 1, "rising": 4 },
    "platforms": [{ "name": "news", "count": 104 }, { "name": "reddit", "count": 27 }],
    "publishers": [{ "name": "Example Wire", "count": 12 }],
    "topics": [{ "name": "recall", "count": 18 }],
    "products": [{ "name": "Heated Blanket", "total": 19, "positive": 3, "negative": 11 }],
    "series": [{ "date": "2026-11-10", "own": 14, "negative": 6, "positive": 2, "competitors": 9 }],
    "brands": [
      { "id": "8ce6…", "name": "Loomwell", "competitor": false, "total": 131,
        "positive": 40, "neutral": 61, "negative": 22, "mixed": 8, "open_alerts": 2 }
    ]
  }
}

Brands

Your brand profiles and the competitors you track, with their keywords and products.

GET/api/v1/brands
GET/api/v1/brands/{id}/products
Response from /brands
{
  "brands": [
    {
      "id": "8ce6304a-fd69-47f2-a0b6-104dfbb334bf",
      "name": "Loomwell",
      "kind": "business",
      "website": "loomwell.com",
      "handle": "",
      "terms": ["Loomwell", "Loom Well"],
      "exclude_terms": ["hotel"],
      "products": ["Heated Blanket", "Cloud Throw"],
      "is_competitor": false,
      "mentions_30d": 131
    }
  ]
}

RSS feeds

The feeds your organisation has added under RSS feeds, and the publications PoppyPR already reads for everyone. Feeds are added and removed in the app.

GET/api/v1/feeds
Response
{
  "feeds": [
    {
      "id": "5d1c2a77-0c1e-4c58-9a5e-3f0a8d1b6e42",
      "name": "Home Textiles Today",
      "url": "https://www.hometextilestoday.com/feed/",
      "added_at": "2026-11-02T09:15:00Z",
      "last_checked_at": "2026-11-11T08:30:12Z",
      "status": "ok",
      "items_30d": 84,
      "mentions_30d": 3
    }
  ],
  "limit": 25,
  "built_in": ["Ars Technica", "BBC News — Business", "TechCrunch"],
  "built_in_sources": [
    {
      "id": "cf141817-c62f-4dc5-a60d-d2eaef8b0811",
      "name": "TechCrunch",
      "url": "https://techcrunch.com/feed/",
      "website_url": "https://techcrunch.com",
      "category": "Tech",
      "last_checked_at": "2026-11-11T08:30:12Z",
      "status": "ok"
    }
  ]
}

status is waiting until the first check, then ok, or failing when the feed can't be read. Mentions found through a feed appear in /mentions like any other.

built_in_sources lists active shared feeds with publisher websites, topics and feed health. The app uses the publisher website to load its favicon. built_in keeps the names-only list for existing clients. Shared news is kept only when its headline or summary matches a monitored brand's keywords or website.

Errors

Errors use ordinary HTTP status codes and always have the same shape. message is written to be shown to a person.

{ "error": { "code": "bad_api_key", "message": "That API key is not valid." } }
401The key is missing, wrong or revoked, or the plan no longer includes API access.
403The request tried to change something. Keys are read-only.
402The feature isn't part of the organisation's plan.
404Nothing with that id belongs to your organisation.
429Too many requests. Your keys share 120 requests a minute; wait a moment and try again.

Webhooks

Add an https:// address under Integrations and PoppyPR sends a POST to it the moment an alert is raised. You are shown a signing secret once when you add it.

What we send
POST /your/endpoint
Content-Type: application/json
X-PoppyPR-Signature: t=1794362235,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "event": "alert.created",
  "alert": {
    "id": "0b6c1d2e-93a4-4f0e-8d5b-2a1f7c9e4b10",
    "kind": "crisis",
    "title": "Loomwell issues recall of heated blankets",
    "summary": "A safety recall is being widely reported.",
    "brand": "Loomwell",
    "created_at": "2026-11-10T14:09:32Z",
    "url": "https://YOUR-POPPYPR-SITE/app/alerts?id=0b6c1d2e-93a4-4f0e-8d5b-2a1f7c9e4b10"
  }
}

Answer with any 2xx status within 10 seconds. If your endpoint errors or times out, we retry a few times with increasing gaps; a 4xx answer (other than 429) is not retried.

Check the signature before trusting a delivery. Take the t value, a full stop, and the exact request body; compute an HMAC-SHA256 of that with your signing secret; compare it to v1. Reject deliveries whose t is more than a few minutes old.

Node.js
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)

Slack: paste a Slack incoming-webhook address (it starts with https://hooks.slack.com/) and alerts arrive as formatted messages in that channel. Nothing else to set up.