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:writescope to create issues (a key withissues:managecan create too), and theissues:managescope to update them. - The
issues:readscope to list or fetch issues. - The Issues module switched on for your workspace. Without it every call under
/api/v1/issuesand theissue-*vocabularies is refused with403.
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,noscope—small,medium,largereach—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 orderingGET /api/v1/issue-types— the type keysGET /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,closedorall. Leave it out and you get issues in every statebehavior— List. The status category:triage,open(open and waiting-on-reporter statuses),closed, oranyfor every categorystatus— List. Your tenant’s status keys fromGET /api/v1/issue-statusestype,priority,customerPriority— List. Your tenant’s type and priority keys — see The vocabularies abovemodule,built,scope,reach— List. The fixed values listed in The vocabularies aboveplatform— List.creworv3: which Atender product the issue is about. Only Atender’s own workspace uses ittriaged—trueorfalse— only issues that are, or are not, marked triagedmerged—only— only issues that have been merged into another issue. Without it, merged issues are left out of the listlabel— List. Label namesassigneeUserId— List. User ids of assigneesreporterContactId— One reporter’s contact idsearch— Free text, up to 200 characters. Matches the title and body, and an issue number such as342or#342sort—created,updated,priority,status,newestoroldest. The default iscreated, newest firstsortDir—ascordesc. Without it,created,updatedandprioritysortdescandstatussortsasc.newestandoldestset their own direction, sosortDiris ignored beside thempage,perPage—pagestarts at 1.perPagedefaults 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 charactersstatus— One of your tenant’s status keyspriority— The team’s own priority — one of your tenant’s priority keystype— One of your tenant’s type keyscustomerPriority— The reporter’s own stated urgency — one of your tenant’s priority keysassigneeUserId— The user the issue is assigned to. Must be an active member of this tenantmodule— One of the fixed module values aboveplatform—creworv3, ornullto clear itbuilt—yes,partlyornoscope—small,mediumorlargereach—new-surface,settingoreveryoneisTriaged— BooleanisDuplicateCandidate— BooleantouchesTwilio— BooleanisUpForGrabs— 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, returnsNOT_FOUNDrather than being silently attached.reporterEmail(optionally withreporterName) — 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, asreporter-conversationsreturns them), 1–200 of them. Every id must be linked to this issue, or the whole call is a400. 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 samesendIdnever delivers to the same conversation twice. A newsendIdis 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).