CryptaukPassGuard

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 keyPersonal key
Used tofeed a group with tasks from softwareact on one person's tasks, on their behalf
Created fora group you are the creator or an admin ofyour own account
Creates tasksshared with the group — and also with other groups its creator administersowned by you; you can share them
Readsall tasks shared with the groupyour 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, deletestasks created by a key of this grouptasks you own
Claims, completesno: that is a human actyes, 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.

Request a key

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.

Request
curl https://cryptauk.com/api/ext/v1/me \
  -H "Authorization: Bearer $CRYPTAUK_API_KEY"
Response
{
  "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

FieldMeaning
idCryptauk identifier. A large integer: it fits in a JavaScript Number, but store it as 64 bits or as text.
name, description, priorityThe 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.
isDoneCompleted or not.
externalRefYour reference, given at creation (see “Avoiding duplicates”). null if none.
createdViaName of the API key that created the task; null if it was created in a Cryptauk app.
ownerThe owner: only they can edit the content.
sharedGroupsAll the groups of the task, [{ id, name }], main one first: a task can have several.
sharedGroupThe main group alone, kept for compatibility.
sharedUserThe person the task is shared with directly — exclusive of groups.
imageCountNumber of attached images (see “Attached images”).
claimedBy, claimedAtWho said “I am on it”, and when. null: nobody.
doneBy, doneAtWho completed it, and when.
createdAt, updatedAtISO 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.

RouteKeyEffect
GET /meanydescribes the key
GET /tasksanylists tasks (filters: see “Synchronizing”)
GET /tasks/:idanyone task
POST /tasksanycreates a task: 201, or 200 if the externalRef already existed
PATCH /tasks/:idanyedits name, description, priority — and isDone with a personal key, on a task that is not shared
DELETE /tasks/:idanydeletes: 204
POST /tasks/:id/claimpersonal“I am on it”
POST /tasks/:id/releasepersonal“I am dropping it”, or unblocking a task (owner)
POST /tasks/:id/donepersonal“it is done” — only by whoever claimed it
POST /tasks/:id/reopenpersonalreopens a completed task (owner)
PUT /tasks/:id/sharepersonalshares with sharedGroupId or sharedUserId, or {} to stop (owner)
GET /groupspersonalyour groups: [{ id, name }]
GET /contactspersonalthe people you can share with directly
Creating a task
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, or 404 if 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.
Example: POST /tasks body, with the team key
{
  "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_scope outside its scope. When it must be specified, 400 categoryId_required or sharedGroupIds_required;
  • it does not re-share and never shares with a single person; GET /groups only 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:

ParameterEffect
done=0, done=1only 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|allpersonal key: your tasks, shared ones, or everything (default)

Results are sorted by ascending updatedAt. To follow progress without reading everything again:

  1. a first call, without updatedSince;
  2. keep the most recent updatedAt you received;
  3. next call with updatedSince set to that value;
  4. 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.
If someone claimed it before you: 409
{
  "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.

RouteEffect
GET /tasks/:id/imagesthe list, without the content
POST /tasks/:id/imagesadds { "data": "<base64>", "name": "optional" }
GET /tasks/:id/images/:imageIdthe raw content, with its Content-Type
DELETE /tasks/:id/images/:imageIddeletes: 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_type otherwise);
  • 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 updatedAt forward, and imageCount tells how many it carries.

Errors

The body is always { "error": "<code>" }, sometimes with useful extra fields such as claimedBy.

HTTPCodeWhen
400invalid_name, name_too_long, invalid_priority, invalid_externalRef…invalid body
400empty_updatePATCH without any field
400invalid_updatedSince, invalid_limit, invalid_scopeinvalid parameter
400not_allowed_for_group_keysharing requested with a group key
400invalid_additionalGroupIds, too_many_shared_groupsinvalid list of groups; more than 10 groups
400not_shared, invalid_shareclaiming a task that is not shared; inconsistent sharing
400invalid_categoryId, categoryId_required, sharedGroupIds_requiredmalformed category; restricted key: specify the task’s category or group
400invalid_data, invalid_imageIdmissing image or invalid base64; image identifier
401invalid_api_keymissing, unknown or revoked key
403not_allowed_for_group_keyaction reserved for personal keys
403not_created_by_integrationgroup key: editing a task created by a member
403out_of_key_scoperestricted key: category or group outside its own, sharing with a person, re-sharing
403owner_only, not_claimeraction reserved for the owner, or for the claimer
403not_image_authordeleting an image you did not add, on a task that is not yours
404not_foundtask does not exist or is out of the key’s reach — deliberately indistinguishable
409already_claimedclaimed by someone else (with claimedBy)
409already_done, not_claimed, use_claim_flowalready completed; completing without claiming; ticking a shared task
409too_many_images10 images at most per task
413image_too_largeimage over 5 MB
415unsupported_image_typeneither PNG, JPEG, WebP nor GIF
429too_many_requestsrate limit reached
500internal_errorserver error: try again later

Rate limits

  • 120 requests per minute per key. Beyond that: 429. The RateLimit-* 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 createdVia tells 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.