Tasks API
Publish tasks to Cryptauk from your own software — a business tool, a monitoring system, a script — and track who is on them and when they are done.
An API key gives access to tasks, never to the vault: no passwords, no notes, no files. It will live on another server, in a configuration file: if it ever leaks, only tasks are exposed.
Two kinds of keys
| Group key | Personal key | |
|---|---|---|
| Used to | feed a group with tasks from software | act on one person's tasks, on their behalf |
| Created for | a group you are the creator or an admin of | your own account |
| Creates tasks | shared with the group — and also with other groups its creator administers | owned by you; you can share them |
| Reads | all tasks shared with the group | your tasks, and those entrusted to you — or, if it is restricted, only your tasks in some categories and the common tasks of some groups |
| Edits, deletes | tasks created by a key of this group | tasks you own |
| Claims, completes | no: that is a human act | yes, as in the app |
A key never has more rights than the person who created it. A group key stops working if its creator leaves the group or is no longer an admin; a personal key, if the account is disabled.
Getting a key
For now, keys are issued by the Cryptauk team. Write to us stating the software you want to connect, and whether you need a personal key or a group key — in that case, the name of the group, which you must be the creator or an admin of.
For a personal key, say whether it should cover all your tasks, or only some categories of your tasks and/or the common tasks of some groups (see “Restricted personal key”).
A key looks like ckt_ followed by 43 characters. It is given to you only once: Cryptauk keeps only a fingerprint and cannot recover it. If it is lost, it is replaced by a new one and the old one is revoked.
The person has no Cryptauk account? The creator or an admin of the group can create a task account for them in the app — no vault, no login — with its own personal key. Your software uses it to act on their behalf. If they ever sign up with the same address, the account becomes a full one and the key keeps working.
Authentication
All requests go to https://cryptauk.com/api/ext/v1, in JSON and UTF-8. Pass the key in the Authorization header — or in X-Api-Key if your tool cannot set the former.
Start with GET /me, which describes the key: the simplest way to check an installation.
curl https://cryptauk.com/api/ext/v1/me \
-H "Authorization: Bearer $CRYPTAUK_API_KEY"
{
"type": "group",
"label": "My software",
"user": {
"id": 2,
"name": "Camille"
},
"group": {
"id": 281474976710668,
"name": "Field team"
},
"restrictedToCategories": null,
"restrictedToGroups": null
}
A missing, malformed, unknown or revoked key, or one whose creator lost their rights, always gets the same answer: 401 with { "error": "invalid_api_key" }. Nothing tells which of these reasons applies.
restrictedToCategories, restrictedToGroups: for a restricted personal key, its task categories and its groups; null otherwise.
The task object
| Field | Meaning |
|---|---|
id | Cryptauk identifier. A large integer: it fits in a JavaScript Number, but store it as 64 bits or as text. |
name, description, priority | The content. priority ranges from 0 to 255; higher = more urgent. |
category | { id, name }: the owner's task category. Only with the owner's personal key; null otherwise. |
isDone | Completed or not. |
externalRef | Your reference, given at creation (see “Avoiding duplicates”). null if none. |
createdVia | Name of the API key that created the task; null if it was created in a Cryptauk app. |
owner | The owner: only they can edit the content. |
sharedGroups | All the groups of the task, [{ id, name }], main one first: a task can have several. |
sharedGroup | The main group alone, kept for compatibility. |
sharedUser | The person the task is shared with directly — exclusive of groups. |
imageCount | Number of attached images (see “Attached images”). |
claimedBy, claimedAt | Who said “I am on it”, and when. null: nobody. |
doneBy, doneAt | Who completed it, and when. |
createdAt, updatedAt | ISO 8601 dates in UTC. updatedAt changes on every edit, claim or completion. |
{
"id": 562949953421340,
"name": "Replace the alarm panel battery",
"description": "Lyon site, access via dock B",
"priority": 2,
"category": null,
"isDone": false,
"externalRef": "mysoftware:job:1234",
"createdVia": "My software",
"owner": {
"id": 2,
"name": "Camille"
},
"sharedGroup": {
"id": 281474976710668,
"name": "Field team"
},
"sharedGroups": [
{
"id": 281474976710668,
"name": "Field team"
}
],
"sharedUser": null,
"imageCount": 1,
"claimedBy": {
"id": 281474976710664,
"name": "Alex"
},
"claimedAt": "2026-10-12T09:41:07.000Z",
"doneBy": null,
"doneAt": null,
"createdAt": "2026-10-12T09:12:55.000Z",
"updatedAt": "2026-10-12T09:41:07.000Z"
}
People's names are the ones chosen in Cryptauk, or their e-mail address otherwise. Tolerate unknown fields: v1 may gain fields, it will never lose or rename one.
Routes
Paths are relative to https://cryptauk.com/api/ext/v1. Routes marked “personal” answer 403 not_allowed_for_group_key to a group key.
| Route | Key | Effect |
|---|---|---|
GET /me | any | describes the key |
GET /tasks | any | lists tasks (filters: see “Synchronizing”) |
GET /tasks/:id | any | one task |
POST /tasks | any | creates a task: 201, or 200 if the externalRef already existed |
PATCH /tasks/:id | any | edits name, description, priority — and isDone with a personal key, on a task that is not shared |
DELETE /tasks/:id | any | deletes: 204 |
POST /tasks/:id/claim | personal | “I am on it” |
POST /tasks/:id/release | personal | “I am dropping it”, or unblocking a task (owner) |
POST /tasks/:id/done | personal | “it is done” — only by whoever claimed it |
POST /tasks/:id/reopen | personal | reopens a completed task (owner) |
PUT /tasks/:id/share | personal | shares with sharedGroupId or sharedUserId, or {} to stop (owner) |
GET /groups | personal | your groups: [{ id, name }] |
GET /contacts | personal | the people you can share with directly |
curl -X POST https://cryptauk.com/api/ext/v1/tasks \
-H "Authorization: Bearer $CRYPTAUK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Replace the alarm panel battery",
"description": "Lyon site, access via dock B",
"priority": 2,
"externalRef": "mysoftware:job:1234"
}'
name is required (255 characters max), description accepts 10,000, externalRef 200. With a personal key, add sharedGroupIds (one or more groups) or sharedUserId to share right away; A personal key can also pass categoryId, one of its task categories. a group key always shares with its group, and can add others (see “Several groups”). PATCH is partial: send only what changes.
Several groups
A group key can publish a task for other groups too, with additionalGroupIds. Example: one team’s key publishes a task that the communication team must see as well.
- accepted only if the key’s creator is the creator or an admin of each of these groups: a key never has more rights than them (
403 owner_only, or404if they are not a member); - it remains a single task: visible to all these groups, and whoever claims it first, from any group, locks it for everyone;
- the key’s group stays the main group; 10 groups at most in total.
{
"name": "Approve the poster artwork",
"externalRef": "mysoftware:com:42",
"additionalGroupIds": [281474976710700]
}
Restricted personal key
By default, a personal key covers all of the person’s tasks — as valid a choice as any. It can also be restricted, at creation or later without changing the key: to categories of their tasks (their own tasks filed there) and/or to groups they belong to (their common tasks). Example: one category per organisation for personal tasks, plus the organisation’s group to be able to claim its common tasks.
- it sees their personal tasks in these categories, subcategories included (even those created later), and the common tasks of these groups; any other task answers
404, as if it did not exist; - on a common task, it acts like the person in the app:
claim,release,done— whoever claims first locks it for the whole group; - it creates in its categories (
categoryId) and/or for its groups (sharedGroupIds) —403 out_of_key_scopeoutside its scope. When it must be specified,400 categoryId_requiredorsharedGroupIds_required; - it does not re-share and never shares with a single person;
GET /groupsonly returns its groups; - if its categories and groups all disappear, it no longer sees anything. It never becomes unrestricted.
GET /me returns restrictedToCategories and restrictedToGroups, or null for an unrestricted key.
Avoiding duplicates: externalRef
A network call can fail after the server has processed it: the response is lost, your software retries and, without care, the task would be created twice.
Always pass an externalRef that identifies the object on your side, for example mysoftware:job:1234:
- no task carries this reference: it is created, answer
201; - a task already carries it: nothing is created, the existing task is returned with
200.
This holds even for simultaneous calls, and even after the key is renewed. The reference also lets you find a task without storing its id: GET /tasks?externalRef=mysoftware:job:1234.
Synchronizing
GET /tasks accepts these parameters:
| Parameter | Effect |
|---|---|
done=0, done=1 | only open tasks, or only completed ones |
externalRef=… | the task carrying this reference |
updatedSince=… | tasks changed since this ISO date, inclusive |
limit=… | 1 to 1000, 200 by default |
scope=mine|shared|all | personal key: your tasks, shared ones, or everything (default) |
Results are sorted by ascending updatedAt. To follow progress without reading everything again:
- a first call, without
updatedSince; - keep the most recent
updatedAtyou received; - next call with
updatedSinceset to that value; - deduplicate by
id: the bound is inclusive, so the last task comes back once more.
If you receive exactly limit tasks, more may be waiting: call again right away with the new bound. Reuse the date we returned rather than your server clock: no clock skew can then make you miss a task.
A deleted task simply disappears from responses: synchronization does not report it. To detect it, query it by externalRef (answer []), or reread the full list from time to time.
Claiming tasks
A shared task must be claimed before it can be completed: two people never work on it at the same time. This flow is for personal keys only.
claim: the free task becomes “claimed by you”;done: whoever claimed it completes it;release: the claimer drops it, or the owner unblocks the task;reopen: the owner reopens a completed task, which becomes free again.
{
"error": "already_claimed",
"claimedBy": { "id": 281474976710664, "name": "Alex" }
}
One claimer at a time, guaranteed by the server even if two people press at the same instant. Show “Alex is already on it” and do not retry in a loop. A task that is not shared has no such flow: it is ticked directly with PATCH and { "isDone": true }.
Attached images
A task can carry images — screenshots, photos: 10 at most, 5 MB each. Any key that sees the task can list, read and add them.
| Route | Effect |
|---|---|
GET /tasks/:id/images | the list, without the content |
POST /tasks/:id/images | adds { "data": "<base64>", "name": "optional" } |
GET /tasks/:id/images/:imageId | the raw content, with its Content-Type |
DELETE /tasks/:id/images/:imageId | deletes: the image’s author or the task’s owner; a group key, what a key of its group added |
- the type is determined from the first bytes: PNG, JPEG, WebP or GIF (
415 unsupported_image_typeotherwise); - 5 MB at most once decoded (
413 image_too_large): resize before sending; 10 images per task (409 too_many_images); - images are in clear, like tasks: never attach anything that contains a password;
- adding or deleting an image moves the task’s
updatedAtforward, andimageCounttells how many it carries.
Errors
The body is always { "error": "<code>" }, sometimes with useful extra fields such as claimedBy.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_name, name_too_long, invalid_priority, invalid_externalRef… | invalid body |
| 400 | empty_update | PATCH without any field |
| 400 | invalid_updatedSince, invalid_limit, invalid_scope | invalid parameter |
| 400 | not_allowed_for_group_key | sharing requested with a group key |
| 400 | invalid_additionalGroupIds, too_many_shared_groups | invalid list of groups; more than 10 groups |
| 400 | not_shared, invalid_share | claiming a task that is not shared; inconsistent sharing |
| 400 | invalid_categoryId, categoryId_required, sharedGroupIds_required | malformed category; restricted key: specify the task’s category or group |
| 400 | invalid_data, invalid_imageId | missing image or invalid base64; image identifier |
| 401 | invalid_api_key | missing, unknown or revoked key |
| 403 | not_allowed_for_group_key | action reserved for personal keys |
| 403 | not_created_by_integration | group key: editing a task created by a member |
| 403 | out_of_key_scope | restricted key: category or group outside its own, sharing with a person, re-sharing |
| 403 | owner_only, not_claimer | action reserved for the owner, or for the claimer |
| 403 | not_image_author | deleting an image you did not add, on a task that is not yours |
| 404 | not_found | task does not exist or is out of the key’s reach — deliberately indistinguishable |
| 409 | already_claimed | claimed by someone else (with claimedBy) |
| 409 | already_done, not_claimed, use_claim_flow | already completed; completing without claiming; ticking a shared task |
| 409 | too_many_images | 10 images at most per task |
| 413 | image_too_large | image over 5 MB |
| 415 | unsupported_image_type | neither PNG, JPEG, WebP nor GIF |
| 429 | too_many_requests | rate limit reached |
| 500 | internal_error | server error: try again later |
Rate limits
- 120 requests per minute per key. Beyond that:
429. TheRateLimit-*headers show the remaining quota. - 30 authentication failures per quarter of an hour per IP address, after which the address is blocked until the end of that quarter. A properly configured integration never comes close.
On a 429 or a 5xx error, wait before retrying, doubling the wait after each failure.
Security
- Keep the key server-side, in an environment variable or a secret store. Never in versioned code, a mobile app or a web page: the API deliberately sends no CORS headers, so a browser cannot call it.
- One key per piece of software, with an explicit name: you can revoke one without cutting the others, and
createdViatells where each task comes from. - Have an exposed key revoked right away: it takes effect immediately.
- Tasks are not encrypted. Never put a secret in them — an alarm code, a password: that is what the Cryptauk vault is for, and this API does not open it.
Full example (Node.js)
Job-management software that publishes each job as a group task, then reads back who is on it and what is done.
const BASE = 'https://cryptauk.com/api/ext/v1';
const KEY = process.env.CRYPTAUK_API_KEY; // group key: never in your code
async function cryptauk(method, path, body) {
const r = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
if (r.status === 204) return null;
const json = await r.json();
if (!r.ok) throw Object.assign(new Error(json.error), { status: r.status, body: json });
return json;
}
// A job created in your software becomes a group task.
// Replaying this call never creates a duplicate, thanks to externalRef.
function publish(job) {
return cryptauk('POST', '/tasks', {
name: `${job.type} — ${job.site}`,
description: job.instructions,
priority: job.urgent ? 3 : 1,
externalRef: `mysoftware:job:${job.id}`,
});
}
// Periodically: who is on it, what is done?
let since = null; // keep it across restarts
async function sync() {
for (;;) {
const q = since ? `?updatedSince=${encodeURIComponent(since)}&limit=500` : '?limit=500';
const tasks = await cryptauk('GET', `/tasks${q}`);
for (const t of tasks) {
if (!t.externalRef?.startsWith('mysoftware:job:')) continue; // task created by hand
await updateJob(t.externalRef.split(':')[2], {
takenBy: t.claimedBy?.name ?? null,
done: t.isDone,
});
}
if (tasks.length) since = tasks[tasks.length - 1].updatedAt;
if (tasks.length < 500) break; // otherwise more are waiting: go on
}
}
Versions
/api/ext/v1 is a stable contract. It may gain fields, routes, optional parameters and error codes. Never: a field removed or renamed, a type changed, a meaning altered.
An incompatible change would come as /api/ext/v2, with v1 still served while you migrate.