Pull reports into your tools, triage from scripts, and manage webhooks without the browser
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
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.
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.
| Scope | Allows |
|---|---|
read | Every GET except webhooks |
write | Creating and updating reports and comments, closing duplicates, proceed_anyway, marking notifications read |
webhooks | Everything 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.
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.
| Bucket | Requests | Limit |
|---|---|---|
read | Every GET | 1,000 per hour and 60 per minute |
quiet | Other writes: webhook changes, marking notifications read | 60 per hour and 10 per minute |
noisy | Creating or editing reports and comments, closing duplicates, proceed_anyway | 20 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.
Lists take page and per_page (1 to 100, default 30; anything outside that gets 30) and return a plain JSON array. Navigation is in the headers:
Link: <https://bugbop.com/api/v1/reports?page=2&per_page=30>; rel="next",
<https://bugbop.com/api/v1/reports?page=1&per_page=30>; rel="first",
<https://bugbop.com/api/v1/reports?page=7&per_page=30>; rel="last"
X-Total-Count: 193
Follow rel="next" until it's gone.
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" }
]
}
| Status | Meaning |
|---|---|
400 | Malformed JSON, a missing required parameter, or a bad since |
401 | No usable token |
403 | Token lacks the scope, your role does not allow the action, or the account is suspended |
404 | Doesn't exist, or you can't see it. We don't say which |
409 | Doesn't apply in the current state |
422 | Validation failed |
429 | Rate limited, see Retry-After |
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.
| Method | Path | Scope |
|---|---|---|
GET | /user | read |
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"
}
| Method | Path | Scope | Who |
|---|---|---|---|
GET | /programs | read | Anyone. Your programs plus public and restricted ones |
GET | /programs/:id | read | Anyone who can see the program |
GET | /programs/:id/hunters | read | Program 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"
}
| Method | Path | Scope | Who |
|---|---|---|---|
GET | /reports | read | Your own reports, plus every report in programs where you are staff |
GET | /programs/:program_id/reports | read | Same, limited to one program |
GET | /reports/:id | read | The reporter and program staff |
POST | /programs/:program_id/reports | write | Anyone allowed to report on the program |
PATCH | /reports/:id | write | Reporter: title, description. Triagers and above: status, severity, duplicate_of_id, internal_notes |
POST | /reports/:id/close_as_duplicate | write | Triagers and above |
Newest first. Filters combine, and unknown values are ignored.
| Parameter | Values |
|---|---|
status | Comma list of new, open, needs_info, fixed, wont_fix, duplicate, not_applicable |
severity | Comma list of low, medium, high, critical |
since | ISO 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 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..."
}
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.
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" }'
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-..." }'
A report waiting on AI scope checking and triage. Only the submitter can see it.
| Method | Path | Scope |
|---|---|---|
GET | /report_submissions/:id | read |
POST | /report_submissions/:id/proceed_anyway | write |
status | Meaning | What to do |
|---|---|---|
pending | Being checked | Poll again in a couple of seconds |
scope_failed | The AI thinks it's out of scope. in_scope_explanation says why | Fix and resubmit, or POST to proceed_anyway_url. Triagers will see a scope warning |
completed | The report exists | Use the embedded report |
failed | Something broke. Nothing was saved, error has details | Resubmit |
{
"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"
}
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.
| Method | Path |
|---|---|
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.
| Method | Path | Scope |
|---|---|---|
GET | /notifications | read |
POST | /notifications/mark_as_read | write |
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.
Tell us what you're wiring up. We'll help, or add the endpoint you're missing.
Comments
GET/reports/:report_id/commentsreadGET/comments/:idreadPOST/reports/:report_id/commentswritePATCH/comments/:idwrite