Build a capture surface
How to send things to Caute from wherever you work: a script, a launcher, a menu bar app, a plugin, an automation.
The command line, @caute/cli, is the worked example.
Everything here is the public API, https://api.getcaute.com/v1. Caute's own apps use it the same way.
1. Get a credential
Pick the one that fits who is using it.
| You're building | Use | How |
|---|---|---|
| A script, cron job or automation for yourself | A personal token | Caute → Settings → Apps and tokens. Shown once. Send it as Authorization: Bearer <token>. |
| An app other people install | OAuth 2.1 with PKCE | Register the app (below), then the authorization code flow with resource set to the API. |
Ask for the least you need. A capture tool needs only items:write, and a token with just that can save
and nothing else: it can't read or search anything already saved. Add the others only for what you'll use:
| Scope | Lets it |
|---|---|
items:write |
Save things. Tags it sends arrive as suggestions (below). |
notes:write |
Write a note on what it saved, or on any card in its spaces. |
tags:write |
Apply tags directly instead of suggesting them. |
items:read |
Read cards and their text. |
search |
Search. A token that can search and also write can be steered by what it reads, so the consent screen warns about the pair. |
Every credential is also limited to the spaces chosen when it was made. Nothing it does reaches past them.
OAuth, for an app on someone's computer. Use a loopback redirect: listen once on 127.0.0.1 on any free port,
and register http://127.0.0.1/callback. A registered loopback address matches any port. Find the endpoints from
the API's own metadata, so a development server works the same way:
GET https://api.getcaute.com/.well-known/oauth-protected-resource → resource, authorization_servers
GET <authorization server>/.well-known/oauth-authorization-server → authorization_endpoint, token_endpoint
Send resource=https://api.getcaute.com with both the authorization request and the token request; a token
issued for another resource is refused. Refresh tokens rotate, and using one twice revokes the whole grant, so
never refresh in two places at once. A refresh answered 400 invalid_grant means the person disconnected you in
Settings: forget the tokens and ask them to sign in again.
To have your app pre-registered, so the consent screen can name it, write to support@axionarylabs.com. Until then, an
app can describe itself with a Client ID Metadata Document: its client_id is the HTTPS address of a JSON
document naming it and its redirect addresses.
2. Save something
POST /v1/items with exactly one of url, text or upload_id, or a file as multipart. It answers 202 at once
with the new card's id; reading, OCR and indexing happen afterwards.
POST /v1/items
Authorization: Bearer <token>
Idempotency-Key: 6f1c0f0e-3b7e-4a4f-9e61-1d1f0b8a2c57
Content-Type: application/json
{ "url": "https://example.com/article",
"source": "api",
"context": { "source_app": "my-launcher" },
"tags": ["reading/later"] }
{ "id": "0190f3a2-7c1d-7e4b-9a10-3f2e1d0c9b8a", "status": "captured", "duplicate": false, "supersedes": null }
sourceis how it arrived. Useapi. Say what you are incontext.source_app; the card shows it.contextis free-form and kept with the card (a selection, the page's title, where it was caught). Keep it small; it is limited in size (413 context_too_large).titleis optional. Nothing is required beyond the content itself.space_idchooses where it goes. Leave it out and it goes to the credential's default space, which is the person's private inbox unless they chose otherwise.GET /v1/spaceslists the spaces a credential can see, withaccess: writeon the ones it can save to.duplicate: truemeans it was already saved in that space: you get the existing card's id, and the context you sent is merged into it. Saving the same thing twice never makes two cards.external_idis for something you'll send again as it changes, such as a document someone keeps editing. The sameexternal_idwith new content makes a new version of the same card (supersedesnames the one it replaced); with the same content it's a duplicate.
Tags arrive as suggestions
Tags you send with items:write alone arrive as suggestions the person accepts or dismisses in Caute. They
don't file anything until a person confirms them. With tags:write they are applied directly. Either way, a tag
path is lowercase and /-separated (clients/acme/contracts). A path that doesn't exist yet is created: as the
person's own tag in a private space, or as the team's shared tag in a shared one.
Notes
A note isn't part of a save. Save first, then write the note on the card with notes:write:
POST /v1/notes
{ "body": "Read this before Thursday.", "annotates": { "item_id": "0190f3a2-…" } }
3. Files
- Up to 10 MiB: send it in the request as
multipart/form-data, with the file asfileand anything else (source,context,tags,space_id) as a JSON string inmetadata. - Larger: upload it first.
POST /v1/uploadswith itsbyte_sizeandsha256(hex), and optionallyfilenameandspace_id, answers with aurl, amethod(PUT) andheaders. Send the bytes there, exactly as given, with those headers, then save it with{ "upload_id": "…" }. Caute checks the size and hash before keeping it. Sending a large file inline is refused with413 use_uploads.
Don't worry about the file's type: Caute reads it from the bytes.
4. Retry safely
Networks drop answers. Send an Idempotency-Key (any unique string; a UUID is good) on every save, make a new
one for each thing the person saves, and send the same key again when you retry that save. Caute answers a
repeated key with the first answer, marked Idempotent-Replayed: true, and saves nothing new.
- The same key with a different request is refused:
422 idempotency_key_mismatch. - The same key while the first request is still being handled:
409 idempotency_in_progresswithRetry-After. Wait that long and send it again. - Retry when the connection fails, and on
429,502,503and504, waitingRetry-Afterseconds when it is given and backing off when it isn't. Don't retry other4xxanswers: they won't change. - A follow-up request (an upload, a note) needs a key of its own.
5. Handle Caute's answers
Errors are RFC 7807 problem details, with a stable code you can act on and a title (and sometimes detail)
you can show to a person:
{ "type": "about:blank", "status": 403, "code": "insufficient_scope",
"title": "This credential lacks the search scope.", "request_id": "532b81c3a4480e359c844217" }
Status and code |
What it means | What to do |
|---|---|---|
401 |
The token is wrong, expired or revoked. | Refresh once (OAuth); otherwise ask the person to sign in or make a new token. |
403 insufficient_scope |
The credential lacks a scope; the title names it. |
Say which permission it needs. |
403 space_not_writable |
It can't save to that space. | Leave out space_id, or let the person choose another. |
402 quota_exceeded |
Past the plan's storage. Nothing already saved is affected. | Show the title and detail. |
413 use_uploads |
A file too large to send inline. | Use POST /v1/uploads. |
422 invalid_request |
The request didn't match the API. errors lists each field and why. |
Fix the request. |
422 one_content_field |
More than one of url, text, upload_id and a file. |
Send one. |
429 rate_limited |
Too many requests. | Wait Retry-After seconds. |
code values are part of the API's contract, and new ones may appear: treat one you don't know by its status.
Include request_id when you report a problem.
Rate limits. Every answer carries RateLimit-Policy ("ingest";q=…;w=…: how many requests in how many
seconds) and RateLimit (r= remaining, t= seconds until it resets). Saving (with uploads) and searching
have budgets of their own. Pace a bulk import by them.
6. Be a good guest
- Save without asking anything. Caute never needs a title, a tag or a folder to save something, and neither should your tool.
- Say what happened in a line: saved, already saved, or why not.
- Send only what the person asked you to send. If your tool reads other content to work (a page, a document), say so where the person can see it.
- A token is a password. Keep it out of logs, repositories and URLs; store it where only its owner can read it.
The worked example
npx @caute/cli (Node 20 or newer):
caute login # OAuth in the browser, loopback hand-back
caute save https://example.com --tag reading/later --note "For Thursday"
echo "an idea worth keeping" | caute save -
caute save report.pdf --space "Research"
caute search kelp forests
Or with a token, for scripts: CAUTE_TOKEN=… caute save notes.md.