Docs

How to put oops in your app, report errors from anywhere, and read or triage the issues through the REST API or an AI agent. The full contract — every endpoint, object and rule — lives in the repository.

Read the full contract (CONTRACT.md) →

Browser SDK

A zero-dependency script under 4 KB gzipped, served from this host at /o.js (also /o/v1.js). Add it once in your layout: it captures window.onerror and unhandledrejection, keeps the last 30 breadcrumbs (clicks, SPA route changes, failed fetch / XHR calls) and posts events with sendBeacon. It never throws into your page, drops identical events within 2 s, sends at most 30 events per minute and skips localhost pages unless told otherwise.

<script defer src="https://oops.3m4.net/o.js" data-key="oops_pub_…"></script>

Attributes (defaults shown)

<script defer src="https://oops.3m4.net/o.js" data-key="oops_pub_…"
        data-env="production" data-release="1.4.2"
        data-console="false" data-local="false" data-sample="1"
        data-user='{"id": "u_42"}'></script>
  • data-key — the project's public key (required).
  • data-env / data-release — environment and release labels stamped on every event (environment defaults to production).
  • data-console — also capture console.error calls as events, and console.warn / console.error as breadcrumbs.
  • data-local — also report from localhost, raw IPs and file: pages (skipped by default).
  • data-sample — fraction of error events sent (0–1). Captured messages and explicit capture() calls always go through.
  • data-user — initial user as JSON ({ id, email, name }); same as calling oops.setUser().
  • data-api — override the ingest endpoint (self-hosted instances behind another host).

JavaScript API

window.oops exposes capture, message, setUser, setTag / setTags, setContext, breadcrumb and flush. Calls queued in window.oops.q before the script loads are replayed.

// report a caught error, with context
oops.capture(err, { level: "error", tags: { page: "checkout" }, extra: { cart: 3 }, fingerprint: ["checkout", "pay-failed"] });

// a plain message (info by default)
oops.message("Payment retried", "warning", { tags: { gateway: "stripe" } });

// who is affected (hashed per project; counted once per issue)
oops.setUser({ id: "u_42", email: "ada@example.com" });   // or null to forget

// tags and context merged into every later event
oops.setTag("plan", "pro");
oops.setTags({ ab: "variant-b" });
oops.setContext("cart", { items: 3, total: 42 });

// your own breadcrumbs (clicks, navigation, failed requests are automatic)
oops.breadcrumb({ category: "cart", message: "coupon applied", data: { code: "SPRING" } });

// queue calls before the script has loaded
window.oops = { q: [["setUser", { id: "u_42" }]] };

What is sent: type, message, the stack (parsed into frames), level, URL without its query string, release, environment, platform, the user you set, tags, extra and the breadcrumb buffer. Browser, OS and device are derived on the server from the request and the user agent is discarded.

Ingest API

POST /api/ingest (or /api/ingest/event) with the project's public key in the X-Project-Key header or a k field. JSON, form-encoded and text/plain bodies are accepted, CORS is open, bodies are capped at 64 KB, and the answer is 202 whenever the key is valid — paused projects, blocked origins and exceeded budgets return ok:false rather than an error, so a failing app never fails a second time.

curl -X POST https://oops.3m4.net/api/ingest \
  -H "X-Project-Key: oops_pub_…" -H "Content-Type: application/json" \
  -d '{
    "type": "TypeError", "message": "x is not a function", "level": "error",
    "stack": "TypeError: x is not a function\n    at init (https://app/x.js:12:4)",
    "url": "https://app.example/checkout?x=1",
    "release": "1.4.2", "environment": "production", "platform": "javascript",
    "user": { "id": "u_42" }, "tags": { "page": "checkout" }, "extra": { "cart": 3 },
    "breadcrumbs": [{ "ts": "2026-10-16T10:00:00Z", "category": "click", "message": "button#pay" }],
    "fingerprint": ["checkout", "pay-failed"],
    "ts": "2026-10-16T10:00:01Z"
  }'
  • type and message: the exception class / error name and its message. At least one is required.
  • level: fatal | error | warning | info (default error). environment (default production) and release are free-text labels used as filters.
  • frames (top of the stack first; filename, function, lineno, colno, in_app) or stack: a raw string parsed server-side (V8, Firefox / Safari and Python traceback formats).
  • url (query and fragment dropped), platform (javascript, python, node, …), sdk, user ({ id, email, name }), tags (≤ 20 pairs), extra (≤ 8 KB JSON).
  • breadcrumbs: up to 30 of { ts, category, message, level, data }; the last 30 are kept.
  • fingerprint: up to 5 strings that fully replace the automatic grouping. ts: optional client timestamp (server time if missing or more than 5 minutes off).

Responses

Stored events return the event id and the issue they landed in. Dropped events say why: paused (ingest switched off for the project), origin (allowed hostnames set and the page / Origin host is not in them) or limit (over the monthly budget — counted, not stored). Malformed payloads are 400, a bad key is 401.

202 { "ok": true, "event_id": "9021", "issue_id": "…", "issue_number": 12 }
202 { "ok": false, "reason": "paused" | "origin" | "limit" }
400 { "error": { "code": "invalid_event", "message": "…" } }
401 { "error": { "code": "invalid_project_key", "message": "…" } }

Batches

POST https://oops.3m4.net/api/ingest/batch
{ "events": [ { … }, { … } ] }           # ≤ 20 events → { "ok": true, "results": [ … ] }

Grouping rules

Every event lands in exactly one issue, keyed by a fingerprint:

  • If the event carries a fingerprint array, that wins — use it to group by business meaning (['checkout', 'pay-failed']).
  • Otherwise the fingerprint is type + normalised message + top in-app frame (filename without query string + function). Normalisation replaces runs of digits, hex blobs ≥ 8 chars, UUIDs and quoted strings with placeholders and truncates to 200 chars.
  • A resolved issue that receives a new event reopens and is marked regressed. An ignored issue stays ignored but keeps counting.
  • Affected users = distinct user hashes: your user.id (hashed per project) when you send one, otherwise a daily cookie-less hash of IP and user agent.

Base URL

https://oops.3m4.net/api

Authentication

Three kinds of credentials, each for one job:

  • Dashboard sessions: JWT bearer tokens issued by /auth/login. Hold every scope.
  • Management API and MCP: API keys with a role (viewer / editor / admin). Header X-API-Key: oops_live_… (or Authorization: Bearer).
  • Ingest API and SDK: project public keys. Header X-Project-Key: oops_pub_… (or the k field). They identify one project, can only report, and can be rotated at any time.

Projects

A project is one app, site or service. It owns a public key, a time zone (used to bucket days), an optional list of allowed hostnames, the ingest switch and the store-IP switch.

MethodPathScopeNotes
GET/projectsprojects:readwith open issues, events 24h / 30d, users 30d
POST/projectsprojects:write{name, key?, description?, url?, allowed_origins?, timezone?} → 201 · 402 limit_reached
GET/projects/:idprojects:read
PATCH/projects/:idprojects:writename, description, url, timezone, allowed_origins, ingest_enabled, store_ip
DELETE/projects/:idprojects:deletecascades issues and events
POST/projects/:id/rotate-keyprojects:writeold public key stops immediately
GET/projects/:id/summaryprojects:readopen_issues, events_24h, events_30d, users_30d, last_event_at
DELETE/projects/:id/eventsevents:deletepurge every event and issue
curl -X POST https://oops.3m4.net/api/projects \
  -H "X-API-Key: oops_live_…" -H "Content-Type: application/json" \
  -d '{"name": "My app", "url": "https://myapp.example.com", "timezone": "Europe/Madrid"}'

Issues and events

Issues are paged (per_page ≤ 100) and default to the open ones seen in the last 30 days, most recent first. An issue is addressed by its uuid or its per-project number. Bulk updates take up to 100 ids. Events are the raw occurrences, newest first, across one issue or the whole project.

MethodPathScopeNotes
GET/projects/:id/issues?status=&level=&environment=&release=&q=&period=&sort=&page=issues:readstatus open (default) | resolved | ignored | all · period 24h | 7d | 30d (default) | 90d | all · sort last_seen | first_seen | events | users
GET/projects/:id/issues/:issueissues:readuuid or #number · includes latest_event
PATCH/projects/:id/issues/:issueissues:write{status: open | resolved | ignored}
POST/projects/:id/issues/bulkissues:write{ids: [≤ 100], status} → {updated}
GET/projects/:id/issues/:issue/events?page=issues:readoccurrences, newest first
GET/projects/:id/events?level=&environment=&release=&q=&issue=&period=&page=issues:readraw events across the project
GET/projects/:id/events/:event_idissues:read
curl "https://oops.3m4.net/api/projects/$PROJECT/issues?status=open&period=7d&sort=users" \
  -H "X-API-Key: oops_live_…"

curl -X PATCH "https://oops.3m4.net/api/projects/$PROJECT/issues/12" \
  -H "X-API-Key: oops_live_…" -H "Content-Type: application/json" \
  -d '{"status": "resolved"}'

Stats, usage and audit

period=today|yesterday|24h|7d|30d|90d|12mo or from/to (dates in the project time zone); interval hour | day | month is chosen automatically. level, environment and release filter everything in the answer: totals and the previous period of the same length, the time series, and the top-10 breakdowns.

MethodPathScopeNotes
GET/projects/:id/stats?period=&from=&to=&interval=&level=&environment=&release=issues:readtotals + previous period, series, top-10 breakdowns by level · environment · release · browser · os · device · type
GET/projects/:id/liveissues:readevents in the last few minutes
GET/account/usageusage:readevents this month, dropped, counts, limits
GET/account/audit?action=&actor_type=&outcome=&since=&page=audit:read
curl "https://oops.3m4.net/api/projects/$PROJECT/stats?period=7d&environment=production" \
  -H "X-API-Key: oops_live_…"

Dimensions

Returned as breakdowns by GET /stats. level, environment and release are also accepted as filters on the issues, events and stats endpoints.

level · environment · release · browser · os · device · type

Errors, limits and privacy

Errors are JSON: { "error": { "code", "message" } }. A missing scope is 403 insufficient_scope. Creating past a plan limit answers 402 limit_reached with error.which = projects | api_keys. Events over the monthly budget are accepted (202) but not stored, and counted as dropped in /account/usage.

Payload caps: type ≤ 200, message ≤ 4096, frames ≤ 50, stack ≤ 16 KB, tags ≤ 20 pairs (key ≤ 40, value ≤ 200), extra ≤ 8 KB, breadcrumbs ≤ 30 / 8 KB, fingerprint ≤ 5 × 100, environment ≤ 40, release ≤ 100. Request bodies ≤ 64 KB; a batch holds ≤ 20 events.

Free plan: 3 projects, 5,000 events per month and 30 days of retention; events older than the plan's window are pruned, and issues left with no events disappear with them.

Privacy: keys matching password, passwd, secret, token, authorization, cookie, api_key, card, cvv or ssn are replaced by [Filtered] in tags, extra, breadcrumbs and user; bearer tokens and card numbers inside the message too. URLs lose query string and fragment. The user agent is reduced to browser / OS / device and discarded. IPs are hashed with a daily salt unless the project sets store_ip.

MCP (AI agents)

The MCP server (npm oops-errors-mcp, or Streamable HTTP at /mcp) exposes the management API as tools, filtered by the role of the key you connect with and re-checked on every call. Destructive tools ask for confirmation. report_error lets an agent post an event into a project it can see.

Local (stdio, npm)
claude mcp add oops --scope user --env OOPS_API_KEY=oops_live_… -- npx -y oops-errors-mcp
Remote (Streamable HTTP)
claude mcp add --transport http oops https://oops.3m4.net/mcp --header "Authorization: Bearer oops_live_…"
ToolScope
list_projects, get_projectprojects:read
create_project, update_project, rotate_public_keyprojects:write
delete_project (confirm)projects:delete
list_issues, get_issue, list_issue_events, get_event, search_eventsissues:read
update_issue, bulk_update_issuesissues:write
get_stats, get_liveissues:read
purge_events (confirm)events:delete
get_usageusage:read
get_audit_logaudit:read
report_errorprojects:read

Prompt triage (project, period?): summarises the open issues of a project — new vs regressed, most users affected, by environment — and proposes what to fix, resolve or ignore first.