Skip to Content
IntegrationAPI Token

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 /bugs periodically 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#

  1. 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.
  2. Note the API host shown at the top of the API Token page (the labelled “API host” chip). Examples:
  3. Copy your fresh token (it starts with sniffer_).
  4. Try it:
    curl -s "https://<your-api-host>/projects?page=1&size=10" \ -H "api-token: sniffer_eyJjb21wYW55SWQiOiI..."
    You should get back the standard envelope .

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#

FieldNotes
NameFree-form, max 100 characters. Use a descriptive name like CI/CD deploys, Slack bot, Q3 audit script.
ExpirationPreset chips — 7d · 30d · 60d · 90d · 180d · 1 year — or Custom with a 1–365 day input. Tokens cannot be issued for “no expiry”.
PermissionsScope presets — Read only, Read & Write, Full access, Custom — or the underlying 3 × 4 matrix when Custom is selected.

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#

EnvironmentHost
Devhttps://support.api.dev.snifferweb.com (or your dev host)
Prodshown at the top of the API Token page

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:

HTTPmessageWhen
401Authentication requiredNo api-token, x-session-id, or session cookie sent.
401Invalid API token formatToken is present but doesn’t start with sniffer_.
401Invalid API tokenThe encrypted payload couldn’t be decrypted, or is missing companyId.
401API token has expiredexpiresAt in the token payload is in the past.
401Invalid or expired API tokenCatch-all decryption error.

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:

projectbugcomment
create
update
delete
get

Scope presets fan out into this matrix as follows:

Presetget everywherecreate + update everywheredelete everywhere
Read only
Read & Write
Full access
Customindividual checkboxesindividual checkboxesindividual checkboxes

⚠️ 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=20

page 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.

MethodPathSummaryScope
POST/projectCreate a new projectRead & Write
GET/projectsList projects (paginated, filterable)Read only
GET/project/:idGet a single project’s detailsRead only
PATCH/project/:idUpdate project metadataRead & Write
DELETE/project/:idDelete a projectFull access
POST/project/api-logsQuery a project’s API call logRead only

Bugs / Tasks#

The primary work unit. Bugs carry summary, status, severity, assignees, tags, and attachments.

MethodPathSummaryScope
POST/bugCreate a bug or taskRead & Write
GET/bugsList bugs (filter by project, status, assignee, etc.)Read only
GET/bugs/searchLightweight bug search (autocomplete-style, max 30 hits)Read only
GET/bugs/by-integration-keyFind a bug linked to an external platform key (Jira / Zendesk / Salesforce)Read only
GET/bugs/by-jira-keyLegacy alias for by-integration-key?platform=JIRARead only
GET/bug/:idGet a single bug’s full detailsRead only
GET/bug/:id/incidentsList incidents / occurrences for a bugRead only
PATCH/bug/:idUpdate a bug (status, severity, assignees, etc.)Read & Write
PATCH/bugsBulk-update multiple bugs in one callRead & Write
PATCH/bug/indexesReorder bugs within a view (drag-and-drop)Read & Write
POST/bug/generate-action-detailsTrigger AI-powered action/code analysis for a bugRead & Write
POST/bugs/backfill-rankRecompute LexoRank values across a projectRead & Write
DELETE/bug/:idDelete a single bugFull access
POST/bug/bulk-deleteDelete multiple bugs in one callFull access
GET/bug/healthHealth probe for the bug subsystemRead only

Comments#

Free-form notes attached to a bug.

MethodPathSummaryScope
POST/commentCreate a commentRead & Write
GET/commentsList comments (filter by bug, project)Read only
GET/comment/:idGet a single commentRead only
PATCH/comment/:idUpdate a comment’s textRead & Write
DELETE/comment/:idDelete a commentFull access
POST/sticky-noteCreate a sticky-note variant of a commentRead & Write

Statuses#

Custom workflow columns per project.

MethodPathSummaryScope
POST/statusCreate a custom statusRead & Write
GET/statusesList statuses (filter by project)Read only
GET/status/:idGet a single statusRead only
GET/status/defaultGet the system-default statusesRead only
PATCH/status/:idRename / recolor a statusRead & Write
PATCH/status/indexesReorder statuses within a projectRead & Write
DELETE/status/:idDelete a custom statusFull access

Tags#

Light labels for grouping bugs.

MethodPathSummaryScope
POST/tagCreate a tagRead & Write
GET/tagsList tags (filter by project, type)Read only
GET/tag/:idGet a single tagRead only
PATCH/tag/:idUpdate a tagRead & Write
DELETE/tag/:idDelete a tagFull access

Members#

Project membership and role management.

MethodPathSummaryScope
POST/project-memberAdd a single member to a projectRead & Write
POST/project-membersBulk-add members to a projectRead & Write
GET/project-membersList members of a projectRead only
GET/project-member/:idGet a single membership recordRead only
PATCH/project-member/:idUpdate a member’s role or permissions on a projectRead & Write
DELETE/project-member/:idRemove a member from a projectFull access
GET/project-members/compareCompare a user’s access across projectsRead only
POST/project-members/syncSync members from an external sourceRead & Write

Worklogs#

Time-tracking entries on bugs.

MethodPathSummaryScope
POST/worklogsCreate a worklog entryRead & Write
GET/worklogsList worklogs (filter by bug, project, user)Read only
GET/worklogs/:idGet a single worklogRead only
PATCH/worklogs/:idUpdate a worklog entryRead & Write
DELETE/worklogs/:idDelete a worklogFull access

Dashboards#

Aggregated views of a project’s health.

MethodPathSummaryScope
GET/project/dashboardCompany-level dashboard across projectsRead only
GET/project/:id/dashboardPer-project dashboardRead only
GET/project/:id/dashboard-betaBeta variant of the per-project dashboardRead only
GET/project/:id/bug-occurrencesBug occurrence trends over timeRead only

Session recordings#

Customer-side session captures uploaded to a bug.

MethodPathSummaryScope
POST/recording-uploadInitiate / commit a recording uploadRead & Write
GET/recording-uploadsList recordings on a project / bugRead only
GET/recording-upload/:idGet recording metadataRead only
PATCH/recording-upload/:idUpdate recording metadataRead & Write
DELETE/recording-upload/:idDelete an upload recordFull access
POST/recordingCreate a recording stubRead & Write
GET/recordingList recordingsRead only
GET/recording/:idGet a recordingRead only
DELETE/recording/:idDelete a recordingFull access

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" }
HTTPWhenTypical cause
400Request validation failedMissing required field, wrong type, unknown enum value.
401Authentication failedNo / invalid / expired api-token. See Authentication failures.
403Authorised but forbiddenReserved for future per-scope checks; rare today.
404Resource not foundWrong :id, deleted record, mismatched company scope.
409ConflictDuplicate unique field (e.g. project key already exists).
500Server errorBug on our side — contact support with the request id.

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.


© 2026 Your Company