Sniffer API Tokens
The Sniffer API lets external scripts, CI pipelines, and integrations read and write the same data you see in the Sniffer web app — projects, bugs, comments, statuses, tags, members, worklogs, dashboards, and session recordings.
To authenticate those requests you issue an API token from Company Settings → API Tokens (/company-settings/settings/api-token), then send it on every request via the api-token HTTP header. The token carries the company scope, the permissions you picked at creation time, and an expiry date.
This document is the public reference for that flow.
Overview#
A Sniffer API token unlocks the same JSON API the web app uses. Typical use cases:
- CI / CD bots — file a bug when a deploy fails, attach the failing commit’s stack trace as a comment, and route the bug to the right assignee.
- ETL pipelines / dashboards — pull
GET /bugsperiodically into a data warehouse for custom reporting on top of dashboards Sniffer doesn’t ship. - External integrations — sync issues between Sniffer and an internal helpdesk, Slack channel, or Notion workspace.
- One-off scripts — bulk-create tags, archive old bugs, audit project membership.
A token is company-scoped: it can read and write only the data that belongs to the company it was issued from. Multi-company users must issue one token per company.
Quick start#
- Issue a token at Company Settings → API Tokens → Create API Token. Pick a name, an expiry (a chip preset or Custom 1–365 days), and a permission scope (Read only / Read & Write / Full access / Custom). Click Create token.
- Note the API host shown at the top of the API Token page (the labelled “API host” chip). Examples:
- Dev:
https://support.api.dev.snifferweb.com - Prod: your company’s production support API host (shown in the UI).
- Dev:
- Copy your fresh token (it starts with
sniffer_). - Try it:
You should get back the standard envelope .
curl -s "https://<your-api-host>/projects?page=1&size=10" \ -H "api-token: sniffer_eyJjb21wYW55SWQiOiI..."
That’s it — you’re authenticated. Skip to Common workflows for representative examples.
Creating a token#
Tokens are created from the UI at /company-settings/settings/api-token. Only Company Admins (or users granted the COMPANY_INFO.UPDATE permission) can issue tokens.
Fields#
After creation#
The detail drawer that opens after Create token shows the full token value with a copy button and a show/hide eye toggle. The token is also retrievable from the Show token action on the table row. Keep it secret — anyone with the string can act as that token for as long as it remains unexpired.
Rotation & revocation#
There is no in-place rotate. To rotate, create a new token, switch your client to it, then delete the old one from the Delete action.
Authentication#
Send the token on every request in the api-token header.
GET /bugs?project=64f1ab2c... HTTP/1.1
Host: <your-api-host>
api-token: sniffer_eyJjb21wYW55SWQiOiI...That’s the only header required. Cookies and Authorization: Bearer headers are not used for token auth.
Base URL#
The same host appears in the “API host” chip on the page header, with a one-click copy button.
Authentication failures#
The auth guard returns 401 Unauthorized with a message body for each failure mode:
If you get any of these, re-issue a token from the UI.
Permission scopes#
When you create a token you pick what it can do across three resources and four actions:
Scope presets fan out into this matrix as follows:
⚠️ Important caveat. The scope you pick is encoded inside the token and visible on the detail drawer, but the API does not currently enforce per-action permissions at the controller level. Any valid, non-expired token can call every endpoint listed in this document.Treat the scope you pick as least-privilege intent: it documents who-can-do-what for your own audits and your future self, but do not depend on the server rejecting a Read-only token that tries to POST /bug. Issue tokens with the narrowest expiry possible and rotate aggressively.
Response format#
Every successful response is wrapped in the same envelope by the global response formatter:
{
"status": 200,
"data": {
"localData": { "...": "the actual payload" },
"encryptData": "AES-encrypted copy of the same payload"
}
}Read data.localData for the real response body. encryptData is for clients that want the encrypted copy (used by the web app for some flows); you can ignore it.
Skipping the plaintext copy#
If you only want the encrypted copy back (smaller payload over the wire), append ?withLocalData=false to any GET. The response then has data.encryptData but no data.localData.
Request bodies are plain JSON#
You send normal JSON request bodies (Content-Type: application/json). The encryption only applies to responses.
Pagination#
List endpoints use:
?page=1&size=20page is 1-indexed. size is the page size (default varies per endpoint — 20 is a safe choice).
List responses have the shape:
{
"status": 200,
"data": {
"localData": {
"items": [ /* page of results */ ],
"total": 137
}
}
}Endpoint reference#
All paths are relative to your API host. The Scope column is the preset that grants the call (anything below “Full access” works too).
Projects#
The container for a team’s bugs, members, and statuses.
Bugs / Tasks#
The primary work unit. Bugs carry summary, status, severity, assignees, tags, and attachments.
Comments#
Free-form notes attached to a bug.
Statuses#
Custom workflow columns per project.
Tags#
Light labels for grouping bugs.
Members#
Project membership and role management.
Worklogs#
Time-tracking entries on bugs.
Dashboards#
Aggregated views of a project’s health.
Session recordings#
Customer-side session captures uploaded to a bug.
Bug attachments other than session recordings (screenshots, log files, etc.) are uploaded via signed S3 URLs minted by the Sniffer web app — they’re outside the scope of token-driven API calls today.
Common workflows#
Every example uses https://<your-api-host> as the base URL placeholder and sniffer_… as a token placeholder. Replace both with the real values from the UI.
Create a bug#
curl -s -X POST "https://<your-api-host>/bug" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
-H "Content-Type: application/json" \
-d '{
"summary": "Checkout flow throws 500 on Safari",
"description": "Repro: open /cart in Safari 17, click Checkout.",
"project": "64f1ab2c8e3d4a0012345678",
"bugStatus": "64f1ab2c8e3d4a0012345abc",
"severity": "High",
"type": "DEFECT",
"bugFor": "BOARD",
"tags": [],
"assignTo": []
}'Response: { status: 201, data: { localData: { /* new bug document */ } } }.
List bugs with filters#
curl -s -G "https://<your-api-host>/bugs" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
--data-urlencode "project=64f1ab2c8e3d4a0012345678" \
--data-urlencode "statuses=64f1ab2c8e3d4a0012345abc" \
--data-urlencode "page=1" \
--data-urlencode "size=20"Response: { status: 200, data: { localData: { items: [...], total: 137 } } }.
Update a bug’s status#
curl -s -X PATCH "https://<your-api-host>/bug/64f1ab2c8e3d4a0099887766" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
-H "Content-Type: application/json" \
-d '{ "bugStatus": "64f1ab2c8e3d4a0012345def" }'Add a comment to a bug#
curl -s -X POST "https://<your-api-host>/comment" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
-H "Content-Type: application/json" \
-d '{
"bug": "64f1ab2c8e3d4a0099887766",
"text": "Reproduced on staging. Patch coming on PR #1234.",
"type": "COMMENT"
}'Fetch a project dashboard#
curl -s "https://<your-api-host>/project/64f1ab2c8e3d4a0012345678/dashboard" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..."Bulk-archive bugs#
curl -s -X PATCH "https://<your-api-host>/bugs" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
-H "Content-Type: application/json" \
-d '{
"bugIds": [
"64f1ab2c8e3d4a0099887766",
"64f1ab2c8e3d4a0099887777"
],
"isArchived": true
}'Upload a session recording#
curl -s -X POST "https://<your-api-host>/recording-upload" \
-H "api-token: sniffer_eyJjb21wYW55SWQiOiI..." \
-F "file=@/tmp/session-replay.json" \
-F 'meta={"bug":"64f1ab2c8e3d4a0099887766"}'Errors#
The API returns standard HTTP status codes with a JSON body of the form:
{ "statusCode": 400, "message": "…", "error": "Bad Request" }Every response also carries X-Response-Time and X-Response-Size headers, useful when reporting issues.
FAQ & troubleshooting#
My token returns 401 API token has expired. Can I extend it?
No — expiry is signed into the token’s encrypted payload. Issue a fresh token from the UI and update your client. Pick a longer expiry preset (or Custom up to 365 days) if you’ve been hitting this often.
How do I rotate a token without downtime?
Create the new one, deploy it to your client, run a quick GET /projects to confirm it works, then delete the old one. There’s no in-place rotate today.
Why can a Read-only token still call write endpoints?
The four scope presets and the underlying 3 × 4 matrix are stamped into the token’s encrypted payload, but per-action permission enforcement isn’t implemented at the controller level yet. The scope is treated as least-privilege intent: it documents your audit story, but the API will not reject a Read-only token that tries to POST /bug. Issue tokens with the narrowest expiry and rotate often.
My request body parses fine in Postman but I see something called encryptData in the response — what is it?
That’s the encrypted copy of the response payload, used by the web app’s offline cache. Read data.localData for the real payload. You can opt out of the plaintext copy with ?withLocalData=false if you only need the encrypted blob.
Where do I find my API host?
At the top of the API Token page (/company-settings/settings/api-token). There’s a labelled “API host” chip with a one-click copy button.
Can a token call endpoints across companies? No. Tokens are scoped to the company they were issued from, and every list / read endpoint filters by that company implicitly. Use one token per company if you operate across multiple.
Does the API support webhooks / streaming? This document covers the request/response API only. Real-time updates (e.g. WebSocket auto-bug events) are not exposed to API tokens today.
Is there a Swagger / OpenAPI spec?
Yes, available at https://<your-api-host>/swagger. This document is the workflow-oriented companion to that schema reference.
Have a question that isn’t answered here? Open an issue against company-settings-web or reach out in the #sniffer-platform channel.