All docs

REST API

Read the item-level change history of any page you monitor as JSON — typed before/after values, cursor pagination, Bearer auth, and a published OpenAPI 3.0 spec.

Most monitors hand you a page diff and leave the interpretation to you. BriefPanel resolves a page into the items that repeat on it — a listing, a coin, a job, a plan — and records each change as a typed update: which field moved, from what to what, and how much it matters.

This API is the read side of that history: every read is a GET, returns JSON, and paginates the same way. It also has one write — resuming a paused page — so an agent that paused a page can start it again without a browser.

Base URL

https://actions.briefpanel.com/v1

Authentication

Send a personal API key as a Bearer token:

Authorization: Bearer YOUR_BRIEFPANEL_API_KEY

Create a key under Settings → API & integrations in the app (https://app.briefpanel.com/integrations). Keys start with bp_mcp_ and the same key works for the MCP server — one credential for both surfaces.

Keys carry scopes: every GET needs read, and the write endpoint needs write. A read-only key gets 403 on a write.

OAuth access tokens (bp_oat_) are not accepted here. They are audience-bound to the MCP resource, so they authenticate MCP calls only.

A missing or invalid key returns 401 with a WWW-Authenticate header. A live MCP OAuth token returns 401 too, but with its own message: the credential works on the MCP server, and this API needs a personal API key instead. Nothing is wrong with the token.

Quick start

List what you monitor. If this returns rows, your key works:

curl https://actions.briefpanel.com/v1/sources \
  -H "Authorization: Bearer YOUR_BRIEFPANEL_API_KEY"

Then read what changed. This is the endpoint most integrations poll:

curl "https://actions.briefpanel.com/v1/updates?importance=material&limit=50" \
  -H "Authorization: Bearer YOUR_BRIEFPANEL_API_KEY"

A trimmed response — the full shape is in the OpenAPI document:

{
  "data": [
    {
      "id": "j57e...",
      "at": 1750000000000,
      "updateType": "modified",
      "importance": "material",
      "field": "price",
      "valueChange": {
        "type": "number",
        "previous": 149.9,
        "current": 129.9,
        "unit": "BRL"
      },
      "entry": { "id": "k17c...", "title": "Plano Pro", "kind": "pricing plan" },
      "source": { "id": "m92a...", "url": "https://example.com/pricing" }
    }
  ],
  "pagination": { "nextCursor": "eyJ...", "hasMore": true }
}

Endpoints

Endpoint Query parameters Returns
GET /v1/account — The account behind your key, with the credit balance left on it.
GET /v1/sources limit, cursor The pages you monitor, newest first, each with its derived status and the entry kinds learned for that page.
GET /v1/sources/{id}/entries limit, cursor The items currently resolved on one page, most recently updated first. In-flight onboarding stubs are omitted.
GET /v1/updates since, importance, limit, cursor Every item-level change across all your sources, newest first. The feed to poll.
GET /v1/entries/{id}/updates field, importance, limit, cursor One entry's full event stream. Filter by field for a single field's history — the read behind a price chart.
GET /v1/entries/{id}/changes limit, cursor One card per fetch that changed this entry: diff churn counts, the derived price move, and pointers to the captured evidence.
GET /v1/changes/{id}/snapshot state, format The captured page state behind a change. Returns bytes, not the JSON envelope.
POST /v1/sources/{id}/resume — Start a paused page again. Needs the write scope.

An id belonging to another account returns 404, not 403 — the API does not confirm that an id it will not serve exists.

Two reads are on the MCP server only for now: a source's writable settings (userPrompt, notificationsEnabled, updateSensitivity), and narrowing the update feed to one source. GET /v1/sources carries health and cadence, and GET /v1/updates is account-wide.

Resuming a paused page

POST /v1/sources/{id}/resume takes no body and answers with the page's monitoring state:

{
  "data": {
    "id": "m92a...",
    "status": "active",
    "alreadyActive": false,
    "nextFetchTime": 1789648163769
  }
}

Checks restart on the page's own monitoringFrequency and spend credits again, one per page read. A page that was paused for longer than its frequency is due immediately, so nextFetchTime can be in the past.

Resuming a page that is already being checked is a success, not a conflict: alreadyActive is true, nothing is changed, and no extra check is scheduled — so a retry is safe.

status is the page's stored state, not a forecast — the same value GET /v1/sources reports. It comes back creditPaused only for a page that was already credit-paused. Resuming a paused page on an account with no credits answers active, and the scheduler re-parks it as creditPaused on the next check it comes due for. To know whether checks will actually run, read the credit balance rather than this field.

Resuming does not rebaseline. The page is still compared against how it looked when it was paused, so the first check after a long pause can report everything that changed in between. That is deliberate: the alternative silently adopts those changes as the new normal, and you could never see them. Items collected before the pause are untouched and are not re-notified.

Pagination

Every collection takes limit and cursor and answers with a pagination object:

{ "pagination": { "nextCursor": "eyJ...", "hasMore": true } }

Pass nextCursor back as cursor to get the next page, and keep going until hasMore is false.

A cursor belongs to the request that issued it: the same endpoint, account and filters. Keep the filters the same from page to page; limit is the only parameter you may change. A cursor sent with a different since, sourceId or field, with a different importance on GET /v1/updates (without sourceId), or from another account, answers 400 invalid_request. Retrying the same request never helps. Drop cursor to start again from the first page, for example when a poll moves since forward.

Treat hasMore as the only end-of-collection signal. A page can come back empty, or shorter than your limit, while there is still more to read — stopping on a short page will silently truncate your sync.

Polling the feed

GET /v1/updates is ordered newest first. Read at off the newest item and pass it back as since on the next poll to get only what has moved since:

curl "https://actions.briefpanel.com/v1/updates?since=1750000000000" \
  -H "Authorization: Bearer YOUR_BRIEFPANEL_API_KEY"

since accepts epoch milliseconds (the form at returns) or an ISO-8601 instant. importance accepts critical, material, or minor.

Each item carries its parent entry and source inline, so an update is actionable without a second call.

Outbound webhooks

If you do not want to poll, add a public HTTPS address that your system owns under Settings → API & integrations → Outbound webhooks. That address is not a BriefPanel API route: BriefPanel sends an HTTP POST to it whenever a new item-level content.update event is recorded.

The body reuses the canonical update fields from GET /v1/updates, including updateType, importance, source and entry context, and the field plus typed valueChange pair. Its webhook envelope also carries the stable event id and occurredAt.

Every request includes:

Header Meaning
BriefPanel-Event-Id Stable content_updates id; unchanged across attempts. Use it as your idempotency key.
BriefPanel-Timestamp Delivery-attempt time in Unix seconds. Use it to reject stale signed requests.
BriefPanel-Signature v1=<hex HMAC-SHA256> of <event-id>.<timestamp>.<raw-body>, keyed by the signing secret shown once when the endpoint is created.

A 2xx response completes the delivery. Timeouts, network/DNS failures, 408, 425, 429, and 5xx responses are attempted up to five times with exponential delays of 1, 2, 4, and 8 minutes. Other responses, including redirects, fail immediately. Delivery is at least once, so receivers must deduplicate by event id. A receiver failure never rolls back content ingestion or blocks another endpoint. The app shows the latest delivery status and attempt count; finished delivery records expire after 30 days.

Entry kinds

Each entry carries a kind — pricing plan, job posting, cryptocurrency. The names are learned per source, so compare them only within one source; the vocabulary for a given page is published as entryKinds on that source, along with hasPrice so you know when a missing price is meaningful.

entryKinds is absent until a source has been analyzed.

Source health

GET /v1/sources returns a derived status:

status Meaning
active Being checked on schedule.
autoPaused Paused after repeated fetch failures.
creditPaused Paused for lack of credits.
disabled Switched off by you.

lastFetchOutcome, lastFetchErrorReason, consecutiveFetchFailures and nextCheckTime carry the detail behind it.

nextCheckTime is when the next check is expected, and it is absent when there is none to report rather than a time nothing will act on. That is two states, and status separates them: an autoPaused or disabled page has no check coming at all, while an active page that has never been checked has none to name until its first one lands. Checks are started by a scheduler that ticks on a fixed interval, so the check lands at or after that time, and a page set to a cadence faster than the tick is still read at the tick: effectiveMonitoringFrequency is that cadence, beside the monitoringFrequency you stored.

Credits

GET /v1/account returns what is left on the account:

curl https://actions.briefpanel.com/v1/account \
  -H "Authorization: Bearer YOUR_BRIEFPANEL_API_KEY"
{ "data": { "credits": 420 } }

A check spends one credit per page it reads — the page you monitor, plus each item page it follows. At zero, checks stop and every source reports creditPaused.

This is the read that answers "will my pages actually be checked?". A source's status reports what is stored right now, not what the next check will decide: a source can still read active on an empty account and be parked as creditPaused on its next due check. The balance is what tells the two apart ahead of time.

It is a point-in-time reading, like any balance: it says whether checks can run now, not what the next check will decide. The same number is on the MCP server as get_credit_balance.

Evidence

GET /v1/changes/{id}/snapshot serves the raw page state a change was derived from, so you can show where a value came from rather than asking someone to trust it.

Snapshots are deduped across accounts and have no owner of their own, so they are addressed through the change they belong to rather than by their own id. Take changeId from a timeline card and check hasCurrentSnapshot / hasPreviousSnapshot before requesting a state.

  • state — current (default) or previous. previous returns 404 on a first change, which has no prior state.
  • format — markdown (default), html, or screenshot. screenshot answers with a 302 to a short-lived signed image URL.

Errors

Failures return an error object:

{ "error": { "code": "not_found", "message": "No source with that id." } }

Branch on code rather than on the HTTP status — the set is fixed, each code answers with exactly one status, and neither the set nor that pairing will change under you. message is written for humans and may be reworded; do not parse it.

code HTTP When
invalid_request 400 An unparseable or out-of-range parameter.
unauthorized 401 Missing, malformed, or revoked key — or an MCP OAuth token, which this surface does not take.
forbidden 403 The key lacks the scope this endpoint needs.
not_found 404 No such id, including one that belongs to another account.
method_not_allowed 405 A method this endpoint does not serve.
rate_limited 429 Too many requests for this key.
internal 500 An error on our side.

GET, the OPTIONS preflight, and POST on the write endpoint above are the methods this API serves. PUT, PATCH and DELETE answer 405 method_not_allowed, and so does a POST to a path that only reads — the Allow header names what that path does serve, in the same error envelope.

Rate limits

Reads are limited per API key, in a fixed window of 300 requests per minute. Writes have their own budget of 60 per minute, so a write loop never spends the allowance a poller needs, or the other way round.

Every response the limit counted carries the current quota, so you can pace a client instead of discovering the ceiling by hitting it:

Header Meaning
X-RateLimit-Limit Requests allowed per window, in the bucket this request was counted against.
X-RateLimit-Remaining Requests left in this window, after the one you just made.
X-RateLimit-Reset Seconds until the window resets — a delta, not a Unix timestamp.

These headers accompany a counted response, which includes the errors a request had to reach a handler to earn: a 400 or 404 reports the quota just like a 200. A request rejected for a bad key (401/403) is turned away before the limit is counted, so it reports none.

Treat their absence as unknown, never as zero. They are also omitted if our limiter is momentarily unreachable, in which case the request is served without being counted.

Over the limit you get 429 with a Retry-After header, in seconds and always at least one — honor it rather than retrying immediately.

OpenAPI

The full specification is served as a live OpenAPI 3.0 document:

https://actions.briefpanel.com/v1/openapi.json

Import it into Postman, point an SDK generator at it, or hand the URL to an agent. It describes every endpoint, the auth scheme, the pagination contract, and the complete response schemas.

It is served unauthenticated on purpose — tooling has to be able to read an API's description before it holds a key.

Two ways in, one API key. Read the typed history of every item on any page you monitor, or hand the same credential to an agent.

Get your API key