Using the DevFlow API
Give an assistant or a script access to your tickets — acting as you, with the same access your own login has. A personal API token is the way in.
What this is for
A personal API token lets a script, a CI job, or an assistant like Claude Code read and create tickets on your behalf — with the same tenant, the same role, and the same visibility your browser session already has. It cannot do anything your account can't do, and it can't touch anything outside the ticket surface: no settings, no billing, no other user's tokens.
Get a token
In DevFlow, go to Settings → API tokens —
or jump straight there at
https://app.digitaldevflow.com/settings?tab=api-tokens.
Click New token, give it a name, and copy it.
The token is shown exactly once — there is no way to reveal it again later. It expires one year after you create it. Rotate replaces it with a new one in a single step (the old one stops working immediately); Revoke kills it outright. Deactivating your DevFlow account revokes every token you've created — there is no separate step for that.
Send it
Send the token as a standard bearer token, on every request:
Authorization: Bearer dft_a1b2c3d4…
curl https://app.digitaldevflow.com/api/users/me \ -H "Authorization: Bearer dft_a1b2c3d4…"
What a token can reach
Every route below is exactly what a token can reach — nothing more. Each example is real: the response shape is checked, on every build, against DevFlow's actual data types.
/api/projects List the projects your token's caller can see, paged.
Response
{
"items": [
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "Acme Corp Website",
"key": "ACME",
"description": "Marketing site rebuild and ongoing support",
"status": 0,
"clientName": "Acme Corp",
"clientId": "00000000-0000-0000-0000-000000000002",
"ticketCount": 14,
"estimatedHours": 120.0,
"projectManagerId": "00000000-0000-0000-0000-000000000001",
"projectManagerName": "Jordan Lee",
"createdAt": "2026-01-15T09:00:00Z",
"teamMembers": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"email": "jordan@acmeconsulting.example",
"avatarUrl": null,
"joinedAt": "2026-01-15T09:00:00Z"
}
]
}
],
"totalCount": 1,
"page": 1,
"pageSize": 50,
"totalPages": 1
} /api/projects/{id} Get one project by id.
Response
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "Acme Corp Website",
"key": "ACME",
"description": "Marketing site rebuild and ongoing support",
"status": 0,
"clientName": "Acme Corp",
"clientId": "00000000-0000-0000-0000-000000000002",
"ticketCount": 14,
"estimatedHours": 120.0,
"projectManagerId": "00000000-0000-0000-0000-000000000001",
"projectManagerName": "Jordan Lee",
"createdAt": "2026-01-15T09:00:00Z",
"teamMembers": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"email": "jordan@acmeconsulting.example",
"avatarUrl": null,
"joinedAt": "2026-01-15T09:00:00Z"
}
]
} /api/projects/{id} Rename a project, edit its description/status/client/estimate, or reassign its project manager. Requires Manager or above.
Request body
{
"name": "Acme Corp Website",
"description": "Marketing site rebuild and ongoing support",
"status": 0,
"clientId": "00000000-0000-0000-0000-000000000002",
"estimatedHours": 120.0,
"projectManagerId": "00000000-0000-0000-0000-000000000001"
} Response
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "Acme Corp Website",
"key": "ACME",
"description": "Marketing site rebuild and ongoing support",
"status": 0,
"clientName": "Acme Corp",
"clientId": "00000000-0000-0000-0000-000000000002",
"ticketCount": 14,
"estimatedHours": 120.0,
"projectManagerId": "00000000-0000-0000-0000-000000000001",
"projectManagerName": "Jordan Lee",
"createdAt": "2026-01-15T09:00:00Z",
"teamMembers": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"email": "jordan@acmeconsulting.example",
"avatarUrl": null,
"joinedAt": "2026-01-15T09:00:00Z"
}
]
} /api/projects/{projectId}/boards List the boards on a project.
Response
[
{
"id": "00000000-0000-0000-0000-000000000004",
"name": "Main Board",
"isDefault": true,
"columnCount": 2
}
] /api/projects/{projectId}/members Add a staff user to a project's team. Requires Manager or above.
Request body
{
"userId": "00000000-0000-0000-0000-000000000014"
} Response
{
"userId": "00000000-0000-0000-0000-000000000014",
"displayName": "Priya Shah",
"email": "priya@acmeconsulting.example",
"avatarUrl": null,
"joinedAt": "2026-09-10T09:00:00Z"
} /api/projects/{projectId}/members/{userId} Remove a staff user from a project's team. Requires Manager or above. Returns 204 No Content.
/api/projects/{projectId}/files List a project's files and links — name, type, size, uploader, folder, a deep link into the app, and whether the connector can read it as text.
readable is true
when the file's type and size are eligible for text extraction; a read can still
refuse attachment_no_text
(no text layer, or an unreadable one) or attachment_unavailable
(damaged file).
Response
[
{
"id": "00000000-0000-0000-0000-000000000030",
"projectId": "00000000-0000-0000-0000-000000000003",
"folderId": "00000000-0000-0000-0000-000000000031",
"fileName": "sow.pdf",
"contentType": "application/pdf",
"fileSizeBytes": 245760,
"isClientFacing": true,
"uploadedByUserId": "00000000-0000-0000-0000-000000000001",
"uploadedByDisplayName": "Jordan Lee",
"createdAt": "2026-08-20T09:00:00Z",
"updatedAt": "2026-08-20T09:00:00Z",
"externalUrl": null,
"isLink": false,
"folderName": "Contracts",
"kind": "file",
"appUrl": "https://app.digitaldevflow.com/projects/00000000-0000-0000-0000-000000000003?tab=files&file=00000000-0000-0000-0000-000000000030",
"readable": true
},
{
"id": "00000000-0000-0000-0000-000000000032",
"projectId": "00000000-0000-0000-0000-000000000003",
"folderId": null,
"fileName": "Shared design board",
"contentType": "",
"fileSizeBytes": 0,
"isClientFacing": false,
"uploadedByUserId": "00000000-0000-0000-0000-000000000001",
"uploadedByDisplayName": "Jordan Lee",
"createdAt": "2026-08-22T11:00:00Z",
"updatedAt": "2026-08-22T11:00:00Z",
"externalUrl": "https://figma.com/file/example-board",
"isLink": true,
"folderName": null,
"kind": "link",
"appUrl": "https://app.digitaldevflow.com/projects/00000000-0000-0000-0000-000000000003?tab=files&file=00000000-0000-0000-0000-000000000032",
"readable": false
}
] /api/projects/{projectId}/files/{id}/text
Read a project file's text content. Supported formats: plain text, Markdown, CSV,
JSON, HTML (tags stripped), PDF (text layer only — words space-separated in reading
order, ligatures normalised), DOCX (paragraphs and tables in document order — each
table row as cell | cell,
headings as #/##/###,
lists as 1./-)
and .xlsx (each sheet as a # <name>
heading, then rows as pipe-separated cells).
A file is capped at 5 MiB (5,242,880 bytes) for extraction,
and the returned text is capped at 60,000
characters, with truncation reported. The raw file bytes never leave the
server — only the extracted text does. Writes one audit row.
Query parameters:
page_from/page_to
(1-based, PDF only) select a page range;
offset/length
(character offsets, every other format) select a substring.
totalCharacters is
the characters available in the document (for PDF, in the requested page range);
truncated is true
whenever the returned text is shorter than that, for any reason. A PDF response always
reports pageCount;
every other format reports it as null.
Refusal codes
| Code | HTTP | Meaning |
|---|---|---|
| attachment_too_large | 413 | Over the 5 MiB (5,242,880-byte) extraction cap — names the cap and the file's size. |
| attachment_unsupported_type | 415 | The file's type is not in the supported list — including images — naming the MIME type; or it's an external link, not a stored file. |
| attachment_no_text | 422 | A recognised PDF with no text layer, or one whose text layer is unreadable (font has no Unicode mapping) — it looks like a scan; DevFlow does not run OCR. |
| attachment_unavailable | 422 | Could not be read — a missing or damaged blob, a malformed file, or a timed-out extraction. Try again, or open it in DevFlow. |
Response
{
"id": "00000000-0000-0000-0000-000000000030",
"fileName": "sow.pdf",
"contentType": "application/pdf",
"format": "pdf",
"contentTypeNote": null,
"text": "STATEMENT OF WORK\n\n1. Scope\nThis engagement covers the redesign of the Acme Corp marketing site...",
"totalCharacters": 60000,
"truncated": true,
"pageCount": 14,
"pageFrom": 1,
"pageTo": 5,
"bytesRead": 245760
} /api/boards/{id} Get one board, with its columns.
Response
{
"id": "00000000-0000-0000-0000-000000000004",
"projectId": "00000000-0000-0000-0000-000000000003",
"name": "Main Board",
"projectName": "Acme Corp Website",
"projectKey": "ACME",
"isDefault": true,
"columns": [
{
"id": "00000000-0000-0000-0000-000000000005",
"name": "Backlog",
"order": 1,
"wipLimit": null,
"category": 1,
"tickets": []
},
{
"id": "00000000-0000-0000-0000-000000000006",
"name": "In Progress",
"order": 2,
"wipLimit": 5,
"category": 3,
"tickets": [
{
"id": "00000000-0000-0000-0000-000000000008",
"ticketNumber": "ACME-42",
"title": "Homepage hero image is cut off on mobile",
"boardColumnId": "00000000-0000-0000-0000-000000000006",
"sortOrder": 1,
"priorityName": "High",
"priorityColor": "#EF8354",
"priorityLevel": 3,
"storyPoints": 3,
"dueDate": "2026-09-20T00:00:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"attachmentCount": 0,
"blockedByCount": 0,
"blockedByTicketNumber": null,
"isBlocked": false
}
]
}
]
} /api/priorities List every priority level the workspace defines.
Response
[
{
"id": "00000000-0000-0000-0000-000000000007",
"name": "High",
"level": 3,
"color": "#EF8354",
"icon": "arrow-up"
}
] /api/users List staff users, for picking an assignee by name.
Response
[
{
"id": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"email": "jordan@acmeconsulting.example",
"avatarUrl": null,
"role": "Member",
"isActive": true
}
] /api/users/me Get the identity your token authenticates as — the same user, tenant and role as your browser session.
Response
{
"id": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"email": "jordan@acmeconsulting.example",
"avatarUrl": null,
"role": "Member",
"isActive": true,
"createdAt": "2025-11-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z",
"isOperator": false
} /api/tickets List tickets visible to your token's caller, paged and optionally filtered by project.
Response
{
"items": [
{
"id": "00000000-0000-0000-0000-000000000008",
"ticketNumber": "ACME-42",
"title": "Homepage hero image is cut off on mobile",
"boardColumnId": "00000000-0000-0000-0000-000000000006",
"sortOrder": 1,
"priorityName": "High",
"priorityColor": "#EF8354",
"priorityLevel": 3,
"storyPoints": 3,
"dueDate": "2026-09-20T00:00:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"attachmentCount": 0,
"blockedByCount": 0,
"blockedByTicketNumber": null,
"isBlocked": false
}
],
"totalCount": 1,
"page": 1,
"pageSize": 50,
"totalPages": 1
} /api/tickets/{id} Get one ticket, with its comments, links, attachments and time entries.
Response
{
"id": "00000000-0000-0000-0000-000000000008",
"projectId": "00000000-0000-0000-0000-000000000003",
"boardColumnId": "00000000-0000-0000-0000-000000000006",
"priorityId": "00000000-0000-0000-0000-000000000007",
"ticketNumber": "ACME-42",
"title": "Homepage hero image is cut off on mobile",
"description": "On iPhone widths the hero image overflows its container and hides the CTA button beneath it.",
"storyPoints": 3,
"sortOrder": 1,
"dueDate": "2026-09-20T00:00:00Z",
"priorityName": "High",
"priorityColor": "#EF8354",
"boardColumnName": "In Progress",
"boardColumnCategory": 3,
"projectName": "Acme Corp Website",
"projectKey": "ACME",
"createdAt": "2026-09-01T14:32:00Z",
"updatedAt": "2026-09-05T10:15:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"comments": [
{
"id": "00000000-0000-0000-0000-000000000009",
"ticketId": "00000000-0000-0000-0000-000000000008",
"userId": "00000000-0000-0000-0000-000000000001",
"userDisplayName": "Jordan Lee",
"body": "Repro'd on iPhone 14 Pro at 100% zoom.",
"type": 0,
"visibility": 0,
"customerUserId": null,
"customerUserDisplayName": null,
"createdAt": "2026-09-02T08:00:00Z"
}
],
"links": [],
"attachments": [],
"timeEntries": [],
"totalHoursLogged": 0,
"hasPortalUsers": false,
"createdByUserId": "00000000-0000-0000-0000-000000000001",
"createdByName": "Jordan Lee",
"enrolledProjectMembers": null
} /api/tickets/{id} Update a ticket's priority, title, description, story points or due date. Only the fields you supply change — an omitted field is left alone.
Ticket title and description are customer-facing: assume the customer will read them.
Request body
{
"priorityId": "00000000-0000-0000-0000-000000000007",
"title": "Homepage hero image is cut off on mobile",
"description": "On iPhone widths the hero image overflows its container and hides the CTA button beneath it.",
"storyPoints": 3,
"dueDate": "2026-09-20T00:00:00Z"
} Response
{
"id": "00000000-0000-0000-0000-000000000008",
"projectId": "00000000-0000-0000-0000-000000000003",
"boardColumnId": "00000000-0000-0000-0000-000000000006",
"priorityId": "00000000-0000-0000-0000-000000000007",
"ticketNumber": "ACME-42",
"title": "Homepage hero image is cut off on mobile",
"description": "On iPhone widths the hero image overflows its container and hides the CTA button beneath it.",
"storyPoints": 3,
"sortOrder": 1,
"dueDate": "2026-09-20T00:00:00Z",
"priorityName": "High",
"priorityColor": "#EF8354",
"boardColumnName": "In Progress",
"boardColumnCategory": 3,
"projectName": "Acme Corp Website",
"projectKey": "ACME",
"createdAt": "2026-09-01T14:32:00Z",
"updatedAt": "2026-09-09T13:00:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"comments": [
{
"id": "00000000-0000-0000-0000-000000000009",
"ticketId": "00000000-0000-0000-0000-000000000008",
"userId": "00000000-0000-0000-0000-000000000001",
"userDisplayName": "Jordan Lee",
"body": "Repro'd on iPhone 14 Pro at 100% zoom.",
"type": 0,
"visibility": 0,
"customerUserId": null,
"customerUserDisplayName": null,
"createdAt": "2026-09-02T08:00:00Z"
}
],
"links": [],
"attachments": [],
"timeEntries": [],
"totalHoursLogged": 0,
"hasPortalUsers": false,
"createdByUserId": "00000000-0000-0000-0000-000000000001",
"createdByName": "Jordan Lee",
"enrolledProjectMembers": null
} /api/tickets/{id}/move Move a ticket to a different column on its own project's board, at an optional position. Returns 204 No Content.
Request body
{
"boardColumnId": "00000000-0000-0000-0000-000000000006",
"sortOrder": 2
} /api/tickets/{id}/move-to-project Move a ticket to a different project entirely (not just a different column). Requires Admin.
Request body
{
"targetProjectId": "00000000-0000-0000-0000-000000000016",
"reason": "Reassigning to the correct project"
} Response
{
"id": "00000000-0000-0000-0000-000000000008",
"projectId": "00000000-0000-0000-0000-000000000016",
"boardColumnId": null,
"priorityId": "00000000-0000-0000-0000-000000000007",
"ticketNumber": "MOB-51",
"title": "Homepage hero image is cut off on mobile",
"description": "On iPhone widths the hero image overflows its container and hides the CTA button beneath it.",
"storyPoints": 3,
"sortOrder": 1,
"dueDate": "2026-09-20T00:00:00Z",
"priorityName": "High",
"priorityColor": "#EF8354",
"boardColumnName": null,
"boardColumnCategory": null,
"projectName": "Mobile App",
"projectKey": "MOB",
"createdAt": "2026-09-01T14:32:00Z",
"updatedAt": "2026-09-09T13:05:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"comments": [
{
"id": "00000000-0000-0000-0000-000000000009",
"ticketId": "00000000-0000-0000-0000-000000000008",
"userId": "00000000-0000-0000-0000-000000000001",
"userDisplayName": "Jordan Lee",
"body": "Repro'd on iPhone 14 Pro at 100% zoom.",
"type": 0,
"visibility": 0,
"customerUserId": null,
"customerUserDisplayName": null,
"createdAt": "2026-09-02T08:00:00Z"
}
],
"links": [],
"attachments": [],
"timeEntries": [],
"totalHoursLogged": 0,
"hasPortalUsers": false,
"createdByUserId": "00000000-0000-0000-0000-000000000001",
"createdByName": "Jordan Lee",
"enrolledProjectMembers": null
} /api/tickets/{id}/assign Set a ticket's assignees, replacing the existing list (an empty list unassigns everyone). Every id must already be a member of the project — unless you are an Admin or Manager, in which case naming a non-member adds them to the project automatically, as part of this same call.
Request body
{
"userIds": [
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000014"
]
} Response
{
"enrolledProjectMembers": [
{
"id": "00000000-0000-0000-0000-000000000014",
"displayName": "Priya Shah",
"email": "priya@acmeconsulting.example",
"avatarUrl": null,
"role": "Member",
"isActive": true
}
]
} /api/tickets/{id}
Delete a ticket. Refused with 409 ticket_has_time_entries
(naming the hours total) when the ticket has logged time — there is no override on
this connector. Returns 204 No Content.
/api/tickets Create a ticket. Requires an Idempotency-Key header — see the section below.
Ticket title and description are customer-facing: assume the customer will read them.
Also send Idempotency-Key: dft-1f2e3d4c-5b6a-4988-9c01-7e8f9a0b1c2d —
see Creating safely below.
Request body
{
"projectId": "00000000-0000-0000-0000-000000000003",
"boardColumnId": "00000000-0000-0000-0000-000000000005",
"priorityId": "00000000-0000-0000-0000-000000000007",
"title": "Contact form validation error message is unreadable",
"description": "Error text renders white-on-white when the email field fails validation.",
"storyPoints": 2,
"dueDate": "2026-09-25T00:00:00Z",
"assigneeIds": [
"00000000-0000-0000-0000-000000000001"
]
} Response (201 Created)
{
"id": "00000000-0000-0000-0000-000000000010",
"projectId": "00000000-0000-0000-0000-000000000003",
"boardColumnId": "00000000-0000-0000-0000-000000000005",
"priorityId": "00000000-0000-0000-0000-000000000007",
"ticketNumber": "ACME-43",
"title": "Contact form validation error message is unreadable",
"description": "Error text renders white-on-white when the email field fails validation.",
"storyPoints": 2,
"sortOrder": 1,
"dueDate": "2026-09-25T00:00:00Z",
"priorityName": "High",
"priorityColor": "#EF8354",
"boardColumnName": "Backlog",
"boardColumnCategory": 1,
"projectName": "Acme Corp Website",
"projectKey": "ACME",
"createdAt": "2026-09-09T12:00:00Z",
"updatedAt": "2026-09-09T12:00:00Z",
"assignees": [
{
"userId": "00000000-0000-0000-0000-000000000001",
"displayName": "Jordan Lee",
"avatarUrl": null
}
],
"comments": [],
"links": [],
"attachments": [],
"timeEntries": [],
"totalHoursLogged": 0,
"hasPortalUsers": false,
"createdByUserId": "00000000-0000-0000-0000-000000000001",
"createdByName": "Jordan Lee",
"enrolledProjectMembers": null
} /api/tickets/{ticketId}/comments List a ticket's comments.
Response
[
{
"id": "00000000-0000-0000-0000-000000000009",
"ticketId": "00000000-0000-0000-0000-000000000008",
"userId": "00000000-0000-0000-0000-000000000001",
"userDisplayName": "Jordan Lee",
"body": "Repro'd on iPhone 14 Pro at 100% zoom.",
"type": 0,
"visibility": 0,
"customerUserId": null,
"customerUserDisplayName": null,
"createdAt": "2026-09-02T08:00:00Z"
}
] /api/tickets/{ticketId}/comments Add a comment to a ticket. Requires an Idempotency-Key header — see the section below.
Also send Idempotency-Key: dft-7c2b9e10-3a4d-4f21-8e6c-1a9b0d5f3e42 —
see Creating safely below.
Request body
{
"body": "Fix is up for review \u2014 see PR #482.",
"visibility": 0
} Response (201 Created)
{
"id": "00000000-0000-0000-0000-000000000011",
"ticketId": "00000000-0000-0000-0000-000000000008",
"userId": "00000000-0000-0000-0000-000000000001",
"userDisplayName": "Jordan Lee",
"body": "Fix is up for review \u2014 see PR #482.",
"type": 0,
"visibility": 0,
"customerUserId": null,
"customerUserDisplayName": null,
"createdAt": "2026-09-09T12:05:00Z"
} A comment posted with a token is always internal — however the request asks for it, it can never be made customer-visible on the portal.
/api/tickets/{ticketId}/links List a ticket's links to other tickets (blocks/blocked-by/related/duplicate).
Response
[
{
"id": "00000000-0000-0000-0000-000000000012",
"sourceTicketId": "00000000-0000-0000-0000-000000000008",
"targetTicketId": "00000000-0000-0000-0000-000000000013",
"targetTicketNumber": "ACME-40",
"targetTicketTitle": "Login button unresponsive on Safari",
"linkType": 1,
"isSource": true,
"isTargetDone": false
}
] /api/tickets/{ticketId}/links
Link this ticket to another one. linkType accepts
RelatedTo, BlockedBy, Blocks, DuplicateOf or DuplicatedBy; the wire value is the
enum's integer (BlockedBy = 1).
Request body
{
"targetTicketId": "00000000-0000-0000-0000-000000000013",
"linkType": 1
} Response
{
"id": "00000000-0000-0000-0000-000000000012",
"sourceTicketId": "00000000-0000-0000-0000-000000000008",
"targetTicketId": "00000000-0000-0000-0000-000000000013",
"targetTicketNumber": "ACME-40",
"targetTicketTitle": "Login button unresponsive on Safari",
"linkType": 1,
"isSource": true,
"isTargetDone": false
} /api/tickets/{ticketId}/links/{id} Remove a link between two tickets. Returns 204 No Content.
/api/tickets/{ticketId}/attachments List a ticket's attachments — name, type, size, uploader (staff or customer, by display name), date, a deep link into the app, and whether the connector can read it as text.
readable is true
when the file's type and size are eligible for text extraction; a read can still
refuse attachment_no_text
(no text layer, or an unreadable one) or attachment_unavailable
(damaged file).
Attachments may have been uploaded by the customer; reading one through Claude is the same act as opening it in the app.
Response
[
{
"id": "00000000-0000-0000-0000-000000000033",
"ticketId": "00000000-0000-0000-0000-000000000008",
"fileName": "repro-screenshot.png",
"contentType": "image/png",
"fileSizeBytes": 184320,
"url": null,
"uploadedByUserId": null,
"uploadedByCustomerUserId": "00000000-0000-0000-0000-000000000034",
"uploadedByDisplayName": "Taylor Morgan",
"createdAt": "2026-09-02T07:50:00Z",
"uploadedByKind": "customer",
"kind": "file",
"appUrl": "https://app.digitaldevflow.com/tickets/00000000-0000-0000-0000-000000000008",
"readable": false
}
] /api/tickets/{ticketId}/attachments/{id}/text
Read a ticket attachment's text content — same formats, caps and refusal codes as a
project file's text route above:
page_from/page_to
for a PDF page range,
offset/length
for every other format, a 5 MiB (5,242,880-byte) extraction cap and a 60,000-character
response cap. Writes one audit row.
Attachments may have been uploaded by the customer; reading one through Claude is the same act as opening it in the app.
Response
{
"id": "00000000-0000-0000-0000-000000000035",
"fileName": "spec.pdf",
"contentType": "application/pdf",
"format": "pdf",
"contentTypeNote": null,
"text": "PRODUCT SPEC\n\n3. Mobile layout\nThe hero image container must use object-fit: cover...",
"totalCharacters": 60000,
"truncated": true,
"pageCount": 9,
"pageFrom": 3,
"pageTo": 5,
"bytesRead": 512000
} What a token cannot do
This holds whatever your role is — including Admin. A token cannot reach:
- Settings, billing, or QuickBooks
- User management, roles, or anyone else's tokens — a token cannot create another token
- Credentials, integrations, or the operator console
- Uploading, renaming or deleting files or attachments — reading them is allowed
An assignee — whether named on a create or through assign_ticket —
must already be a member of that project,
unless your token belongs to an Admin or Manager: naming a non-member then adds them
to the project automatically, as part of the same request, and the response names who
was added. A Member's token gains nothing extra here — a non-member assignee is still
refused.
A comment posted with a token is always internal — however the request asks for it, it can never be made customer-visible on the portal.
A ticket that carries any logged time cannot be deleted with a token at all; there is no override reachable this way. Delete it in the browser instead, where the confirmation dialog makes that override explicit.
Limits and failures
A token is limited to 120 requests per minute. Go over it and you get a 429 with no body — back off and retry after a few seconds.
An unknown, revoked, or expired token — or one whose owner's account was deactivated — is deliberately indistinguishable: all four look exactly like this, so a script can never learn whether a token it's guessing ever existed.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json
{"error":"Unauthorized"} A route your token cannot reach — whatever your role — is a bare 403 with an empty body:
HTTP/1.1 403 Forbidden
A bad request — invalid input, or a rule your request broke, like the project-membership rule above — comes back as a 400 with a reason:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"status":400,"title":"Bad Request","detail":"assigneeIds must already be members of the project"}
Creating safely: Idempotency-Key
Both routes that create something —
POST /api/tickets and
POST /api/tickets/{ticketId}/comments —
require an Idempotency-Key
header on every token-authenticated request. Generate a fresh key per NEW ticket or comment
(a UUID is fine) and send the same one again only when retrying that exact request.
Send it without the header and you get a 400 naming the header:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"status":400,"title":"Bad Request","detail":"Idempotency-Key header is required"} Send the request, don't hear back — your connection drops, a proxy times out, anything — and the ONE rule that matters is: retry with the SAME key. Sending a new key on retry is how a duplicate ticket, and a duplicate notification to your team, gets created; that notification can't be recalled once it's sent. Retry with the same key within 24 hours and you get back the exact original response — the same 201, the same body — with one extra header, and no second ticket is created:
HTTP/1.1 201 Created
Idempotency-Replayed: true
Content-Type: application/json
{ ...the exact same ticket you got back the first time... } Send that same key again with a different body — a different title, a different project — and you get a 409, because the key is already spoken for:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{"status":409,"title":"Conflict","detail":"Idempotency-Key was already used for a different request"} A key is scoped to you, and to the ONE route you sent it to — the same key sent to the comments route is a completely separate record, and creates its own comment. A replayed request still counts toward your 120-per-minute limit. Keys are kept for 24 hours; after that, reusing one starts something new.
The Claude Code skill
DevFlow ships a Claude Code skill that wraps all of this — install it once, and Claude can look up projects, boards and people, then file or comment on tickets for you.
-
Get the skill into Claude's skills folder — download the zip and unzip it, or fetch
the two files directly from a terminal:
Download devflow-tickets-skill.zip — unzip into
~/.claude/skills/.mkdir -p ~/.claude/skills && unzip devflow-tickets-skill.zip -d ~/.claude/skills
No download folder handy? Pull the two files straight from a terminal:
mkdir -p ~/.claude/skills/devflow-tickets/scripts curl -fsSL https://digitaldevflow.com/skills/devflow-tickets/SKILL.md -o ~/.claude/skills/devflow-tickets/SKILL.md curl -fsSL https://digitaldevflow.com/skills/devflow-tickets/scripts/devflow-tickets.mjs -o ~/.claude/skills/devflow-tickets/scripts/devflow-tickets.mjs
-
Export your token (never commit it to a file):
export DEVFLOW_API_TOKEN="dft_a1b2c3d4…" export DEVFLOW_API_URL="https://app.digitaldevflow.com"
-
Ask Claude Code to use it.
lookuptakes a search term;createtakes a JSON ticket draft — as a file path, or piped in on stdin with-— not command-line flags;commenttakes a ticket key and a message:$ node devflow-tickets.mjs lookup "Acme" Projects Acme Corp Website (ACME) Board: Main Board (default) Columns: Backlog, To Do, In Progress, Done Priorities Staff Ana Cruz <ana@acme.dev> $ echo '{"project": "Acme", "title": "Fix mobile hero image"}' | node devflow-tickets.mjs create - CREATED ACME-43 — Fix mobile hero image — https://app.digitaldevflow.com/tickets/00000000-0000-0000-0000-000000000010 $ node devflow-tickets.mjs comment ACME-43 "Fix is up for review" CREATED ACME-43 — https://app.digitaldevflow.com/tickets/00000000-0000-0000-0000-000000000010The JSON draft acceptsprojectandtitle(both required), plus optionalboard,column,priority,descriptionandassignee— each matched by name, same as the API itself. An array of drafts files more than one ticket in a single call.
The skill reaches exactly the same twenty-seven routes documented above — nothing a script can do here, Claude can't, and nothing Claude can do here, a script couldn't.
Use DevFlow in claude.ai
The same twenty-seven routes are available inside claude.ai, as a connector. Nothing is installed and no token is typed anywhere — you add one URL and sign in with your DevFlow account.
https://app.digitaldevflow.com/mcp
Client ID
28c99e56-5743-40ac-a16d-2c5f2b8b4563
Add it once:
- In claude.ai, open Settings → Connectors and choose Add custom connector.
- Name it DevFlow, paste the URL above, and choose Connect.
- If claude.ai asks how to authenticate, pick No client ID — register one automatically — that's the option it detects by default. If it instead shows a choice of OAuth client options, pick Use your own OAuth client, paste the Client ID shown above, and leave Client Secret empty — DevFlow's connector is a public client and doesn't use one. Don't pick "Use Anthropic's hosted client metadata" — it isn't compatible with DevFlow's sign-in.
- Sign in with your DevFlow account and approve the requested access, staying signed in when asked. That's the whole setup: there's no token to copy anywhere.
Claude can then look up projects, boards and columns, priorities and staff, read tickets, their comments, their links and their attachments, and a project's files as text (plain text, Markdown, CSV, JSON, HTML, PDF text layer, DOCX or .xlsx — up to 5 MiB (5,242,880 bytes) and 60,000 characters, and never uploading, renaming or deleting one), and write: create tickets and comments, edit a ticket's title, description, priority, story points and due date, move it between columns and between projects, assign or unassign it, delete it when it carries no logged time, manage its links to other tickets, and update a project's own details and its membership — as you, in your workspace, with your role and your project access.
Twenty-seven tools, one for each route above:
list_projects get_project update_project list_boards add_project_member remove_project_member list_project_files read_project_file get_board list_priorities list_users get_current_user list_tickets get_ticket update_ticket move_ticket move_ticket_to_project assign_ticket delete_ticket create_ticket list_comments add_comment list_ticket_links add_ticket_link remove_ticket_link list_ticket_attachments read_ticket_attachment
The limits are the token's limits, unchanged — including for an Admin. Through the connector Claude cannot reach:
- Settings, billing, or QuickBooks
- User management, roles, or anyone else's tokens — a token cannot create another token
- Credentials, integrations, or the operator console
- Uploading, renaming or deleting files or attachments — reading them is allowed
And the project-membership rule holds: anyone Claude names as an assignee must already be a member of that project — unless you are an Admin or Manager, in which case naming a non-member adds them to the project automatically, as part of the same request, and Claude will say so. As a Member, naming a non-member is still refused.
A comment Claude adds through the connector is always internal — it can never be made customer-visible on the portal. And a ticket that carries any logged time cannot be deleted through the connector at all; delete it in the browser instead.
Connector requests are limited to 120 requests per minute.
Why no token? A token is a password you have to keep somewhere. The connector doesn't need one: claude.ai sends you to the normal DevFlow sign-in, and access is granted to your account, not to a string you stored. Turn a person's DevFlow account off and their connector stops working on its very next request. And a connector sign-in only ever reaches the routes on this page — it can't be pointed at the rest of the API. If you're already signed in to DevFlow in that browser, connecting again won't ask you to sign in a second time.
Done with it? Remove the connector in claude.ai. There's nothing to revoke in DevFlow — no token was ever created.
Still have questions?
Contact us