bugwatch docs

Authentication

For agents: you authenticate with Authorization: Bearer bwt_…; whoami() tells you which organization the token belongs to and its scopes. Errors: 401 bad or expired token, 403 missing scope, 404 unknown org/project — including organizations the token cannot see.

Two principals

API tokens — Authorization: Bearer bwt_…. Bound to one organization, with an explicit scope list and optional expiry. Stored as SHA-256; shown once at creation. Used by scripts, CI, sentry-cli (SENTRY_AUTH_TOKEN), and MCP clients. See Tokens & scopes.

Sessions — the bugwatch_session cookie the dashboard sets after sign-in (HttpOnly, Secure, SameSite=Lax, 30 days; only its SHA-256 is stored). A session user's scopes for an organization are derived from their membership role on every request. The API allows the dashboard origin with credentials, so the cookie works cross-subdomain from app.bugwatch.io.

Cross-site requests. A state-changing request that carries the session cookie, and every /auth/* POST, must come from the dashboard's origin (or the API's own host) and, if it has a body, send application/json. Anything else is refused: 403 for a foreign Origin, 415 for another body type. Bearer-token requests are not affected, because a token is never sent by a browser on its own.

Rate limits. Sign-in, signup, magic links, /oauth/token, /oauth/register and status-page subscribe are limited per client IP (20 a minute per route), and password sign-in also per account (5 a minute), whatever IP the attempts come from. One address gets at most 5 sign-in or confirmation emails an hour. Over a limit the answer is 429 with Retry-After.

Every /v1/* and /api/0/* route accepts either. /auth/* routes handle sign-in and need neither. /stripe/webhook authenticates by Stripe signature.

Sign-in routes

Route Purpose
POST /auth/signup {email, password (≥10), name?, orgName, orgSlug} → creates user + org (owner). With an email provider configured: 202 {verifyEmail: true} and a confirmation link is emailed, with no cookie until the link is followed (the same answer whether or not the address already had an account). Without one: 201 and sets the cookie
POST /auth/login {email, password}; uniform error and timing on unknown emails
POST /auth/logout Deletes the session
GET /auth/me Current user and their org memberships
GET /auth/providers Which of google, github, microsoft, magicLink are enabled on this deployment
GET /auth/oauth/{provider}/start?next=/path Begin OAuth; callback is GET /auth/oauth/{provider}/callback
POST /auth/magic → POST /auth/magic/verify Email magic link (needs the email provider). Each link works once. /auth/login answers 403 {code: "email_unverified"} for an unconfirmed password account and re-sends its confirmation link
POST /auth/orgs Create a first organization after social/magic sign-in

Social identities are linked by (provider, subject); an email match links to an existing account only when the provider reports it verified.

Organization scoping

Every resource route is under /v1/orgs/{org}/…. The handler resolves the slug, then checks:

  1. A token must belong to that organization; a session user must be a member. Otherwise 404 — cross-org requests never reveal that the org exists.
  2. The required scope must be satisfied (hasScope: exact match, org:admin implies all, :write implies :read). Otherwise 403 {"error": "insufficient scope"}.

Projects are addressed by slug within the org; unknown projects, issues, keys, or rules are 404.

Status codes

Code Meaning
401 No credentials, invalid token, expired token, or expired session
403 Authenticated but missing the scope
404 Org not accessible, or resource not found
400 Validation failure; body is {"error": {"formErrors": [], "fieldErrors": {…}}} (Zod)
402 Plan limit (adding a billable member beyond the plan's seats)
409 Slug already taken
413 Upload too large
501 Feature not configured on this deployment (Stripe, an OAuth provider, email)

Example

curl -sS https://api.bugwatch.io/v1/whoami -H 'Authorization: Bearer bwt_…'
# {"via":"token","tokenName":"claude-code","user":{"id":"01J…","email":"you@example.com","name":null},
#  "orgs":[{"slug":"acme","name":"Acme","scopes":["org:read","project:read","event:read"]}]}

Ingest uses a different credential entirely — the DSN public key in X-Sentry-Auth — and can only send events; see SDKs overview.