AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
This is the official TypeScript SDK for the Todoist API.
Developer toolsTypeScript · JavaScriptGitHub @DoistOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
A TypeScript wrapper for the Todoist REST API.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
The official TypeScript SDK for the Todoist REST API.
Requires Node 20.18.1+.
An example of initializing the API client and fetching a user's tasks:
For more detailed reference documentation, have a look at the Todoist API v1 Documentation.
If you're migrating from an older version of the Todoist API (v9), please refer to the official migration guide for detailed information about the changes and breaking updates.Read the full README ↗The official TypeScript SDK for the Todoist REST API.
Requires Node 20.18.1+.
npm install @doist/todoist-sdk
An example of initializing the API client and fetching a user's tasks:
import { TodoistApi } from '@doist/todoist-sdk'
const api = new TodoistApi('YOURTOKEN')
api.getTasks()
.then((tasks) => console.log(tasks))
.catch((error) => console.log(error))
For more detailed reference documentation, have a look at the Todoist API v1 Documentation.
If you're migrating from an older version of the Todoist API (v9), please refer to the official migration guide for detailed information about the changes and breaking updates.
Key changes in v1 include:
Pass headers to send extra headers with every API request, for example to tell the backend which client is calling:
const api = new TodoistApi('YOURTOKEN', {
headers: { 'Doist-Platform': 'cli' },
})
For values that change between requests, pass a function instead. The SDK calls it once per request, and a retried request reuses its result:
import { randomUUID } from 'node:crypto'
const api = new TodoistApi('YOURTOKEN', {
headers: () => ({ 'Doist-Platform': 'cli', 'Request-Id': randomUUID() }),
})
These headers cannot replace Authorization, X-Request-Id or the multipart Content-Type; the SDK's own values always win. They are not sent on attachment downloads (viewAttachment), which redirect to a third-party CDN. The OAuth helpers (getAuthToken, refreshAuthToken, revokeToken, …) take their own options and do not send them.
The Todoist API client supports custom HTTP implementations to enable usage in environments with specific networking requirements, such as:
import { TodoistApi } from '@doist/todoist-sdk'
// Using the new options-based constructor
const api = new TodoistApi('YOURTOKEN', {
baseUrl: 'https://custom-api.example.com', // optional
customFetch: myCustomFetch, // your custom fetch implementation
})
// Legacy constructor (deprecated but supported)
const apiLegacy = new TodoistApi('YOURTOKEN', 'https://custom-api.example.com')
Your custom fetch function must implement this interface:
type CustomFetch = (
url: string,
options?: RequestInit & { timeout?: number },
) => Promise<CustomFetchResponse>
type CustomFetchResponse = {
ok: boolean
status: number
statusText: string
headers: Record<string, string>
text(): Promise<string>
json(): Promise<unknown>
}
OAuth authentication functions (getAuthToken, revokeAuthToken, revokeToken) support custom fetch through an options object:
// New options-based usage
const { accessToken } = await getAuthToken(args, {
baseUrl: 'https://custom-auth.example.com',
customFetch: myCustomFetch,
})
await revokeToken(args, {
customFetch: myCustomFetch,
})
// Legacy usage (deprecated)
const { accessToken } = await getAuthToken(args, baseUrl)
When the authorization server issues a refreshToken, use refreshAuthToken
to obtain a new access token once the current one expires, without sending the
user back through the authorization flow:
const refreshed = await refreshAuthToken({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
refreshToken: storedRefreshToken,
})
// The server may rotate the refresh token — persist refreshed.refreshToken
// when present, otherwise keep using the previous one.
Note: Refresh tokens are only issued to OAuth apps that have them enabled (the default for new apps). Apps without them receive a long-lived access token, so
getAuthTokenreturns norefreshTokenand there is nothing to refresh. For public clients (tokenEndpointAuthMethod: 'none'), omitclientSecretwhen callingrefreshAuthToken.
On Node, the SDK's default transport reads the proxy environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) and decodes compressed response bodies. Supplying a customFetch replaces that transport, so a custom implementation that still wants both can borrow it with getDefaultTransport:
import { type CustomFetch, getDefaultTransport, TodoistApi } from '@doist/todoist-sdk'
const customFetch: CustomFetch = async (url, options) => {
const transport = await getDefaultTransport()
// undici's `fetch` and the global one are the same call at runtime but
// carry different types, so settle on one signature.
const fetchImpl = (transport?.fetch ?? fetch) as typeof fetch
const response = await fetchImpl(url, {
...options,
headers: {
...(options?.headers as Record<string, string> | undefined),
'x-my-header': 'value',
},
// `dispatcher` is a Node fetch option that the DOM types don't declare.
dispatcher: transport?.dispatcher,
} as RequestInit)
// `CustomFetchResponse` wants plain-object headers, not a `Headers`.
const headers: Record<string, string> = {}
response.headers.forEach((value, key) => {
headers[key] = value
})
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers,
text: () => response.text(),
json: () => response.json(),
arrayBuffer: () => response.arrayBuffer(),
}
}
const api = new TodoistApi('YOURTOKEN', { customFetch })
getDefaultTransport returns the dispatcher and the fetch it must be used with as a single value, and both halves have to be used together. The dispatcher decompresses the response body itself, so pairing it with a fetch from a different undici build — including the runtime's global fetch on Node 26 — decodes the body twice and the request fails mid-stream with terminated. A fetch of undefined means the global fetch is the correct partner; the whole value is undefined outside Node, where no dispatcher applies.
Blob, or a ReadableStream when uploading from a stream — a custom fetch must pass them through unchangedUse escapeFilterToken for user-provided search text, handling validation and trimming in your code. Pass complete filter queries directly to getTasksByFilter.
await api.getTasksByFilter({
query: `search: ${escapeFilterToken(searchText)}`,
})
Instead of having an example app in the repository to assist development and testing, we have included ts-node as a dev dependency. This allows us to have a scratch file locally that can import and utilize the API while developing or reviewing pull requests without having to manage a separate app project.
npm installscratch.ts in the src folder.ts-node (instructions for VSCode, WebStorm), or you can optionally run ts-node in a terminal using instructions here (npx ts-node ./src/scratch.ts should be enough).Example scratch.ts file:
/* eslint-disable no-console */
import { TodoistApi } from './todoist-api'
const token = 'YOURTOKEN'
const api = new TodoistApi(token)
api.getProjects()
.then((projects) => {
console.log(projects)
})
.catch((error) => console.error(error))
For live API verification, you can run raw requests with a local token:
.env.example to .env.TODOIST_API_TOKEN in .env.npm run api:request -- ....TODOIST_API_BASE_URL in .env (defaults to https://api.todoist.com).Examples:
npm run api:request -- --path /api/v1/tasks
npm run api:request -- --method POST --path /api/v1/tasks --body '{"content":"API smoke test"}'
npm run api:request -- --method POST --path /api/v1/tasks/123 --body '{"due_string":"no date"}'
npm run api:request -- --path /api/v1/tasks --query '{"project_id":"123","limit":10}'
To see all options:
npm run api:request -- --help
This project uses Release Please to automate releases. Releases are created automatically based on Conventional Commits.
When making changes, use conventional commit messages:
feat: - New features (triggers a minor version bump)fix: - Bug fixes (triggers a patch version bump)feat!: or BREAKING CHANGE: - Breaking changes (triggers a major version bump)chore:, docs:, refactor:, perf: - Other changes (included in changelog)Example:
feat: add support for recurring tasks
fix: resolve issue with date parsing
feat!: remove deprecated getTask method
The release process is fully automated:
Automatic PR Creation: When commits are merged to main, Release Please automatically creates or updates a release PR with:
package.jsonCHANGELOG.mdReview and Merge: Review the release PR to ensure the version bump and changelog are correct, then merge it.
Automatic Release: Upon merging the release PR:
publish.yml workflow is triggered by the tagUsers of the API client can then update to the new version in their package.json.
Any feedback, such as bugs, questions, comments, etc. can be reported as Issues in this repository, and will be handled by us in Todoist.
We would also love contributions in the form of Pull requests in this repository.