Intend API v0.9

This page offers endpoints that you can hit in order to do something, or get data.

You may also be interested in:

  • MCP Server, to let AI assistants like Claude interact with your intentions, timer, and goals
  • Webhooks, to get automatically notified when:
    • the timer state changes
    • your "next action" changes (the Do tab)
    • changes to your intentions match specific filters (like the beeminder integration)
  • Custom intention sources, which lets you build an integration like the workflowy one by providing us with:
    • URL to get draft intentions from
    • URL to send updates of "this item has been completed"

🏖️ Quick start (no account needed)

Create a sandbox user to start building and testing immediately—no sign-up, no auth token needed:

curl -X POST 'https://intend.do/api/v0/sandbox_user' \
  -H 'Content-Type: application/json' \
  -d '{"preset": "goals_intentions"}'

Returns an auth_token you can use right away. Sandbox users are temporary accounts that expire after 1 week—safe for testing irreversible actions like setting intentions. Full details below.

If you're just testing, or building something for yourself... use your personal auth token: (it'll show up here if you're logged in)

Append it to your request like this:
?auth_token=INTEND_AUTH_TOKEN

If you're building an application that others will use, you'll want to set up OAuth2.

OAuth2 Instructions

Step 1: Create an Intend API client

Register your app at intend.do/apiclient/myapps. You'll get a client_id and client_secret.

We recommend storing these as environment variables or in a .env file:

INTEND_CLIENT_ID=ido_12345...
INTEND_CLIENT_SECRET=ido_sk_1234567...

Step 2: Redirect the user to authorize your app

https://intend.do/oauth/authorize?response_type=code
&client_id=CLIENT_KEY
&redirect_uri=http://example.com/REDIRECT_URI/

After the user approves, they'll be redirected to your redirect_uri with an authorization code:

http://example.com/REDIRECT_URI/?code=23rf87g2v0jij02f

Step 3: Exchange the code for an access token

curl -X POST https://intend.do/oauth/token \
  -d grant_type=authorization_code \
  -d code=CODE \
  -d client_id=CLIENT_KEY \
  -d client_secret=ido_sk_1234567... \
  -d redirect_uri=http://example.com/REDIRECT_URI/

The response includes an access_token and refresh_token. Store both—the access token expires after one year, and you can use the refresh token to get a new one.


Step 4: Make API requests

Include the access token in your requests:

curl https://intend.do/api/v0/u/me/userinfo.json \
  -H "Authorization: Bearer ACCESS_TOKEN"

That's it! See the endpoint documentation below for what you can do.

Data format:

See this javascript file for some documentation of the sorts of objects returned by the API.

Hi AI coding agents! Welcome :)

Some additional context about how intend.do works, since unlike a human developer, you've probably never used it!

The premise: Intend is a to-do list app, but different from most to-do list apps. Where most such apps have a whole system for organizing a ton of things you're never actually going to do, Intend focuses you on only intentions you're going to do today. You can *draft* intentions for tomorrow, but you can't set them. Then at the end of the day, you review the day ("outcomes"), and set new ones afresh.

The daily flow: Each day, the user sets intentions. These are stored in today.core.list. The top uncompleted item is the user's current "next action" (nexa).

Intentions format: When POSTing to /api/v0/u/me/intentions, the raw field uses a specific text format: goalcode) intention text, one per line. For example: 1) Review pull requests. The goal code is a short identifier (like 1, 2) that maps to one of the user's goals. Use GET /api/v0/u/me/goals/active.json to see the codes. Items without a goal code are silently ignored.

More on intentions format:

For an intention that goes towards multiple goals, it can be: (eg if goal 2 is 'move more' and goal 3 is 'fun with friends'.)
  2,3) go for a walk with John
A user can have maximum 10 active goals, numbered 1-9 or 0. When goals are archived/completed/inactive, their code switches to a two-letter code, eg 'CT'.
  AB) upgrade node version on inactive project
For an intention that doesn't go towards any goal, it's standard to use a "&" instead of the goal:
  &) laundry
But you can actually use almost any code there, and it'll still be goal-less (item.gids=[]):
  $) fill out budget
  ❤️‍🔥) date night

Goals: Goals are the user's high-level areas of focus (e.g. "Health & Fitness", "Ship the Project"). Each goal has a code (used in text format), an _id (used internally in gids arrays), a color, and a privacy level. Goals have start/end dates and can be archived.

Item IDs (zid): Each intention/outcome has a zid (a short random string). Use zids with endpoints like completeById, nottodayById, setNowById, deferNext, etc. Get zids from today/core.json or newtabpage.json.

Day boundaries: A "day" doesn't end at midnight — it ends when the user goes to sleep. The dayStartTime setting (default: 5am) determines when one day ends and the next begins. Hours can be >24 (e.g. 25.5 = 1:30am, still counted as the previous day).

Timer: The timer supports pomodoros ("tomato" type, typically 25min, focus=1.2) and hourglass sessions ("sand" type, flexible duration). Timer ticking is handled by the frontend — the API can start/stop timers but the actual countdown requires a browser tab open.

Dates: Dates are in YYYY-MM-DD format (called "ymd"). Timestamps are JavaScript milliseconds, not Unix seconds.

Testing your work: This API has sandbox users—temporary, disposable accounts you can create with a single unauthenticated POST. This is relatively unusual for an API (though hopefully it'll become more common!). Use them! Create a sandbox user to test everything you build—post intentions, complete them, start timers, whatever—without worrying about messing up a real account. Don't ask the human for their auth token when you can just spin up a sandbox user in one request.

API endpoints:

Get active data

Great for testing—returns the user's name and username:

  • GET /api/v0/u/me/userinfo.json

Gets the user's list of goals, including each goal's current top priority. Returns {goals: [...], updated: timestamp}:

  • GET /api/v0/u/me/goals/active.json — currently active goals only
  • GET /api/v0/u/me/goals/all.json — all goals, including archived/completed

The item below returns most of the data used to render the user's today page

  • GET /api/v0/u/me/today/full.json
    which contains these sub-objects, that you can also access separately:
    • core ... GET /api/v0/u/me/today/core.json
    • drafts ... GET /api/v0/u/me/today/drafts.json
    • recent ... GET /api/v0/u/me/today/recent.json
    • settings ... GET /api/v0/u/me/today/settings.json
    • timer ... GET /api/v0/u/me/today/timer.json

Plain text endpoints

These return text/plain responses designed for humans and LLMs to read at a glance. The format may change slightly over time — if you need something reliably parseable, use the .json endpoints instead.

  • GET /api/v0/u/me/today/core.txt — today's intentions as a readable list. Each line includes a timestamp, the item's zid, status, and text:
    List for today (2026-03-06, last updated 2026-03-06 10:30:00 AM EST)
    - 09:15:00 a8f3k2x1 [ ] 1) Review pull requests
    - 09:15:00 b7d2m4y9 [X] 2) 30 min run in the park
    - 09:15:00 c1e5n8z3 [nvm] 3) Read chapter 5
  • GET /api/v0/u/me/today/timer.txt — timer status in plain English, e.g. Timer is ticking. Mode: pomo. Ends in 14m32s, at 3:45 PM EST
  • GET /api/v0/u/me/goals/active.txt — one goal per line, e.g. Goal 1: Health & Fitness

Posting intentions/outcomes

Add new intention(s)

  • POST /api/v0/u/me/intentions   ...parameters: {
    • raw (string: Intend data in intentions format, one item per line. returns 400 if no valid intentions found)
    • ymd (string, optional: date in YYYY-MM-DD format. If omitted will use the current date based on user's dayStartTime. If in the past (ie a ymd before the one currently displayed on the today page) will just update the timeline. If tomorrow, will finish today & shift to "set intentions for tomorrow" mode. Does not play nice with dates further in the future—don't bother 😉)
    • makeCurrent (optional boolean to put the new item(s) above all other not-yet-done items)
    • makeAlmostCurrent (optional boolean to put the new item(s) immediately below the top not-yet-done item)
    • response (optional: what to return. options are)
      • "count": how many items on the list, total (default)
      • "ntp": the next action (ntp stands for "new tab page"; this is the data used by the New Tab Page extension — you may find it useful for similar applications. see documentation of ntp format below)
      • "today": the /today/full.json object
    }

Post outcomes

  • POST /api/v0/u/me/outcomes   ...parameters: {
    • raw (string: Intend data in outcomes format, one item per line. returns 400 if no valid data found)
    • ymd (string, optional: date in YYYY-MM-DD format. if omitted will use the current date based on user's dayStartTime)
    • response (optional: what to return. options are)
      • "today": the /today/full.json object
      • "count": how many completed outcomes for the day (default)
    }
💡 Ready to test? If you haven't already, create a sandbox user to try posting intentions and outcomes without affecting a real account. It takes one curl command—no auth needed.

New-tab-page extension endpoints

These endpoints were made for the New Tab Page extension and they therefore return the ntp json format:
{nexa: {/* see format here */}, colors, goalName, darkTheme, remainingcount, remainingDenominator}

Get next action

  • GET /api/v0/u/me/newtabpage.json

Returns the current next action with details, or indicates if there are no intentions set.

Complete a specific intention

  • POST /api/v0/u/me/completeById/zid

(A zid is an id for an intention or outcome—any such item. You can get the zid from newtabpage.json or today/full.json or today/core.json)

Mark a specific intention as "not today"

  • POST /api/v0/u/me/nottodayById/zid

Enter "now" mode

Both of these put the system into "now" mode (like the now page) where when you complete the current intention it shows empty space instead of whatever's next on the list. This mostly affects what the ntp data returns.
  • POST /api/v0/u/me/setNowById/zid (enter "now" mode for this intention)
  • POST /api/v0/u/me/notNow (enter "now" mode with no current intention)

Move an intention

  • POST /api/v0/u/me/moveToCurrent/zid (move item to above all other not-yet-done items)
  • POST /api/v0/u/me/deferNext/zid (move item to after the next outstanding item)
  • POST /api/v0/u/me/deferLast/zid (move item to the end of the list)

Pick a random item and set it as the next action

  • POST /api/v0/u/me/getRandomNextAction

Append to an intention

  • POST /api/v0/u/me/appendById/zid   ...parameters: {
    • text (string. text to append to the intention)
    }

Past data

Retrieve past entries/days

  • GET /api/v0/u/me/timeline/entries.json
    Optional query parameters: (add as ?)
    • limit (maximum to pull; default = 21)
    • startymd (the earliest date to include, in YYYY-MM-DD format)
    • endymd (the latest date to exclude, in YYYY-MM-DD format)
    • select (things to select separated by +. main options: intentions, outcomes, activity, goalCounts. default is all of these except activity)
    Example: GET https://intend.do/api/v0/u/me/timeline/entries.json?startymd=2018-05-01&select=outcomes

Retrieve reviews

Each frequency has a remarks.json endpoint (the user's written reflections) and where available an overview.json endpoint (aggregated outcomes by goal).

  • Weekly:
    • GET /api/v0/u/me/reviews/:year/week/:weeknum/overview.json
    • GET /api/v0/u/me/reviews/:year/week/:weeknum/remarks.json
  • Monthly: (month is 3-letter name, e.g. Jan, Feb)
    • GET /api/v0/u/me/reviews/:year/:month/overview.json
    • GET /api/v0/u/me/reviews/:year/:month/remarks.json
  • Quarterly:
    • GET /api/v0/u/me/reviews/:year/Q/:q/remarks.json (q = 1–4)
  • Yearly:
    • GET /api/v0/u/me/reviews/:year/yearly/remarks.json

Timer

Creating new work blocks ('dur', short for duration)

  • POST /api/v0/u/me/add_dur   ...parameters: {
    • ty (string, required. "type" (must be either 'tomato' or 'sand'))
    • min (number, required. "minutes" of duration. can be decimal)
    • foc (number, optional. "focusedness". 20% = 0.2, 100% = 1. pomos default to 120% = 1.2)
    • sCp (number, optional. "stamp completed". unix timestamp in millis. defaults)
    • zidToAssignTo (string, optional. assigns the dur to the intention with this zid, returns 406 error if not found. returns {intention: ...} if found)
    }

These two old endpoints both return {sparePomos: n, spareDurs: [...]}

  • POST /api/v0/u/me/addpomo
  • POST /api/v0/u/me/set_spare_pomos (takes a parameter n in the request body)

The endpoints below control the actual timer itself. IMPORTANT: Note that the timer ticking logic is handled by the frontend on Intend, so while this system can let you start or stop pomodoros with a button, the actual tallying of pomodoros won't occur unless you have your today page or timer page open somewhere! Similarly, being in continuous/automatic mode will only cause the timer to continue if a tab is open.

All of them return the timer object, which looks like

{
  ticker: {
    state: 'ticking',
    mode: 'pomo',
    endTime: 1522442719000,
    continuous: true
  },
  extras: {
    breakCount: 3,
    spareTomatos: 4,
    spareDurs: [{       // a list of duration items
      min: 25,            // length in minutes
      ty: "tomato",       // type ("tomato" or "sand")
      src: "r",           // source (t=today page timer, r=room, a=api, m=manual)
      sCp: 1619231101181, // stamp completed
      hCp: 19.42          // hour completed (hours since midnight of relevant day)
    }],
    lastPomoIncrementStamp: 1522437901064,
  }
}

Get current timer

  • GET /api/v0/u/me/today/timer/all

Start pomodoro

  • POST /api/v0/u/me/today/timer/startpomodoro
    Query parameters: duration (in minutes; if omitted, uses current setting)

Start break

  • POST /api/v0/u/me/today/timer/startbreak
    Query parameters: duration (in minutes; if omitted, uses current setting)

Start hourglass timer

  • POST /api/v0/u/me/today/timer/hourglass
    Query parameters: duration (in minutes; if omitted, uses 15 minutes)

Pause timer

  • POST /api/v0/u/me/today/timer/pause

Unpause timer

  • POST /api/v0/u/me/today/timer/unpause

Cancel timer

  • POST /api/v0/u/me/today/timer/cancel

Custom timer update

  • POST /api/v0/u/me/today/timer/ticker   ...parameters: {
    • mode (either "pomo" or "hourglass")
    • state (one of "inactive", "ticking", "breaking", "paused")
    • endTime (required if ticking or breaking: numerical unix timestamp in millis)
    • remainingSeconds (required if paused)
    • continuous (boolean; will stay the same if omitted)
    }
    Returns 400 if invalid in some way (eg pomo+paused or missing required field)

Sandbox Users

Sandbox users are temporary, API-only accounts for safely testing the API—including irreversible actions like setting intentions—without affecting your real account. They expire after 1 week and are automatically cleaned up.

They're also great for automated tests—create a sandbox user in your test setup, run your assertions against the API, and delete it in teardown. No test fixtures or mocking needed.

⚠️ Privacy note: Sandbox user profiles are publicly viewable at their URL (e.g. /~sandbox_abc123.../). In practice this is similar to a Google Docs link set to "anyone with the link"—the long random username makes it effectively unguessable, but anyone with the URL can see the profile. In practice this will cause you zero issues; I mention it only to avoid someone going "hey! you exposed my data!" after cloning their real profile.

Create a sandbox user

  • POST /api/v0/sandbox_user   No authentication required. Parameters: {
    • preset (string, optional. default: "goals". options:
      • "blank" — empty user, no goals, no intentions
      • "goals" — 4 sample goals, no intentions for today (good starting point for most testing)
      • "goals_intentions" — sample goals + 3 intentions already set for today
      • "clone" — copies your goals and today's intentions to the sandbox user (requires authentication)
    }
    Returns: { auth_token, username, preset, expiresAt }
    Use the returned auth_token to make API calls as the sandbox user.
    Limits: 10 active sandbox users per IP without auth (5 creations per hour), or 30 active per credential with auth. Returns 409 if the active limit is exceeded, 429 if the hourly rate limit is exceeded.

Try it! No account needed:

curl -X POST 'https://intend.do/api/v0/sandbox_user' \
  -H 'Content-Type: application/json' \
  -d '{"preset": "goals"}'

Then use the sandbox user's auth_token to interact with the API:

curl 'https://intend.do/api/v0/u/me/goals/active.json?auth_token=SANDBOX_AUTH_TOKEN'

The sandbox user's profile is publicly viewable at /~sandbox_username/ so you can visually confirm what the API did.

List your sandbox users

  • GET /api/v0/sandbox_user
    Returns a list of your active sandbox users with their auth tokens, usernames, presets, and expiry dates.

Delete a sandbox user

  • DELETE /api/v0/sandbox_user/auth_token
    Immediately deletes the sandbox user and all associated data. You can only delete sandbox users you created.

Beeminder

Get settings

  • GET /api/v0/u/me/apps/beeminder/settings.json

Put settings

  • PUT /api/v0/u/me/apps/beeminder/settings.json
    everything goes inside a property called "settings"
    {settings: {
    • intentionsBinary (beeminder goal slug for setting intentions at all)
    • outcomesBinary
    • outcomesPercentage
    • roomPresence
    • weeklyReviewCount
    • weeklyReviewComplete
    • monthlyReviewCount
    • monthlyReviewComplete
    • quarterlyReviewCount
    • quarterlyReviewComplete
    • arbitrary (array of "beemind arbitrary things" items)
    }}
    ...arbitrary object values: [{
    • beemgoal (string; beeminder goal slug for this arb match)
    • trackWhat (string enum: [
      • 'exists' (intended)
      • 'd' (completed)
      • 'pd' (pomodoros completed)
      • 'totalMinutes'
      • 'focusedMinutes'
      • 'number'
      • 'sum'
      ])
    • goalid (Intend goalid, 'misc' if misc-only)
    • textMatch (string; text or regex match)
    • useRegex (boolean; true if textMatch is regex)
    • isTextMatchInverse (boolean; true if counting only non-matches)
    • starred (boolean; true means match only starred items)
    • staked (boolean; true means match only items with $ at stake)
    • multiplier (number; undefined means x1)
    • paused (boolean; true means this beem arb doesn't fire)
    }]

Tell us what other endpoints you want

Use the question box on the right