Channelsintermediate

File issues via the public API

Use your tenant API key with the issues:read, issues:write and issues:manage 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. The 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 issues (a key with issues:manage can create too), and the issues:manage scope to update them.
  • The issues:read scope to list or fetch issues.
  • The Issues module switched on for your workspace. Without it every call under /api/v1/issues and the issue-* vocabularies is refused with 403.

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, and a request that carries a JSON body also sends Content-Type: application/json:

Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Two requests are not JSON: an attachment upload (see Attach a screenshot or video below) and a comment image upload, POST /api/v1/issues/:number/comment-media. Both are multipart/form-data with a single file part.

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

An endpoint that returns something wraps it in a data key. A success looks like this:

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

The issue list adds pagination and meta alongside it. A few calls that only remove something return 204 No Content with an empty body instead — deleting a comment, deleting an attachment, and unlinking a conversation. Errors replace data with error:

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

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

The vocabularies

Five fields have a fixed list: module, built, scope, reach and platform. The rest are defined by your own tenant, so an integration should fetch them rather than hard-code them.

module, built, scope, reach and platform are fixed. Send anything outside these lists and the request is rejected with VALIDATION_ERROR.

module:

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

The three triage fields:

  • built — yes, partly, no
  • scope — small, medium, large
  • reach — new-surface, setting, everyone

platform is crew or v3: which Atender product the issue is about.

module and platform can be set on create and on update; built, scope and reach are set on update. All five can be used as list filters.

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 or issues:manage.

{
  "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 30,000 characters, appVersion at 100, pageTitle at 255, and pageUrl at 2,000. You can also send priority (see customerPriority vs. priority below) and createdByUserId — the teammate the issue is filed for, who must be an active member of your tenant. Without it, an issue created with an API key is filed by the person who made the key, if they are still a member.

platform is optional. On Atender’s own workspace, a create that omits it takes the value from the linked conversation, and sending null files the issue with no platform. The Atender app shows the field only on Atender’s own tracker.

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",
    "customerPriority": "critical",
    "module": "conversations",
    "platform": null,
    "built": null,
    "scope": null,
    "reach": null,
    "isTriaged": false,
    "isDuplicateCandidate": false,
    "touchesTwilio": false,
    "isUpForGrabs": false,
    "labels": [],
    "commentCount": 0,
    "linkedConversationCount": 1,
    "attachmentCount": 0,
    "reporterContactId": "contact_88a1",
    "reporterName": "Jane Doe",
    "assigneeUserId": null,
    "assignee": null,
    "createdByUserId": null,
    "createdBy": null,
    "duplicateOfNumber": null,
    "environment": "production",
    "appVersion": "4.12.0",
    "pageUrl": "https://app.customer.com/checkout",
    "pageTitle": "Checkout",
    "externalRef": null,
    "url": "https://v3.atender.com/acme/issues/214",
    "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. There’s no id: the number is what you keep, and it’s what every other endpoint takes. customerPriority is returned alongside priority, so you can read back the value you sent. url opens the issue in the Atender app, and duplicateOfNumber is the number of the issue this one was merged into, or null when it has not been merged.

List issues

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

Requires issues:read. Every filter is optional. A filter marked list takes several values, either comma-separated (status=new,in-progress) or as a repeated parameter (status=new&status=in-progress), up to 50 values; an issue that matches any one of them is returned.

  • state — open, closed or all. Leave it out and you get issues in every state
  • behavior — List. The status category: triage, open (open and waiting-on-reporter statuses), closed, or any for every category
  • status — List. Your tenant’s status keys from GET /api/v1/issue-statuses
  • type, priority, customerPriority — List. Your tenant’s type and priority keys — see The vocabularies above
  • module, built, scope, reach — List. The fixed values listed in The vocabularies above
  • platform — List. crew or v3: which Atender product the issue is about. Only Atender’s own workspace uses it
  • triaged — true or false — only issues that are, or are not, marked triaged
  • merged — only — only issues that have been merged into another issue. Without it, merged issues are left out of the list
  • label — List. Label names
  • assigneeUserId — List. User ids of assignees
  • reporterContactId — One reporter’s contact id
  • search — Free text, up to 200 characters. Matches the title and body, and an issue number such as 342 or #342
  • sort — created, updated, priority, status, newest or oldest. The default is created, newest first
  • sortDir — asc or desc. Without it, created, updated and priority sort desc and status sorts asc. newest and oldest set their own direction, so sortDir is ignored beside them
  • page, perPage — page starts at 1. perPage defaults to 30 and 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:manage. Fifteen fields can be changed through the API, and at least one of them must be present. One call can carry a whole triage verdict:

  • body — The issue text, 1–30,000 characters
  • 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
  • platform — crew or v3, or null to clear it
  • built — yes, partly or no
  • scope — small, medium or large
  • reach — new-surface, setting or everyone
  • isTriaged — Boolean
  • isDuplicateCandidate — Boolean
  • touchesTwilio — Boolean
  • isUpForGrabs — Boolean

module, platform, 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"
}

A field outside this table is rejected. To clear assigneeUserId, customerPriority, module, platform, built, scope or reach, send it as null. 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, once for each linked conversation (none if the issue has no linked conversation).

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 — unless you pass a conversationId, in which case that conversation’s contact becomes the reporter.

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

Reply to the people who reported it

When several duplicate reports are merged onto one issue, the issue carries many linked conversations, each with its own reporter. Two endpoints let an integration answer all of them the same way the Reply to reporters box does in the app — one message written into each linked conversation as a normal agent reply, delivered and translated the way a reply from the inbox is.

List an issue’s reporter conversations

GET /api/v1/issues/:number/reporter-conversations

Requires issues:read. It returns every conversation linked to the issue, with the detail you need to choose who to answer: the reporter’s name, the channel, whether the conversation is archived, and — on a cross-workspace issue — the reporting workspace. An archived conversation is returned but cannot be replied to. This is the list behind the reply below. On an issue that has been merged into another, the list holds only that issue’s own reporter; the issue it was merged into carries the whole group.

{
  "data": {
    "conversations": [
      {
        "conversationId": "6f2a…",
        "subject": "Checkout fails on Safari",
        "contactName": "Jane Doe",
        "channel": "email",
        "status": "open",
        "archived": false,
        "linkedAt": "2026-08-29T10:00:00.000Z",
        "isReporterLink": true,
        "viaIssueNumber": null
      }
    ]
  }
}

isReporterLink marks the reporter’s own conversation. viaIssueNumber is the number of the merged issue that carried a reporter in, or null for a reporter linked to this issue alone. Each row also carries sourceTenant, tenantTagName and appPath.

Reply to the reporters

POST /api/v1/issues/:number/reporter-replies

Requires issues:manage — the manage scope, the same one the in-app reply box needs, not the plain write scope the create call takes. The body carries the message, the conversations to write it into, and an idempotency id:

{
  "body": "We've shipped a fix — please try again and let us know.",
  "conversationIds": ["6f2a…", "b81c…"],
  "sendId": "b3f7c0e2-5a91-4d2e-9c44-1f0a2e8d7c31"
}
  • body — The reply text, 1–30,000 characters. Required.
  • conversationIds — The linked conversations to answer (their UUIDs, as reporter-conversations returns them), 1–200 of them. Every id must be linked to this issue, or the whole call is a 400. Required.
  • sendId — A client-minted id, 1–200 characters, one per compose. It makes the send safe to repeat: combined with each conversation it forms the message’s identity, so a retry or a double send with the same sendId never delivers to the same conversation twice. A new sendId is a new message, even with the same text. Required.

The message goes into each named conversation as a normal agent reply — the same delivery and side effects as a reply from the inbox:

  • It goes out on the conversation’s own channel, with two exceptions. A phone conversation is answered by email when the customer has an email address and email is on for your workspace, otherwise by SMS when they have a phone number and SMS is on; with neither, the result is failed. A chat the customer has already left is answered by email when the customer has an email address and your workspace can send email; otherwise the reply stays in the chat.
  • It is translated only when the conversation’s customer language is known and is not English, and Enable translation is on in Settings → Translation. The body is read as English. When translation is off, the reply goes out as written.

An archived conversation is skipped rather than failing the call.

An issue that has been merged into another issue answers 409 with CONFLICT. Its reporters were carried over to the issue it was merged into, so reply there.

The reply result

The response reports the outcome per conversation, so a caller can retry only what didn’t land:

{
  "data": {
    "results": [
      { "conversationId": "6f2a…", "status": "sent" },
      { "conversationId": "b81c…", "status": "failed" }
    ]
  }
}

status is sent, failed, or skipped_archived. On an email conversation, sent means the email was queued: it is translated as it goes out, and a translation that fails holds it and marks that message failed in the conversation. To retry the failures, send again with the same sendId and only the failed ids — the ones already sent are not written to a second time. A key with no person behind it authors the reply under the generic agent name Agent.

A call adds one line to the issue’s history when it delivers a new message to at least one conversation. A repeat of a send that already went out delivers nothing new, so it adds no second line.

Attach a screenshot or video

POST /api/v1/issues/:number/attachments

Requires issues:write or issues:manage. Create and update do not take a file path — a path string would let a caller point an issue at storage it doesn’t own. Upload the file itself instead, as multipart/form-data with a single file part:

  • Types: PNG, JPEG, GIF and WebP images, PDF documents, and MP4, QuickTime and WebM videos.
  • Size: up to 10 MB for an image or a PDF, up to 100 MB for a video.
  • Count: up to 5 files on one issue. Delete one before you add a sixth.

A successful upload returns 201 with the new attachment. GET /api/v1/issues/:number/attachments lists them (issues:read), and DELETE /api/v1/issues/:number/attachments/:id removes one (issues:manage, 204 with no body).

Tags

How ToGetting Started