Critically

The API

Read and write your own work from somewhere else: a script, a tool you are building, a model you have given a key to. A key acts as you, so it reaches exactly what you reach and nothing else.

New here? What you can build with it, try it for an hour, or point an agent at it.

A key

Issue one in settings. It is shown once — we store a hash of it, so there is nothing for us to look up later, even for you. Keys need a paid plan, and a managed student account can never hold one.

shell
curl https://critically.io/api/v1/notes \  -H "Authorization: Bearer YOUR_KEY"

The header, never the query string. URLs land in access logs, referrer headers and browser history, and a credential that can travel in one eventually does — so a key sent that way is refused rather than honoured.

There is no Access-Control-Allow-Origin header, on purpose. A key belongs on a server. Allowing a browser to send one would let any page a person visits spend their key.

What a key may do

Every key is read or read and write. Writing creates notes, boards and citations; it cannot publish anything.

A key may also be scoped to one board. That is not a permission — the holder could sign in and reach more — it is a promise about blast radius: naming one board is the difference between a leaked key costing you that board and costing you everything.

Authorisation itself is not in the API. A key is exchanged for your own session and every rule the site enforces then applies unchanged, which is why a private note is absent here for exactly the reason it is absent from your dashboard.

A key can also be marked held by a model when it is issued, which is what an MCP server or an agent should use. Sources added with it wait for the author to accept them even on the author's own notes, exactly as a suggestion from another person does, and they do not clear the unsourced flag until somebody has.

It is set on the key rather than sent with the request, because a model that ought to declare itself is a model that can decline to.

Limits

LimitResearcher, TeacherBusiness
Requests a minuteCounted per key rather than per account, so this is a burst control on one client. Every answer carries X-RateLimit-Remaining.120600
Writes a dayPOST, PATCH and DELETE, across every key you hold. Resets at midnight UTC.5005,000
New pages archived a monthOnly a page nothing had captured before. Citing one the archive already holds costs nothing, whoever put it there.2502,500
Live keysRevoke one and the room is back.1010

A client that cannot see how close it is to a limit can only find it by crossing it, so the minute is in a header on every answer and the other refusals name the figure they were measured against. A free account has none of this: a key needs a paid plan, and a managed student account can never hold one at all.

What comes back

Always a JSON object with one key, data. A list adds next_cursor, null on the last page. Nothing else sits at the top level, so one field gets you either the thing or the page.

200 · GET /api/v1/notes
{  "data": [    {      "id": "9f1c0e4a-5b7d-4a21-9c3e-7b2f8d6a1c05",      "title": "The motorcade slowed before the first shot",      "body": "Three witnesses on the south kerb put the brake lights…",      "visibility": "public",      "occurred_at": "1963-11-22T18:30:00+00:00",      "citation_count": 4,      "dispute_count": 1,      "is_opinion": false,      "created_at": "2026-10-14T09:12:44.118+00:00",      "updated_at": "2026-10-20T16:03:02.551+00:00",      "url": "https://critically.io/notes/9f1c0e4a-5b7d-4a21-9c3e-7b2f8d6a1c05"    }  ],  "next_cursor": "MjAyNi0xMC0xNFQwOToxMjo0NC4xMTgrMDA6MDB8OWYxYzBlNGEtNWI3ZC00YTIxLTljM2UtN2IyZjhkNmExYzA1"}

citation_count, dispute_count and is_opinion are maintained by the database and count approved rows only. They are the signal here: there is no score, no rating and nothing to rank by approval, so a client wanting to show how well a claim is held shows the distribution.

A refusal replaces the envelope rather than filling it in. No data, one error, and the sentence is written to be shown to whoever sent the request.

403 · any endpoint
{ "error": "This key is scoped to one notebook." }

Paging

Lists return at most 100 rows and a next_cursor. Send it back as cursor to get the next page, and keep going until it comes back null.

shell
curl "https://critically.io/api/v1/notes?limit=100&cursor=$CURSOR" \  -H "Authorization: Bearer YOUR_KEY"

It is a keyset rather than an offset, because an offset is counted from the top of a list that is still being written to: a note created while you are on page two pushes a row you have already seen onto page three, so the walk both repeats and skips. This is why these lists are ordered by when a thing was created rather than when it was last touched — a key that moves while you walk past it is not a key.

The cursor is opaque. Parsing one means depending on the sort key, and then the sort key can never change.

Notes

  • GET/api/v1/notes

    Notes you can read, newest first. Abandoned notes are left out.

    limit
    1 to 100. Default 25.
    cursor
    From next_cursor. Opaque; do not parse it.
    q
    Match against the title.
  • DELETE/api/v1/notes/{id}

    Delete a note you wrote and nobody else holds.

    409 if another person's notebook holds it, with the count and a pointer to abandoning. They were never asked to lose it.

  • POST/api/v1/notes/{id}/abandon

    Take your name off a note other people are holding.

    Not a delete. `abandoned_at` is stamped and the byline goes; the note stays on every board, in search, and readable.

  • GET/api/v1/notes/{id}

    One note, with its citations and the boards you can see it on.

    Citations come with it rather than behind a second request: a claim without its sources is the thing this platform exists not to serve. One hop and no further — a citation pointing at another note gives you its address, not its contents.

  • POST/api/v1/notes

    Write a note. It is created private.

    title
    Required.
    body
    Optional.
    notebook_id
    Optional. Files the note on that board.

    If the board is one you cannot write to, nothing is created and the answer is 403. A note that exists but was not filed is a partial success, and the retry a client would reasonably make would leave two notes behind.

  • PATCH/api/v1/notes/{id}

    Revise a note you wrote.

    title
    Optional.
    body
    Optional. Send null to clear it.
    occurred_at
    Optional. When the thing happened.

    Only the fields you send are changed, so correcting a title does not blank a body. Somebody else's note answers 404 — an update that matches no row is not an error in Postgres, so the route checks rather than reporting a cheerful no-op.

Notebooks

  • GET/api/v1/notebooks

    Boards you can read, newest first.

    limit
    1 to 100. Default 25.
    cursor
    From next_cursor. Opaque; do not parse it.
  • GET/api/v1/notebooks/{id}

    One board, its sections, and the notes pinned to it.

    A note with no section_id is in the Loose notes column, which is not a section — it is where a deleted section's notes land.

  • DELETE/api/v1/notebooks/{id}/notes/{noteId}

    Take a note off a board. The note itself is untouched.

    If it was the last board holding it, the note is in Unfiled notes. Removing a pin never un-publishes.

  • POST/api/v1/notebooks

    Make a board. It is created private.

    title
    Required.
    description
    Optional.

Citations

  • GET/api/v1/citations

    What a note rests on, with each source described.

    note_id
    Required.
    limit
    1 to 100. Default 100.
  • DELETE/api/v1/citations/{id}

    Remove a citation. The archived page is untouched.

    The note's author and whoever added it may both do this — somebody who offered a source can take the offer back.

  • POST/api/v1/citations

    Point a claim at a page, another note, or a board.

    note_id
    Required. The note making the claim.
    url | target_note_id | target_notebook_id
    Exactly one.
    stance
    supports, refutes or contextualizes. Default supports.
    excerpt
    The passage being cited.
    why
    Up to 500 characters.
    title
    What to call this source here.
    t_start
    Seconds into a recording.
    occurred_at
    When the cited thing happened.
    lat, lng
    Where it happened.

    A url pointing at one of our own objects becomes a pointer rather than a captured page, because archiving a link to a note would freeze something the model deliberately keeps live. Anything else is sent to the capture worker, which owns canonicalisation and dedupe.

Search

  • GET/api/v1/search

    Notes and boards, through the same index the site uses.

    q
    Required, at least two characters.
    sort
    cited or recent. Default cited.
    limit
    1 to 100. Default 25.

    Ranking is a control rather than an opaque default, so the order you asked for is named in the answer.

When it refuses

400
The request is wrong, and the message says how.
401
No key, a forged key, a revoked key, or a key whose plan has lapsed. All the same answer, because telling them apart tells an attacker which half of the guess was right.
403
A real key that may not do this — a read key writing, or a scoped key reaching past its board.
404
Not there, or not yours to read. One answer for both, so trying addresses cannot map what somebody keeps private.
409
The write could not be completed, and nothing was left behind.
429
Past your rate limit for this minute.

What it will not do

  • Publish anything

    Notes and boards are created private and visibility is not read from the request. Publishing is one-way, it deletes a private board's comments, and it is warned about on screen before it happens. It stays something a person does.

  • Hand over the archive

    A citation carries the address, the publisher, whether the page has changed since, and archive_url — a page on this site. The capture itself is read here and never downloaded.

  • Search the archive

    Search covers notes and boards. Sweeping captured page text across the whole corpus is the blanket archive this platform does not offer; the way to an archive is the note that cites it.

  • Delete across other people's boards

    A note only you hold can be deleted. One sitting on somebody else's board answers 409 with a count of the boards holding it, and names abandoning as the way through — that takes your name off and leaves the note where people put it.

  • Decide approval

    A citation on your own note lands approved; on anybody else's it waits for the author. A database trigger decides that, so sending approved_at changes nothing.

Versioning

The surface is /api/v1. Anything that would break a working client goes behind a new version rather than changing this one. New fields can appear in a response, so read the fields you want rather than assuming the shape.