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 endpoint is a GET, returns JSON, and paginates
the same way.
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/connect-agent).
Keys start with bp_mcp_ and the same key works for the MCP server — one credential
for both surfaces.
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.
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/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. |
An id belonging to another account returns 404, not 403 — the API does not confirm that an id
it will not serve exists.
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.
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 nextFetchTime carry the
detail behind it.
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) orprevious.previousreturns404on a first change, which has no prior state.format—markdown(default),html, orscreenshot.screenshotanswers with a302to 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. |
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 write verb on this read-only API. |
rate_limited |
429 | Too many requests for this key. |
internal |
500 | An error on our side. |
The API is read-only: GET and the OPTIONS preflight are the only methods it serves. Anything
else — POST, PUT, PATCH, DELETE — answers 405 method_not_allowed with an Allow header,
in the same error envelope.
Rate limits
Reads are limited per API key, in a fixed window of 300 requests per minute.
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. |
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.