Connect a Dunly workspace to your own systems — read and write tasks (and their attachment metadata) over HTTP, with delta sync, cursor pagination, and deletion tombstones.
Every request needs an API integration key, sent as a bearer token:
Authorization: Bearer dnk_your_key_here
You generate a key in Settings → API integration keys inside the app. A key is shown once — store it somewhere safe. Revoke a key any time from the same screen; a revoked key stops working immediately.
/api/v1 surface below; the unversioned app routes answer 401 to a key.All endpoints live under your workspace:
https://YOUR_HANDLE.rooche.biz/dunly/api/v1/orgs/YOUR_HANDLE
Content-Type: application/json on writes).2026-08-06T00:00:00.000Z).{ data, page }, a single
record (read or write) is { data }.https://YOUR_HANDLE.rooche.biz/dunly/openapi.json (OpenAPI 3.1).{
"data": [ …records… ],
"page": { "limit": 100, "hasMore": true, "nextCursor": "eyJtIjoi…" }
}
A task is title, description, tags (an array of strings),
and done, plus the optional dueDate and priority
(null when unset). Each record also carries updatedAt (touched on every
create, update, and delete — the value you page a delta feed by) and deletedAt
(null for a live task; an ISO-8601 timestamp for a tombstone).
/api/v1/orgs/YOUR_HANDLE/tasksList tasks with cursor pagination, delta sync, and deletion tombstones.
| Query | Type | Notes |
|---|---|---|
limit | integer | Page size. Default 100, max 500. |
cursor | string | The nextCursor from the previous page. Keyset-based (stable under inserts) — page until hasMore is false. |
updatedSince | string | ISO-8601. Returns only records whose updatedAt is strictly after this, oldest-change first — a delta feed. In this mode tombstones are always included (a delete is a change). Store the newest updatedAt you saw and pass it back next time. |
includeDeleted | boolean | When not doing a delta, set true to include soft-deleted records (each has a non-null deletedAt). |
| Query | Type | Notes |
|---|---|---|
done | boolean | Filter to done (true) or open (false) tasks. |
expand | string | expand=attachments inlines each task's live attachment metadata so you don't fetch them per task. |
deletedAt in the updatedSince feed.
Apply it as a delete on your side.# first sync: no watermark yet, page through everything
curl "https://YOUR_HANDLE.rooche.biz/dunly/api/v1/orgs/YOUR_HANDLE/tasks?limit=200" \
-H "Authorization: Bearer dnk_your_key_here"
# → { "data": [...], "page": { "hasMore": true, "nextCursor": "…" } }
# follow nextCursor until hasMore is false, remembering the max updatedAt you saw.
# later: fetch only what changed (incl. deletes) since that watermark
curl "https://YOUR_HANDLE.rooche.biz/dunly/api/v1/orgs/YOUR_HANDLE/tasks?updatedSince=2026-08-06T09:15:04.000Z" \
-H "Authorization: Bearer dnk_your_key_here"
/api/v1/orgs/YOUR_HANDLE/tasks/:idFetch one task: { "data": task }. A soft-deleted task answers 404 unless ?includeDeleted=true. Supports ?expand=attachments.
/api/v1/orgs/YOUR_HANDLE/tasksCreate a task. Returns 201 with { "data": task }.
| Field | Type | Notes |
|---|---|---|
title | string | Required, ≤ 200 chars. |
description | string? | Optional, ≤ 4000 chars. |
tags | string[]? | Optional, ≤ 20 tags of ≤ 40 chars. |
done | boolean? | Optional; defaults to false. |
dueDate | string? | Optional ISO-8601 timestamp; an unparseable value is stored as null. |
priority | string? | Optional low | medium | high; anything else is stored as null. |
Send an Idempotency-Key header (any unique string, ≤ 200 chars) so a retried create
after a dropped response never makes a duplicate — the first response is stored per workspace and
replayed verbatim for the same key.
curl -X POST https://YOUR_HANDLE.rooche.biz/dunly/api/v1/orgs/YOUR_HANDLE/tasks \
-H "Authorization: Bearer dnk_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9c1a7e-import-4821" \
-d '{ "title": "Follow up with supplier", "tags": ["work"] }'
/api/v1/orgs/YOUR_HANDLE/tasks/:idUpdate a task (same body as create). done may ride along; omitting it leaves the current value (or use the dedicated POST …/done below to flip it without re-sending the body). priority and dueDate follow the same rule: omit the field to leave it unchanged, or send it as null to clear it — so a partial edit never silently wipes a task’s due date or priority. (title, description, and tags are replaced from the body as sent.) Returns { "data": task }.
/api/v1/orgs/YOUR_HANDLE/tasks/:id/doneSet or toggle done without re-sending the whole body (PUT is a full replace and needs a title). Send { "done": true } / { "done": false } to set it explicitly, or an empty body to toggle. Becoming done starts the auto-archive clock; un-doing clears done, doneAt, and archivedAt. Returns { "data": task }.
/api/v1/orgs/YOUR_HANDLE/tasks/:id/archiveArchive a task — file it out of the active list into the archive. Distinct from delete: the task stays owned and restorable (a tombstone is gone). Idempotent — re-archiving keeps the original archivedAt. Returns { "data": task } with archivedAt set.
/api/v1/orgs/YOUR_HANDLE/tasks/:id/unarchiveRestore an archived task to the active list (clears archivedAt; leaves done as-is). Idempotent. Returns { "data": task }.
/api/v1/orgs/YOUR_HANDLE/tasks/archivedList archived tasks — { "data": [task, …] }, newest-archived first. Unpaged (an owner’s archive is bounded). The active GET …/tasks list excludes archived tasks; this is where they live.
/api/v1/orgs/YOUR_HANDLE/tasks/:idSoft-delete a task. Returns { "data": task } with the task as a tombstone (deletedAt set). Repeating the delete answers the same tombstone.
A task can carry media — voice, video, image, or file. The versioned API exposes the
metadata and lifecycle of those attachments: what exists, its sniffed
mimeType, sizeBytes, durationMs, the delta/tombstone feed to
reconcile against, and soft-delete.
/api/v1/orgs/YOUR_HANDLE/tasks/:taskId/attachmentsOne task's attachment metadata (the task must be yours; a deleted task's list is still readable for reconciliation). Same pagination/delta parameters as the tasks list.
/api/v1/orgs/YOUR_HANDLE/attachmentsEvery attachment across your tasks — the delta/tombstone feed (?updatedSince, ?includeDeleted, ?cursor). ?taskId narrows to one task.
{
"data": [
{
"id": "att_…", "taskId": "task_…", "kind": "voice",
"mimeType": "audio/webm", "filename": "voice", "sizeBytes": 48213,
"durationMs": 4120, "createdAt": "2026-08-06T09:15:04.000Z",
"updatedAt": "2026-08-06T09:15:04.000Z", "deletedAt": null
}
],
"page": { "limit": 100, "hasMore": false, "nextCursor": null }
}
/api/v1/orgs/YOUR_HANDLE/attachments/:idOne attachment’s metadata: { "data": attachment }. Never the bytes. A tombstone answers 404 unless ?includeDeleted=true.
/api/v1/orgs/YOUR_HANDLE/attachments/:idSoft-delete an attachment. The record tombstones (so a sync can reconcile it) but the file itself is removed immediately — deletion reclaims storage quota. Returns { "data": attachment } as a tombstone.
Errors are JSON: { "error": "message" }, with a matching status:
| Status | Meaning |
|---|---|
400 | Bad request — a validation rule failed (the message says which). |
401 | Missing or invalid key (or a key on a non-/api/v1 route). |
403 | Not allowed for a key (key management, webhooks, support) — or the workspace is suspended (code: "suspended"). |
404 | No such workspace or record. Another principal's task or attachment is indistinguishable from a missing one — that is the privacy model, not an omission. |
429 | Rate limit exceeded — slow down and retry after Retry-After seconds. |
RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset
(seconds until the window resets) so you can pace yourself. Over the limit answers
429 with a Retry-After header — back off and retry. The exact numbers
depend on your plan; contact us to raise them.