REST API

Pull reports into your tools, triage from scripts, and manage webhooks without the browser

Introduction

JSON over HTTPS at https://bugbop.com/api/v1. If you've used GitHub's API it'll feel familiar: bearer tokens, Link header pagination, one error format. Every object carries a url (its page in Bugbop) and an api_url (the same thing over the API).

A token acts as you and can do what your program role lets you do in the browser, nothing more.

The usual integration: a webhook tells you a report came in, you GET it and open a ticket, then later PATCH the status and comment back to the hunter.

Using an AI agent? The Bugbop skill works in Claude Code, Codex, Cursor, Copilot, Gemini CLI and anything else that reads Agent Skills. It handles tokens, pagination and rate limits, and checks with you before anything gets sent to a hunter.

curl -H "Authorization: Bearer bugbop_pat_..." https://bugbop.com/api/v1/user

Authentication

Create a token on the API Tokens tab of your profile. It starts with bugbop_pat_ and is only shown once, so treat it like a password. Expiry is 30, 90 or 365 days, or never. Deleting a token revokes it straight away.

Authorization: Bearer bugbop_pat_Ab3dEf...

A missing, expired or deleted token gets 401. A suspended, locked or unconfirmed account gets 403. No cookies, no CSRF token.

Scopes and Programs

Pick one or more scopes per token. They don't imply each other, so a write-only token can't GET anything. Missing scope means 403.

ScopeAllows
readEvery GET except webhooks
writeCreating and updating reports and comments, closing duplicates, proceed_anyway, marking notifications read
webhooksEverything under webhooks, reads included, since they show delivery URLs

A token covers either all your programs (including ones you join later) or a list of programs you belong to. Anything outside the list behaves as if it doesn't exist: left out of lists, 404 if you ask for it directly.

Scopes never beat your role. A hunter's write token still can't change a report's status.

Rate Limits

Limits are per user, shared across all your tokens. Anything that emails people or fires webhooks is kept tight so a runaway script can't flood someone's inbox.

BucketRequestsLimit
readEvery GET1,000 per hour and 60 per minute
quietOther writes: webhook changes, marking notifications read60 per hour and 10 per minute
noisyCreating or editing reports and comments, closing duplicates, proceed_anyway20 per hour and 3 per minute

Responses show where you're at in the hourly window. Hit a per-minute cap and Retry-After tells you how long to wait:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1760151600
X-RateLimit-Resource: read

Go over and you get 429 with Retry-After in seconds. That's it, it won't suspend your account.

Errors

Errors are JSON with a message. Validation errors add an errors array.

HTTP/1.1 422 Unprocessable Content
{
  "message": "Validation failed",
  "errors": [
    { "field": "title", "code": "blank", "message": "Title can't be blank" },
    { "field": "duplicate_of_id", "code": "invalid", "message": "Duplicate of must be selected when marking as duplicate" }
  ]
}
StatusMeaning
400Malformed JSON, a missing required parameter, or a bad since
401No usable token
403Token lacks the scope, your role does not allow the action, or the account is suspended
404Doesn't exist, or you can't see it. We don't say which
409Doesn't apply in the current state
422Validation failed
429Rate limited, see Retry-After

Markdown

Descriptions, internal notes and comments are GitHub flavoured Markdown, except a single newline is a line break. Raw HTML is escaped.

Responses hand it back as Markdown. Images from the web editor come through as image links.

User

MethodPathScope
GET/userread

Who the token belongs to. Handy for checking a token works.

{
  "id": "9f8e7d6c-5b4a-4f3e-8d2c-1b0a9f8e7d6c",
  "username": "maverick",
  "url": "https://bugbop.com/profile/maverick",
  "created_at": "2024-11-02T08:12:00Z",
  "email": "[email protected]",
  "identity_verified": true,
  "api_url": "https://bugbop.com/api/v1/user"
}

Programs

MethodPathScopeWho
GET/programsreadAnyone. Your programs plus public and restricted ones
GET/programs/:idreadAnyone who can see the program
GET/programs/:id/huntersreadProgram staff, viewers included. Returns user objects

role is your role on the program (owner, admin, triager, viewer, hunter) or null. bounty_ranges is null unless it's a cash program. webhooks_url only shows up if you can manage webhooks.

{
  "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "name": "Web Security Program",
  "site": "https://app.example.com",
  "visibility": "private",
  "bounty_type": "cash",
  "bounty_paused": false,
  "ai_triage": true,
  "require_kyc": false,
  "description": "Everything under *.example.com is in scope...",
  "bounty_ranges": {
    "critical": { "min_cents": 50000, "max_cents": 100000 },
    "high": { "min_cents": 25000, "max_cents": 50000 },
    "medium": { "min_cents": 10000, "max_cents": 25000 },
    "low": { "min_cents": 5000, "max_cents": 10000 }
  },
  "role": "triager",
  "url": "https://bugbop.com/programs/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "api_url": "https://bugbop.com/api/v1/programs/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "reports_url": "https://bugbop.com/api/v1/programs/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/reports",
  "created_at": "2024-06-01T00:00:00Z",
  "updated_at": "2025-09-30T11:20:41Z"
}

Reports

MethodPathScopeWho
GET/reportsreadYour own reports, plus every report in programs where you are staff
GET/programs/:program_id/reportsreadSame, limited to one program
GET/reports/:idreadThe reporter and program staff
POST/programs/:program_id/reportswriteAnyone allowed to report on the program
PATCH/reports/:idwriteReporter: title, description. Triagers and above: status, severity, duplicate_of_id, internal_notes
POST/reports/:id/close_as_duplicatewriteTriagers and above

Listing and filtering

Newest first. Filters combine, and unknown values are ignored.

ParameterValues
statusComma list of new, open, needs_info, fixed, wont_fix, duplicate, not_applicable
severityComma list of low, medium, high, critical
sinceISO 8601 timestamp. Reports updated at or after it, for incremental syncs
curl -H "Authorization: Bearer bugbop_pat_..." \
  "https://bugbop.com/api/v1/reports?status=new,open&severity=high,critical&since=2025-10-01T00:00:00Z"

The report object

The last three fields are for program staff only.

{
  "id": "6f5e4d3c-2b1a-4c3d-9e8f-0a1b2c3d4e5f",
  "title": "Stored XSS in profile bio",
  "status": "open",
  "severity": "high",
  "scope_warning": false,
  "description": "## Steps\n\n1. Set your bio to `<img src=x onerror=alert(1)>`\n2. View the public profile",
  "program": { "id": "1a2b3c4d-...", "name": "Web Security Program", "url": "...", "api_url": "..." },
  "user": { "id": "9f8e7d6c-...", "username": "maverick", "url": "https://bugbop.com/profile/maverick", "created_at": "2024-11-02T08:12:00Z" },
  "duplicate_of_id": null,
  "comments_count": 3,
  "bounty_total_cents": 25000,
  "url": "https://bugbop.com/programs/1a2b3c4d-.../reports/6f5e4d3c-...",
  "api_url": "https://bugbop.com/api/v1/reports/6f5e4d3c-...",
  "comments_url": "https://bugbop.com/api/v1/reports/6f5e4d3c-.../comments",
  "created_at": "2025-10-03T09:32:53Z",
  "updated_at": "2025-10-04T14:10:02Z",
  "suspected_duplicate_of_id": null,
  "internal_notes": "Reproduced on staging, fix in PR #412",
  "ai_triage": "### Severity: High\n\n..."
}

Creating a report

Send title and description. You can send severity too, but on AI triage programs it's ignored and the AI sets it.

curl -X POST -H "Authorization: Bearer bugbop_pat_..." -H "Content-Type: application/json" \
  https://bugbop.com/api/v1/programs/1a2b3c4d-.../reports \
  -d '{
    "title": "Stored XSS in profile bio",
    "description": "## Steps\n\n1. Set your bio to `<img src=x onerror=alert(1)>`\n2. View the public profile"
  }'

Without AI triage you get 201 and the report.

With AI triage the report gets scope checked before it exists, so you get 202 and a report submission. Poll its Location until status isn't pending. Usually a few seconds.

Updating a report

Send just what's changing. Touch a field your role can't change and you get 403 and nothing is saved. status: "duplicate" needs a duplicate_of_id.

curl -X PATCH -H "Authorization: Bearer bugbop_pat_..." -H "Content-Type: application/json" \
  https://bugbop.com/api/v1/reports/6f5e4d3c-... \
  -d '{ "status": "fixed", "internal_notes": "Deployed in 2.14.0" }'

Closing as a duplicate

Pass duplicate_of_id, or leave it out to use the one the AI suspected. Duplicates of duplicates point at the original.

curl -X POST -H "Authorization: Bearer bugbop_pat_..." -H "Content-Type: application/json" \
  https://bugbop.com/api/v1/reports/6f5e4d3c-.../close_as_duplicate \
  -d '{ "duplicate_of_id": "0a1b2c3d-..." }'

Report Submissions

A report waiting on AI scope checking and triage. Only the submitter can see it.

MethodPathScope
GET/report_submissions/:idread
POST/report_submissions/:id/proceed_anywaywrite
statusMeaningWhat to do
pendingBeing checkedPoll again in a couple of seconds
scope_failedThe AI thinks it's out of scope. in_scope_explanation says whyFix and resubmit, or POST to proceed_anyway_url. Triagers will see a scope warning
completedThe report existsUse the embedded report
failedSomething broke. Nothing was saved, error has detailsResubmit
{
  "id": "c0ffee00-1234-4abc-9def-0123456789ab",
  "status": "scope_failed",
  "program_id": "1a2b3c4d-...",
  "title": "Stored XSS in profile bio",
  "in_scope": false,
  "in_scope_explanation": "The profile service is listed as out of scope in the program description.",
  "error": null,
  "api_url": "https://bugbop.com/api/v1/report_submissions/c0ffee00-...",
  "proceed_anyway_url": "https://bugbop.com/api/v1/report_submissions/c0ffee00-.../proceed_anyway",
  "created_at": "2025-10-03T09:32:53Z",
  "updated_at": "2025-10-03T09:33:01Z"
}

Comments

MethodPathScopeWho
GET/reports/:report_id/commentsreadAnyone who can see the report. Oldest first
GET/comments/:idreadAnyone who can see the report
POST/reports/:report_id/commentswriteThe reporter and program staff, except viewers
PATCH/comments/:idwriteThe comment's author
curl -X POST -H "Authorization: Bearer bugbop_pat_..." -H "Content-Type: application/json" \
  https://bugbop.com/api/v1/reports/6f5e4d3c-.../comments \
  -d '{ "content": "Thanks, reproduced. Fix is in review." }'
{
  "id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
  "report_id": "6f5e4d3c-...",
  "user": { "id": "2b3c4d5e-...", "username": "programadmin", "url": "...", "created_at": "..." },
  "content": "Thanks, reproduced. Fix is in review.",
  "metadata": {},
  "program": { "id": "1a2b3c4d-...", "name": "Web Security Program", "url": "...", "api_url": "..." },
  "url": "https://bugbop.com/programs/1a2b3c4d-.../reports/6f5e4d3c-...#comment-3c4d5e6f-...",
  "api_url": "https://bugbop.com/api/v1/comments/3c4d5e6f-...",
  "report_url": "https://bugbop.com/api/v1/reports/6f5e4d3c-...",
  "created_at": "2025-10-04T14:10:02Z",
  "updated_at": "2025-10-04T14:10:02Z"
}

Webhooks

Needs the webhooks scope and an owner or admin role on the program. Creating one needs a paid plan. Events, signing and payloads are in the webhook docs.

MethodPath
GET/programs/:program_id/webhooks
POST/programs/:program_id/webhooks
GET/webhooks/:id
PATCH/webhooks/:id
DELETE/webhooks/:id
POST/webhooks/:id/ping

Create with url (HTTPS), description and events (event types, or ["all_events"]). The signing secret is only in the create response.

curl -X POST -H "Authorization: Bearer bugbop_pat_..." -H "Content-Type: application/json" \
  https://bugbop.com/api/v1/programs/1a2b3c4d-.../webhooks \
  -d '{
    "url": "https://hooks.example.com/bugbop",
    "description": "Jira sync",
    "events": ["report.created", "report.updated", "comment.created"]
  }'
{
  "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
  "program_id": "1a2b3c4d-...",
  "url": "https://hooks.example.com/bugbop",
  "description": "Jira sync",
  "events": ["report.created", "report.updated", "comment.created"],
  "settings_url": "https://bugbop.com/programs/1a2b3c4d-.../program_webhooks/7a8b9c0d-...",
  "api_url": "https://bugbop.com/api/v1/webhooks/7a8b9c0d-...",
  "ping_url": "https://bugbop.com/api/v1/webhooks/7a8b9c0d-.../ping",
  "created_at": "2025-10-11T02:00:00Z",
  "updated_at": "2025-10-11T02:00:00Z",
  "secret": "4f1c...e9"
}

ping queues a webhook.ping and returns 202. DELETE returns 204.

Notifications

MethodPathScope
GET/notificationsread
POST/notifications/mark_as_readwrite

Your in-app notifications, newest first. unread=true for unread only. mark_as_read takes an optional ids array, otherwise it marks everything read, and returns {"marked_read": n}.

{
  "id": "e1f2a3b4-...",
  "kind": "report_created",
  "read": false,
  "data": { "program_id": "1a2b3c4d-...", "report_id": "6f5e4d3c-...", "title": "Stored XSS in profile bio", "severity": "high", "status": "new" },
  "url": "https://bugbop.com/notifications/e1f2a3b4-.../goto",
  "created_at": "2025-10-03T09:32:53Z",
  "program": { "id": "1a2b3c4d-...", "name": "Web Security Program", "url": "...", "api_url": "..." },
  "report": { "id": "6f5e4d3c-...", "title": "Stored XSS in profile bio", "api_url": "https://bugbop.com/api/v1/reports/6f5e4d3c-..." }
}

Kinds: report_created, report_updated, comment_created, comment_updated, program_access_request_created, program_access_request_approved, program_access_request_rejected, program_updated, kyc_verification_successful, kyc_verification_failed.

Building an integration?

Tell us what you're wiring up. We'll help, or add the endpoint you're missing.