Channelsintermediate

File issues via the public API

Use your tenant API key with the issues:read and issues:write scopes to create, list, fetch, and update issues in your Atender issue tracker directly from your own application.

8 min read

File issues via the public API

If your own application already knows something is broken — a crash your mobile app caught, a failed job your backend detected, a bug report a customer typed into your product’s own feedback box — you don’t have to route it through a conversation first. Four endpoints under /api/v1/issues let you create, list, read, and update issues in your Atender tracker directly, authenticated with the same tenant API key you use for the rest of the v1 API.

Before you start

  • A tenant API key, generated in Settings → API & MCP.
  • The issues:write scope to create or update issues.
  • The issues:read scope to list or fetch issues.

A key can carry both scopes if your integration needs to do both. Scopes are checked per endpoint — a key with only issues:read can list and fetch, but a create or patch call against it is rejected with 403.

Every request needs your key in the Authorization header:

Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Use your sa_test_ key while you’re building. Atender stamps each issue with the environment the key belongs to, so reports from a staging integration land in the same backlog and say where they came from — you don’t declare it yourself.

The response envelope

Every endpoint on this API wraps its result in a data key. A success looks like this:

{ "data": { "number": 214, "title": "..." } }

List endpoints add pagination and meta alongside it. Errors replace data with error:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": [{ "field": "priority", "message": "Invalid enum value" }],
    "requestId": "req_9c1f..."
  }
}

VALIDATION_ERROR is 400, NOT_FOUND is 404, FORBIDDEN is 403. Log the requestId — it’s what support will ask for.

The vocabularies

Only one field has a fixed list. The rest are defined by your own tenant, so an integration should fetch them rather than hard-code them.

module is fixed. Send anything outside this list and the request is rejected with VALIDATION_ERROR:

conversations, tasks, knowledge-base, crm, agent-stacks, capabilities, form-builder, snippets, automation, ai-settings, analytics, csat, sla, custom-fields, email, voice, web-chat, sidekick, user-management, tag-management, incidents-settings, opening-hours, conversation-settings, general-settings, superadmin, general, dashboard

status, type, priority and customerPriority are your tenant’s own. Each is a short key, validated at request time against the vocabulary configured for your workspace — so the values differ between tenants and change when someone edits them in Settings. Three endpoints let a client discover them, all requiring issues:read:

  • GET /api/v1/issue-statuses — the status keys for your tenant, with labels, colours and ordering
  • GET /api/v1/issue-types — the type keys
  • GET /api/v1/issue-priorities — the priority keys

Fetch these when you render a picker, filter, or submit one of those fields. Hard-coding the values that happen to be in your workspace today will break when the vocabulary is edited.

Create an issue

POST /api/v1/issues

Requires issues:write.

{
  "title": "Checkout button does nothing on Safari",
  "body": "Tapping Pay on iOS Safari 17 does not submit the form.",
  "type": "bug",
  "module": "conversations",
  "customerPriority": "critical",
  "reporterEmail": "jane@customer.com",
  "reporterName": "Jane Doe",
  "conversationId": "conv_9f2a",
  "appVersion": "4.12.0",
  "pageUrl": "https://app.customer.com/checkout",
  "pageTitle": "Checkout"
}

title is the only required field (1–255 characters). body is capped at 20,000 characters, appVersion at 100, pageTitle at 255, and pageUrl at 2,000.

The body is strict: a field the endpoint doesn’t recognise is a rejection, not something quietly ignored. In particular you cannot send tenantId or environment — both come from your API key.

A successful create returns 201 with the new issue:

{
  "data": {
    "number": 214,
    "title": "Checkout button does nothing on Safari",
    "body": "Tapping Pay on iOS Safari 17 does not submit the form.",
    "type": "bug",
    "state": "open",
    "status": "new",
    "priority": "medium",
    "module": "conversations",
    "labels": [],
    "commentCount": 0,
    "linkedConversationCount": 1,
    "reporterContactId": "contact_88a1",
    "assigneeUserId": null,
    "environment": "production",
    "appVersion": "4.12.0",
    "pageUrl": "https://app.customer.com/checkout",
    "pageTitle": "Checkout",
    "externalRef": null,
    "createdAt": "2026-08-29T10:00:00.000Z",
    "updatedAt": "2026-08-29T10:00:00.000Z",
    "closedAt": null
  }
}

That shape is the same everywhere — create, list, read and update all return it. Two things worth noting about what’s not in it. There’s no id: the number is what you keep, and it’s what every other endpoint takes. And customerPriority is accepted on create but not returned, so store the value you sent if your own system needs to display it.

List issues

GET /api/v1/issues?state=open&status=new&sort=newest&page=1&perPage=30

Requires issues:read. Every filter is optional:

  • stateopen, closed or all
  • type, priority, module — See The vocabularies above — only module is a fixed list
  • status — One of your tenant’s status keys from GET /api/v1/issue-statuses
  • label — A single label
  • assigneeUserId, reporterContactId — Narrow to one assignee or one reporter
  • search — Free text, up to 200 characters
  • sortnewest, oldest, updated, priority, status
  • page, perPageperPage maxes out at 100
{
  "data": [ /* array of issue objects, same shape as create returns */ ],
  "pagination": { "hasMore": true, "nextCursor": null, "totalCount": 214 },
  "meta": { "page": 1, "perPage": 30 }
}

Every row returned belongs to your tenant only — there’s no way to pass a tenantId filter to see anyone else’s data, because the tenant scoping isn’t a filter, it’s the boundary of the query.

Read a single issue

GET /api/v1/issues/:number

Requires issues:read. :number is the tenant-scoped issue number returned by create and list — not a global database id. Asking for a number that doesn’t exist in your tenant (including one that exists in someone else’s) returns NOT_FOUND, not another tenant’s issue.

Update an issue

PATCH /api/v1/issues/:number

Requires issues:write. Thirteen fields can be changed through the API, and at least one of them must be present. One call can carry a whole triage verdict:

  • status — One of your tenant’s status keys
  • priority — The team’s own priority — one of your tenant’s priority keys
  • type — One of your tenant’s type keys
  • customerPriority — The reporter’s own stated urgency — one of your tenant’s priority keys
  • assigneeUserId — The user the issue is assigned to. Must be an active member of this tenant
  • module — One of the fixed module values above
  • builtyes, partly or no
  • scopesmall, medium or large
  • reachnew-surface, setting or everyone
  • isTriaged — Boolean
  • isDuplicateCandidate — Boolean
  • touchesTwilio — Boolean
  • isUpForGrabs — Boolean

module, built, scope and reach are fixed enums, so they’re safe to send literally. status, type, priority and customerPriority are your tenant’s own keys — fetch them from the three discovery endpoints above.

{
  "status": "in-progress",
  "priority": "high"
}

Anything else is rejected. An unknown status, type or priority key returns a 400 validation error that names the valid keys for your tenant. The response is the updated issue in the same shape as create and read.

Changing status has side effects: a status whose behavior is closed stamps closedAt and closes the issue, while any other status reopens it. Either way the change fires your tenant’s issue_status_changed automations.

How the reporter is identified

An issue can carry a reporter, and the API gives you two ways to name one — one or the other, never both in the same request. Both are optional: omit them and the issue is created with no reporter attached.

  • reporterContactId — pass the id of a contact that already exists in your tenant. It’s verified against your tenant before the issue is created; a contact id from another tenant, or one that doesn’t exist, returns NOT_FOUND rather than being silently attached.
  • reporterEmail (optionally with reporterName) — Atender resolves this to an existing contact by address, or creates one if none exists yet, then attaches that contact as the reporter.

Sending both in one request is a validation error.

customerPriority vs. priority

The API distinguishes two priority fields, and they’re meant to stay separate:

  • customerPriority — set by the caller on create. This is the urgency your application (or the customer, through your application) assigns to the report. It’s the same field Atender’s own reporter form uses to record how the reporter feels about the issue.
  • priority — the team’s own priority, set by whoever triages the issue in Atender. You can set it on create and change it on patch, but it’s the team’s field: most integrations should leave it alone and let triage own it.

Most integrations only ever set customerPriority, since your application usually knows how urgent the report feels to the person who filed it, but has no reason to know how Atender’s team has triaged its backlog.

Linking a conversation

If the issue you’re filing came out of an existing conversation, pass its id as conversationId on create:

{
  "title": "...",
  "conversationId": "conv_9f2a"
}

If the conversation id belongs to your tenant, the issue is linked to it. If it doesn’t — a typo, a stale id, an id from another tenant — the link is simply not made. The issue is still created either way; a bad conversationId never blocks or fails the create call. Check linkedConversationCount on the created issue if you need to confirm the link succeeded.

What’s not supported yet

The public API does not currently accept attachment or screenshot paths on create or update. There’s no upload endpoint on this API, so there’s no way for a caller to obtain a valid storage path to submit — and accepting an arbitrary string here would let a caller point an issue at storage it doesn’t own. If your integration needs to attach screenshots to a report, that’s not available through /api/v1/issues today.

Tags

How ToGetting Started