init.Tasks openapi.json inittasks.com

what the server sees, and what a grant hands over

Encryption

This page is the one to read if you are deciding whether to build on this API. It explains what is genuinely private, what is not, and which of your instincts about a REST API will be wrong here.

What the server stores

Ciphertext, per field. Each encrypted value is an envelope:

text
v1.<base64url nonce>.<base64url ciphertext+tag>

AES-256-GCM, with the row's identity bound into the authenticated data — so a ciphertext cannot be lifted from one row and pasted into another.

Encrypted: titles, subtitles, notes, tag names, dates, status, priority, colours, sort keys, inbox and attachment text, filter criteria, recurrence rules, file names and file bytes.

Not encrypted, because the database needs them to function: row ids, the owning user, created/updated timestamps, and the parent-child links between rows.

So the shape of a user's data — how many projects, how often they write — is visible to the server. The content is not.

Tags are blind ids

A tag's row id is HMAC(key, normalised name). The server can tell that two to-dos share a tag; it cannot tell you what the tag is called. Names are resolved client-side from the tag table.

One consequence reaches the API: a tag cannot be renamed. The id IS the name, so renaming would change the identity and silently detach every to-do. PATCH /tags/{id} accepts a colour and refuses a name. The honest operation is create-new, delete-old, and you can see it happen.

What a grant hands over

When a user approves your app, the grant carries the encryption key, wrapped for that grant alone. The server unwraps it only while serving your request, and holds it for no longer.

This is the honest description: an app you approve can read everything in the scopes it was granted. End-to-end encryption protects the user from the server and from anyone who steals the database. It does not protect them from an app they deliberately connected — nothing could, since the app has to see plaintext to be useful.

What it does mean:

Read-only really is read-only

A grant scoped to reads gets a key that opens data and a server that refuses writes. Both halves. Attempting a write returns forbidden_scope — it does not silently no-op, and it does not half-apply.

When the user changes their key

The user can rotate their encryption key (after losing a device, say). Everything re-encrypts under a new generation, and every existing credential stops working — yours included. There is no migration path for a token: it carried the old key.

You will see grant_revoked. Send the user through approval again. Do not retry, and do not treat it as a transient failure: it will never recover on its own.

What this means for your design