Documentation REST API

REST API

File reports from anything that can make an HTTP request — a mobile app, a crash handler, a CI job, a support tool. These are the same endpoints the Web SDK itself calls, documented so you can use them directly.

Base URL: https://api.bugcatch.sphoro.com Auth: workspace API key Format: JSON

Building for the browser? Use the Web SDK instead. It calls all of this for you, and it collects the console, network and environment context that makes a report worth filing — which this API accepts but cannot gather on its own.

Authentication

Every ingest route takes a workspace API key, in either of two headers. They are equivalent; use whichever your client makes easier.

either of these
Authorization: Bearer bc_live_…
X-BugCatch-Key: bc_live_…

Get a key from Workspace Settings → API Keys. It is shown once — only a hash and a display prefix are stored, so there is no screen that can show it to you again.

What the key can and cannot do

This key is publishable. It is designed to sit in your page source where anyone can read it, which is only safe because of how little it can reach.

CanCannot
File a report into any project in its workspace List, read or search your bugs
List that workspace's projects (id, name, key) Read another reporter's thread
Presign an attachment upload Change any setting, member or plan
Start and confirm a reporter sign-in Reach anything in the dashboard

Reading a report back needs a watch token instead — a separate credential handed out when a report is filed, which opens that reporter's own reports and nothing else. See reading reports back.

Do not use this key as a server credential for anything that matters. It is not a secret, it has no scopes to narrow, and treating a leak of it as an incident will waste your time. If you need to read data out of BugCatch programmatically, talk to us — that is not what this key is.

CORS

The /ingest/ routes accept any origin. The SDK runs on your site and we cannot know its address ahead of time, so there is nothing to allowlist and no domain to send us. That is safe because these routes authenticate with a header and never with a cookie: reflecting the origin grants a browser exactly what curl already had. Every other route on the API keeps a strict allowlist.

Filing a report

The one endpoint most integrations ever call.

POST /ingest/bugs
curl -X POST https://api.bugcatch.sphoro.com/ingest/bugs \
  -H "Authorization: Bearer bc_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "",
    "title": "Checkout returns 500 on card payment",
    "description": "Only with Amex. Visa is fine.",
    "priority": "HIGH",
    "url": "https://shop.acme.com/checkout",
    "reporterEmail": "alex@acme.com",
    "reporterName": "Alex",
    "metadata": { "env": "production", "build": "2026.8.19-4" }
  }'

Body

FieldTypeNotes
projectIdstring requiredMust belong to the key's workspace, or the call is a 400.
titlestring requiredWhat the reporter would say happened.
descriptionstringFree text.
prioritystringLOW · MEDIUM · HIGH · CRITICAL. Defaults to MEDIUM.
urlstringWhere it happened. Scrubbed for credentials on the way in.
metadataobjectAnything you want on the report — build number, tenant, feature flags.
logsobjectConsole, network and environment. See the shape.
reporterEmailstringThreads their reports together and is where the resolution mail goes. Only send addresses you have authenticated.
reporterNamestringFor display.
attachmentsarray{ type, storageKey, mime }. See attachments.

Response

200 OK
{
  "id": "clz9…",
  "redactionsApplied": 2,
  "watchToken": "…",
  "estimate": { "lowHours": 48, "highHours": 120, "label": "2–5 days" }
}
  • redactionsApplied — how many secrets the server stripped on intake. Worth logging; a number that is never zero means something upstream is leaking credentials into URLs.
  • watchToken — returned only when the report could be attributed to a reporter. Keep it if you want to read the report back later.
  • estimate — how long it is likely to take, as a band and a ready-to-print phrase. null when the project has estimates switched off, has decided reporters do not see them, or has nothing closed to base one on. Draw nothing in that case — a confirmation that admits it cannot answer is worse than one that never raised the question.

Sending captured context

The optional logs object is what turns a description into something reproducible. Every field is optional; send what you have.

the logs object
{
  "logs": {
    "console": {
      "entries": [
        { "level": "error", "text": "Uncaught TypeError: t.map is not a function",
          "at": "2026-08-25T09:14:02.881Z" }
      ]
    },
    "network": {
      "entries": [
        { "url": "https://api.acme.com/v1/cart", "method": "POST",
          "status": 500, "ms": 1841, "failed": true, "slow": true,
          "at": "2026-08-25T09:14:02.401Z" }
      ]
    },
    "env": {
      "userAgent": "…", "platform": "iOS", "language": "en-GB",
      "viewport": "390x844", "timezone": "Europe/London"
    }
  }
}
FieldShape
console.entries[]level, text, at. Levels are the console.* names; uncaught errors go here as error.
network.entries[]url, method, status, ms, and the booleans failed and slow so triage can find the interesting rows without reading everything. Optional error.
envA flat object. There is no required key — send what your platform can tell you.

Do not send request or response bodies. The SDK never reads them, and the report format has nowhere to put them. If you send secrets in a URL they are stripped on intake and again when the report is read back — but the reliable place to not leak a credential is before it is sent.

Attachments

Two steps, because the bytes never pass through the report call: ask for a URL, upload to it, then reference the key it gave you.

  1. Presign

    POST /ingest/presign
    curl -X POST https://api.bugcatch.sphoro.com/ingest/presign \
      -H "X-BugCatch-Key: bc_live_…" \
      -H "Content-Type: application/json" \
      -d '{ "mime": "image/png" }'
    
    # → { "key": "uploads/2026-08-25/8f2a….png", "uploadUrl": "https://…" }

    The URL is good for five minutes. An unsupported mime is a 400 before you have uploaded anything.

  2. PUT the bytes

    straight to the upload URL
    curl -X PUT "<uploadUrl>" \
      -H "Content-Type: image/png" \
      --data-binary @screenshot.png

    Send exactly the Content-Type you presigned. It is bound into the signature, so a different one is refused.

  3. Reference the key on the report

    in the POST /ingest/bugs body
    "attachments": [
      { "type": "SCREENSHOT", "storageKey": "uploads/2026-08-25/8f2a….png",
        "mime": "image/png" }
    ]
WhereAccepted types
On a report
typed SCREENSHOT or VIDEO
image/png, image/jpeg, image/webp, video/webm, video/mp4
On a message
a reply in the thread
The above, plus image/gif, audio/* voice notes, and documents: PDF, txt, csv, json, zip, doc(x), xls(x)

A document has no honest place on a report, because a report's attachments are typed SCREENSHOT or VIDEO and one filed as a screenshot renders as a broken image. Put it on a message, where it keeps its own type.

Listing projects

Useful for a setup screen, so nobody has to paste an id by hand.

GET /ingest/projects
curl https://api.bugcatch.sphoro.com/ingest/projects \
  -H "X-BugCatch-Key: bc_live_…"

# → [{ "id": "clz9…", "name": "Storefront", "key": "WEB" }]

Returns every project in the key's workspace, and nothing from any other.

Reading reports back

Deliberately not something the API key can do. Reading uses the watch token returned when a report was filed — a credential that opens that reporter's own reports and nothing else.

The reasoning: the key is publishable and sits in page source, so a key-authenticated feed keyed on an email address would let anyone read anyone's reports by guessing an address.

the watch token goes in Authorization
curl https://api.bugcatch.sphoro.com/ingest/watch/reports \
  -H "Authorization: Bearer <watchToken>"
EndpointReturns
GET /ingest/watch/reports Everything this token's holder filed, newest first, with each one's status.
GET /ingest/watch/reports/:bugId One report in full, with its timeline — every step from filing to now, both sides' messages included.
POST /ingest/watch/reports/:bugId/messages Write back on it. Body or attachments — at least one.
GET /ingest/watch/stream Server-sent events: status changes, severity changes, assignment and shared replies, as they happen. Sends a ping every 25 seconds.

Internal comments never appear on any of these. Only a comment a team member explicitly marked as visible to the reporter is sent. And the assignee is never named — assigned: true says the report has an owner; who that is stays inside the workspace.

Reporter sign-in

Where a project asks reporters to prove their address, these are the three calls behind it. All take the API key; the session they mint goes in its own header afterwards.

EndpointDoes
GET /ingest/login?projectId=…What this project asks (off / optional / required), and who this session is.
POST /ingest/login/startMails a code to any address.
POST /ingest/loginSpends the code, returning a session token.
POST /ingest/logoutEnds it, here and on the server.
carrying the session on later calls
Authorization: Bearer bc_live_…    // which workspace this page reports to
X-BugCatch-Session: …              // who is sitting in front of it

Two headers because they answer two different questions, and a request carries both. Where a session is present it wins: the address on the report is the one that answered the code, never the one in reporterEmail.

One live session per address per project. Signing in somewhere else ends this one immediately.

Errors

StatusMeansWhat to do
400 The body failed validation, or the project does not belong to this key's workspace, or the mime type is not accepted. Read the message — it names the field. Retrying unchanged will not help.
401 Missing, invalid or revoked key. Or, on a project requiring sign-in, no live reporter session. Check the key, or sign the reporter in.
403 The workspace is suspended. The key is valid and re-issuing it will not help. Do not treat this as a credential problem or rotate keys. Contact support.
404 No such report, or a watch token that does not open it. A token opens only its own reports, never another reporter's.

Errors are JSON, in Nest's standard shape:

a 400
{
  "statusCode": 400,
  "message": "That project does not belong to this API key.",
  "error": "Bad Request"
}

Retrying. 401 and 400 are permanent for that request — a client that retries them is filing nothing and generating load. 403 is also permanent. Retry 5xx and network failures with a backoff, and queue reports locally if your client can, since a bug report is worth filing late.

End to end, in one script

A screenshot and a report, in the order a client would do it. Nothing here needs the SDK.

file-a-bug.sh
#!/usr/bin/env bash
set -euo pipefail

API=https://api.bugcatch.sphoro.com
KEY=bc_live_…
PROJECT=

# 1. Ask for somewhere to put the screenshot.
PRESIGN=$(curl -sS -X POST "$API/ingest/presign" \
  -H "X-BugCatch-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"mime":"image/png"}')

KEY_PATH=$(echo "$PRESIGN" | jq -r .key)
UPLOAD_URL=$(echo "$PRESIGN" | jq -r .uploadUrl)

# 2. Upload it. The bytes never pass through the API's report route.
curl -sS -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/png" --data-binary @screenshot.png

# 3. File the report, referencing the key from step 1.
curl -sS -X POST "$API/ingest/bugs" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg p "$PROJECT" --arg k "$KEY_PATH" '{
        projectId: $p,
        title: "Checkout returns 500 on card payment",
        description: "Only with Amex. Visa is fine.",
        priority: "HIGH",
        url: "https://shop.acme.com/checkout",
        reporterEmail: "alex@acme.com",
        metadata: { env: "production" },
        attachments: [{ type: "SCREENSHOT", storageKey: $k, mime: "image/png" }]
      }')" | jq

Versioning and stability

There is no version prefix in the path, and no dated version header. What that means in practice:

  • Fields get added. Parse responses tolerantly — an unfamiliar key in a response is not an error, and a client that rejects one will break on the next release.
  • The request shapes above are stable. Optional fields stay optional and required ones stay required.
  • Pin the SDK version if you use it. That is the piece with a version number to pin, and an unpinned CDN URL changes behaviour on a site nobody redeployed.

If you are building something that depends on this API more deeply than filing reports, tell us what you need before you build it.

Still stuck?

Every answer here describes what the product actually does today. If something behaves differently, that is worth telling us about — it means the page is wrong.

Support
support@sphoro.com
Security
info@sphoro.com
Talk to us
Book 20 minutes