API reference
Every endpoint of the Ultrathink REST API. Start with API: getting started for keys, limits and errors.
All paths are relative to https://us-central1-ultrathink-50f7f.cloudfunctions.net/api/v1, and every request needs Authorization: Bearer YOUR_DEVICE_KEY. Replies wrap their result in data, as described on the getting started page.
| Endpoint | What it does | Key needed |
|---|---|---|
GET /entries | List entries, with filters and paging | Read only |
GET /entries/{id} | Read one entry | Read only |
POST /entries | Create an entry | Read and write |
PATCH /entries/{id} | Change an entry | Read and write |
DELETE /entries/{id} | Delete an entry | Read and write |
GET /entries/{id}/related | An entry’s similar and linked entries | Read only |
GET /topics | Your topics, with entry counts | Read only |
GET /people | The people in your entries, with entry counts | Read only |
POST /search | Ask a question, get an answer and the entries it used | Read only |
POST /upload | Upload a file and get its address | Read and write |
The entry object
Every endpoint that returns entries returns them in this shape.
| Field | Type | What it is |
|---|---|---|
id | string | The entry’s id |
title | string | The title |
type | string | The entry type (see Entry types) |
notes | string or null | Your notes |
content | string or null | Captured text, such as a page or a conversation |
url | string or null | The source address |
topics | array | Every topic, the ones a person set first |
userTopics | array | Topics you or another person set. The AI never changes these |
aiTopics | array | Topics the AI added |
people | array | People the entry is about |
visibility | string | private, or org when shared with your organisation |
owned | boolean | false for an entry another member shared with your organisation: you can read it, but only its owner can change or delete it |
entity | string or null | project, task, knowledge or unclassified |
taskStatus | string or null | A status id from the owner’s status workflows |
statusDone | boolean or null | Whether that status counts as done. null with no status |
dueDate | string or null | YYYY-MM-DD |
dueTime | string or null | HH:MM, 24 hours, in your own local time |
waitingOn | string or null | "me", or one person’s name |
webLinks | array | Web pages kept on the entry, each { "label", "url" } |
category | string or null | work or personal |
summary | string or null | The AI summary |
keyPoints, actionItems, suggestedQuestions | array or null | What the AI picked out |
transcript | string or null | The transcript, for audio entries |
speakers | array or null | The transcript split by speaker, for audio entries |
source | string or null | Where the entry came from, for example api |
externalId | string or null | Your own id for the entry, if you gave one |
createdAt, updatedAt | string | ISO 8601 times |
GET /entries
Lists your own entries, newest first unless you sort otherwise.
| Parameter | Default | What it does |
|---|---|---|
limit | 20 | Entries per page, up to 100 |
cursor | The previous page’s pagination.nextCursor. Continues from there, and page is ignored | |
page | 1 | Page number. Can skip at most 2,000 entries; past that, use cursor |
type | One entry type | |
entity | project, task, knowledge or unclassified | |
topics | Topic names separated by commas. Matches entries with any of them, up to 30 | |
person | One person’s exact name, as GET /people lists it. Not with topics | |
waitingOn | me, or one person’s exact name | |
taskStatus | A status id from your workflows (for example in-progress), or @open or @done for every status that does not, or does, count as done. An unknown status returns a 400 listing yours | |
done | true or false: whether the status counts as done. Entries with no status are left out. Not with taskStatus | |
externalId | The entry with this externalId, exact match | |
dueAfter | Due on or after this date, YYYY-MM-DD | |
dueBefore | Due on or before this date, YYYY-MM-DD | |
updatedSince | Changed since this date (YYYY-MM-DD) or ISO 8601 time. Always sorted by updatedAt. At most 2,000 entries can match | |
sort | createdAt | createdAt, updatedAt or due_date |
order | desc | asc or desc |
The reply’s data has items (entry objects) and pagination with page, limit, total, totalPages, hasMore and nextCursor (null on the last page).
Filters that work together
- No filters, with any
sortandorder. - Any one or two filters with the default sort (
createdAt, newest first). topicsand a due date range, withtypeorentityas well.topicswithsort=createdAtorsort=updatedAt, in either order.- A due date range with any
sort. More than 2,000 entries due in the range returns a400. entitywithsort=due_date&order=asc, for example tasks by due date.taskStatusordoneon their own withsort=createdAt, withsort=due_date&order=asc, or with a due date range.taskStatusordonewithtopicsorentity, using the default sort,sort=due_date&order=ascor a due date range.externalIdwith the default sort.personon its own or withentity, in either order.waitingOnon its own or withentity, also withsort=due_date&order=asc.updatedSinceon its own, or with one ofentity,type,topicsorperson. Not with a due date range, or asortother thanupdatedAt.
Other combinations may return a 400 saying the combination is not supported. Use fewer filters, or the default sort.
curl -H "Authorization: Bearer YOUR_DEVICE_KEY" \
"https://us-central1-ultrathink-50f7f.cloudfunctions.net/api/v1/entries?waitingOn=me&entity=task&sort=due_date&order=asc"
GET /entries/{id}
Reads one entry: one of yours, or one another member shared with your organisation (owned is then false). Every id that search returns can be read here. An entry you cannot see returns 404, the same as one that does not exist.
POST /entries
Creates an entry. Send JSON. Only title is required.
| Field | What it is |
|---|---|
title | Required. Up to 500 characters |
type | An entry type. Default note |
notes | Your notes, up to 50,000 characters |
content | Captured text, up to 50,000 characters |
url | The source address, up to 2,000 characters |
entity | project, task, knowledge or unclassified. Default knowledge. An entity you set is kept when the AI runs |
topics | Topic names. Kept first and in your order; the AI adds up to three of its own after them, reusing your topics where it can |
aiTopics | false stops the AI adding topics to this entry |
people | People’s names |
category | work or personal |
visibility | private (default) or org to share with your organisation |
dueDate | A real date, YYYY-MM-DD. Kept when the AI runs |
dueTime | HH:MM, 24 hours, your local time. Needs dueDate |
waitingOn | "me", or one person’s name (up to 200 characters). A new name joins your people list |
webLinks | Up to 20 links, each { "label": "Live post", "url": "https://..." }. The label is optional, up to 100 characters; the address must be http or https |
taskStatus | A status by id (in-progress) or name (In progress, any case), or @done or @open for the first status of the entry’s workflow that does, or does not, count as done. Optional |
externalId | Your own id, up to 500 characters, so a later sync can find the entry |
triggerAI | false skips the AI pass. Default true |
Rules:
- Only work entries can be shared.
"visibility": "org"with"category": "personal"returns a400. An entry with no category yet can be shared. - Sharing needs you to be a member of an organisation now, otherwise
400. - A status your workflows do not have returns a
400listing the ones you can use.
The reply is 201 with the new entry. The AI then writes its title, summary, topics and people, as for anything you save in the app.
curl -X POST \
-H "Authorization: Bearer YOUR_DEVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Send Ana the report", "entity": "task", "dueDate": "2026-10-20", "dueTime": "14:30", "waitingOn": "me"}' \
"https://us-central1-ultrathink-50f7f.cloudfunctions.net/api/v1/entries"
PATCH /entries/{id}
Changes one of your own entries. Send only the fields you want to change; they are the same as for POST /entries.
taskStatus: nullclears the status. Changing the status never moves the entry to another kanban column: columns are independent of status.topicsreplaces the whole list. AI topics you keep stay AI topics, and the AI does not add back the ones you leave out.aiTopics: falsetakes the AI’s topics off and stops it adding more.truelets it add them again.dueTime,waitingOnandwebLinksare cleared withnull. ClearingdueDateclears its time too. AdueTimewith no due date returns a400.webLinksreplaces the whole list.- Moving a shared entry out of work (for example
"category": "personal") also makes it private. triggerAI: trueruns the AI pass again. Without it, the AI does not run.
Entries shared with you by someone else cannot be changed, and return 404.
DELETE /entries/{id}
Deletes one of your own entries. It cannot be undone.
curl -X DELETE \
-H "Authorization: Bearer YOUR_DEVICE_KEY" \
"https://us-central1-ultrathink-50f7f.cloudfunctions.net/api/v1/entries/ENTRY_ID"
The reply’s data is { "id": "ENTRY_ID", "deleted": true }. An entry that is not yours returns 404.
GET /entries/{id}/related
An entry’s similar entries, as the AI picked them when the entry was saved, and the entries you linked to it. Only entries you can read are listed.
{
"entryId": "abc123",
"mode": "classic",
"similar": [
{
"id": "def456",
"title": "Partner deals tracker",
"type": "note",
"entity": "task",
"timestamp": "2026-08-01T10:00:00.000Z",
"excerpt": "Deals by partner, weekly.",
"score": null,
"reason": "Both track partner deals",
"duplicate": false
}
],
"links": [
{
"id": "ghi789",
"title": "Call with Ana",
"type": "note",
"entity": "knowledge",
"timestamp": "2026-07-01T10:00:00.000Z",
"excerpt": "Ana wants the report in Teams.",
"linkType": "related"
}
]
}
reason says why the AI thought the entries are alike. excerpt is the first words of the entry as plain text.
GET /topics and GET /people
Your topics, or the people in your entries, each with how many entries have it, most used first.
| Parameter | Default | What it does |
|---|---|---|
q | Only names containing this text, any case (up to 200 characters) | |
limit | 100 | How many names to return, up to 500 |
{
"items": [
{ "name": "planning", "count": 12 },
{ "name": "meetings", "count": 9 }
],
"total": 48,
"entriesCounted": 1311,
"partial": false
}
total is how many names match before limit. partial is true only when you have more than 10,000 entries, and then the newest 10,000 are counted. Counts can lag a few minutes behind changes made in the app.
POST /search
Asks a question in plain words. The AI reads your most recent entries, up to the Entry limit in Settings, then AI search (500 unless you change it), and answers with the entries it used. Entries shared with your organisation are read too.
| Field | What it is |
|---|---|
query | Required. The question, up to 1,000 characters |
entityFilter | Only search project, task or knowledge entries |
limit | How many recent entries to read. It can only lower your entry limit, never raise it |
{
"answer": "You agreed to move the pricing page launch to November.",
"mode": "classic",
"matches": [
{
"id": "abc123",
"title": "Pricing page review",
"type": "note",
"entity": "project",
"timestamp": "2026-10-02T09:15:00.000Z",
"source": "user",
"score": null,
"cited": true,
"excerpt": "Launch moves to November so the copy can be checked..."
}
],
"entryCount": 500,
"durationMs": 2100
}
| Field | What it is |
|---|---|
answer | A short answer from your entries |
matches | The entries the answer used. Each id can be read with GET /entries/{id} |
source | user for your own entries, org for ones shared by your organisation |
entryCount | How many entries were read |
durationMs | How long the answer took |
Search only sees the entries inside your entry limit. To find something older, filter with GET /entries instead.
POST /upload
Uploads a file and returns its address. It does not create an entry by itself.
Send it as multipart/form-data with the file in a field called file, and give the file’s type, as the API checks it:
curl -X POST \
-H "Authorization: Bearer YOUR_DEVICE_KEY" \
-F "file=@meeting.m4a;type=audio/mp4" \
"https://us-central1-ultrathink-50f7f.cloudfunctions.net/api/v1/upload"
Or as JSON, with the file as a data URL:
{ "file": "data:application/pdf;base64,JVBERi0...", "filename": "report.pdf" }
| File | Types | Largest size |
|---|---|---|
| Documents | PDF, Word (doc, docx), Excel (xls, xlsx), PowerPoint (ppt, pptx), plain text, Markdown | 50 MB |
| Images | PNG, JPEG, WebP, GIF, HEIC | 50 MB |
| Audio | MP3, WAV, M4A, OGG, WebM | 200 MB |
| Video | MP4, MOV, WebM | 200 MB |
A request over about 32 MB fails before it reaches Ultrathink, whatever the file. Upload large recordings and videos from the web app or the desktop app instead.
The reply is 201:
{
"url": "https://firebasestorage.googleapis.com/v0/b/...",
"path": "user-uploads/USER_ID/audio/1760440000000-ab12cd34.m4a",
"mimeType": "audio/mp4",
"size": 4823011,
"category": "audio"
}
Entry types
These are the values type takes.
| Type | What it is |
|---|---|
link | A web page |
note, snippet, para | Short text |
idea, long-note | Longer notes and research |
pdf, markdown | Documents |
ms-word, ms-excel, ms-powerpoint, ms-onenote | Microsoft Office files and pages |
screenshot, image | Pictures |
audio, video | Recordings and video links |
chatgpt, claude, perplexity | AI conversations |
outlook | An Outlook email |
notion | A Notion page |
profile | A LinkedIn, X or GitHub profile |
file | Any other file |
OpenAPI file
The same API is described in an OpenAPI file, which you can import into Postman, Insomnia or a ChatGPT custom GPT:
https://tryultrathink.com/docs/api/openapi.yaml