Tweek API Reference
Read and write tasks and calendars over a simple REST API — the same data you see in Tweek. Automate your own account with a personal key, or build an integration other people connect to with OAuth 2.0.

Overview
The base URL is https://tweek.so/api/v1. Every request goes over HTTPS and carries credentials.
Two ways to authenticate, depending on whose data you're working with:
- Personal API key — for your own account: a script, an automation, or another app you use yourself. Generate a key in Tweek and send it as
X-API-Key. - Connect with Tweek (OAuth 2.0) — for an app other Tweek users sign into. Each user approves scopes on a consent screen, and you get access tokens.
Quickstart
From nothing to a created task in two calls. You'll need credentials first — the fastest is a personal API key for your own account, or Connect with Tweek for a multi-user app. The examples below use a personal key; with an OAuth token, send Authorization: Bearer ACCESS_TOKEN instead.
1. List the user's calendars. This also confirms your key works. Every task lives in a calendar, so you need a calendar id first.
curl https://tweek.so/api/v1/calendars \
-H 'X-API-Key: YOUR_KEY'
2. Create a task in one of those calendars, on a date.
curl -X POST https://tweek.so/api/v1/tasks \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{ "calendarId": "CALENDAR_ID", "text": "Buy milk", "done": false, "date": "2026-07-10" }'
You get back { "id": "..." }. That's a working integration — the rest of this page is the detail.
Personal API key
The quickest way in, for working with your own account — a script, a Zapier or n8n automation, or another app you use yourself. A personal key acts as you, with full access to your data, so there are no scopes to manage.
In Tweek, open Profile → API Settings, and under Personal API keys create a key. Copy it right away — Tweek keeps only a hash, so it can't show the key again.
Send it as an X-API-Key header. Every example on this page works with a personal key — just swap the Authorization: Bearer header for this one:
curl https://tweek.so/api/v1/calendars \
-H 'X-API-Key: YOUR_KEY'
Connect with Tweek
Tweek uses OAuth 2.0, the Authorization Code flow with PKCE. A few things to know first:
- Every app is a public client — there's no client secret. SPAs, mobile, and CLIs can't keep one safe, so PKCE protects the flow instead.
- Access tokens are JWTs, good for 1 hour.
- Ask for the
offline_accessscope if you want a refresh token.
Register an app
Self-serve app registration isn't available yet. To get a client_id, email us at hello@tweek.so with your app's name (users see it on the consent screen), your redirect URI(s), and the scopes it needs. We'll set it up and send you a client_id — a public identifier, with no secret to manage. The redirect_uri you send in the flow must match a registered one exactly — same scheme, host, path and trailing slash, no wildcards.
Make a PKCE pair
For each sign-in, make a one-time code_verifier and its code_challenge. Keep the verifier — you'll need it when you swap the code. Only S256 is accepted.
code_verifier=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')
Send the user to the consent screen
Open this in the user's browser. They sign in if needed, then approve the scopes.
https://tweek.so/oauth
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
&scope=tasks:read tasks:write offline_access
&state=RANDOM_STRING
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
state yourself and check it on the callback. Tweek returns it unchanged, and it's what protects you from forged callbacks.Get the code back
On Allow, the browser lands on your redirect_uri with a code that's good for 5 minutes. On Deny, you get ?error=access_denied instead.
https://your.app/callback?code=AUTH_CODE&state=RANDOM_STRING
Swap the code for tokens
Do it right away — the code is single-use. Send it as form data.
curl -X POST https://tweek.so/api/v1/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=authorization_code \
-d code=AUTH_CODE \
-d redirect_uri=https://your.app/callback \
-d client_id=YOUR_CLIENT_ID \
-d code_verifier=CODE_VERIFIER
{
"access_token": "eyJhbGciOiJSUzI1Ni...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...", // only if you asked for offline_access
"scope": "tasks:read tasks:write offline_access"
}
Now you have a token — jump to the Quickstart to make your first call.
Refreshing tokens
Access tokens last an hour. Trade the refresh token for a new pair before it runs out:
curl -X POST https://tweek.so/api/v1/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=refresh_token \
-d refresh_token=REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID
To disconnect, revoke the refresh token at POST /api/v1/oauth2/revoke. Access tokens are stateless, so they can't be revoked — they just expire.
Validating tokens
Access tokens are RS256 JWTs. If your app is the resource server, verify them offline with our public keys — no call to Tweek per request.
| Check | Value |
|---|---|
| Signature | Public keys at GET /api/v1/oauth2/jwks |
Issuer (iss) | https://tweek.so |
Audience (aud) | https://tweek.so/api |
Useful claims: sub (user id), scope, client_id, exp.
Need a live answer instead — whether a token is revoked, or checking an opaque refresh token — call POST /api/v1/oauth2/introspect with a token parameter (RFC 7662). It returns { "active": true, ... } or { "active": false }.
Scopes
Scopes apply to OAuth apps only — a personal key isn't scoped. Ask for the least you need. Scopes go space-separated in the authorize URL, and the user sees them on the consent screen.
| Scope | Lets your app |
|---|---|
| tasks:read | See the user's tasks |
| tasks:write | Create, update, and delete tasks |
| calendars:read | See the user's calendars |
| colors:read | See the user's custom colors |
| offline_access | Get a refresh token |
Endpoints
Everything is under /api/v1. Authenticate every call with either X-API-Key: YOUR_KEY (personal) or Authorization: Bearer ACCESS_TOKEN (OAuth). JSON bodies as application/json.
| Method | Path | Scope | Use it to |
|---|---|---|---|
| GET | /api/v1/tasks | tasks:read | List tasks in a calendar |
| GET | /api/v1/tasks/{id} | tasks:read | Get one task |
| POST | /api/v1/tasks | tasks:write | Create a task |
| PATCH | /api/v1/tasks/{id} | tasks:write | Update a task |
| DELETE | /api/v1/tasks/{id} | tasks:write | Delete a task |
| GET | /api/v1/calendars | calendars:read | List calendars |
| GET | /api/v1/custom-colors | colors:read | List custom colors |
Tasks
Every task lives in a calendar. Get calendar ids from GET /api/v1/calendars, then pass one as calendarId — it's required to list or create tasks.
List tasks
Pass calendarId as a query param. Optionally narrow by date with dateFrom and dateTo (both YYYY-MM-DD, inclusive).
curl 'https://tweek.so/api/v1/tasks?calendarId=CALENDAR_ID' \
-H 'X-API-Key: YOUR_KEY'
The response is paginated. Tasks are in data; nextDocId is the cursor for the next page (null on the last one), and pageSize is set by the server.
{
"data": [ /* task objects */ ],
"pageSize": 100,
"nextDocId": "TASK_ID"
}
For the next page, pass that nextDocId back as startAt, and repeat until nextDocId is null.
curl 'https://tweek.so/api/v1/tasks?calendarId=CALENDAR_ID&startAt=NEXT_DOC_ID' \
-H 'X-API-Key: YOUR_KEY'
The task object
On create, calendarId, text, and done are required. A task also needs a placement — either date or listId (see the note).
| Field | Type | Notes |
|---|---|---|
id | string | Returned by the API; you don't send it |
calendarId | string | Required. Which calendar the task is in |
text | string | Required. The task title |
done | boolean | Required. Whether it's checked off |
date | string · null | YYYY-MM-DD — a regular dated task |
listId | string · null | List id, used instead of date for a task in a list (e.g. a someday list) |
color | string · null | A color id — see Default colors |
note | string · null | Free text |
notifyAt | string · null | Reminder time, ISO 8601. Paid plan. |
checklist | array · null | Subtasks, each { id, text, done }. Paid plan. |
freq | number · null | How the task repeats — see Recurring tasks. Paid plan. |
recurrence | string · null | An RRULE, only for a custom repeat (freq: 7). Paid plan. |
dtStart | string · null | Anchor date a repeat counts from, YYYY-MM-DD. Paid plan. |
gcal | boolean | On create only — also add the task to the user's connected Google Calendar. Default false. |
date (a dated task) or listId (a task in a list).notifyAt) and checklists (checklist) need a paid plan, and color is limited to blank, pink and yellowish. Sending a paid-only field on a free account returns 400.Create a task
curl -X POST https://tweek.so/api/v1/tasks \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "calendarId": "CALENDAR_ID", "text": "Buy milk", "done": false, "date": "2026-07-10", "gcal": false }'
// 201 Created
{ "id": "GENERATED_TASK_ID" }
Update a task
Send only the fields you want to change — for example, to check it off. You can't move a task between calendars this way; calendarId is fixed once the task exists.
curl -X PATCH https://tweek.so/api/v1/tasks/TASK_ID \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "done": true }'
Delete a task
curl -X DELETE https://tweek.so/api/v1/tasks/TASK_ID \
-H 'Authorization: Bearer ACCESS_TOKEN'
# → 204 No Content
Recurring tasks
There's no separate endpoint for repeats — you make a task recur through the same create and update calls, by setting freq on it. For the built-in patterns Tweek builds the schedule for you; for anything else use freq: 7 and supply your own RRULE in recurrence. Recurrence needs a paid plan.
freq | Repeats |
|---|---|
0 | Never — a one-off task (the default) |
1 | Daily |
2 | Weekly |
3 | Monthly |
4 | Annually |
5 | Every weekday (Mon–Fri) |
6 | Every two weeks |
7 | Custom — set your own RRULE in recurrence |
Set dtStart to the date the series starts from (YYYY-MM-DD), normally the same as the first occurrence's date. A weekly task from July 13:
curl -X POST https://tweek.so/api/v1/tasks \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{ "calendarId": "CALENDAR_ID", "text": "Team standup", "done": false, "date": "2026-07-13", "dtStart": "2026-07-13", "freq": 2 }'
For a custom cadence, set freq to 7 and pass an RFC 5545 RRULE in recurrence — e.g. every other Monday and Wednesday:
{ /* … */ "freq": 7, "recurrence": "RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE" }
freq, recurrence and dtStart, not one row per date; Tweek expands the individual occurrences from the rule. On read you may also see isBase, recurringTodoId and isBaseDeleted — series bookkeeping Tweek maintains, which you don't set yourself.Default colors
A task's color field takes a color id. These are the built-in colors:
| Color | Id | Fill |
|---|---|---|
blank | No highlight | |
pink | #CD2C54 | |
yellowish | #FDEF5D | |
black | #000000 | |
grey | #C2C2C2 | |
cornflower | #5167F4 | |
mango | #FFAF23 | |
greenish | #21FFA1 | |
lilac | #D95CFE |
blank, pink and yellowish. The other built-in colors, and any custom colors, need a paid plan — sending one on a free account returns 400.Calendars & colors
Lists the user's calendars. Each has at least id, name, and isDefault. You need a calendar's id to list or create tasks.
Lists the user's custom colors.
Errors
OAuth errors
The authorize and token endpoints return errors as { "error": "...", "error_description": "..." }.
| Error | What it means & what to do |
|---|---|
invalid_request | A parameter is missing or wrong — often PKCE isn't S256. Check the authorize URL. |
invalid_grant | The code or refresh token is wrong, expired, or already used. Codes last 5 minutes; refresh tokens rotate. Start over with the latest one. |
invalid_client | The client_id isn't registered. Double-check it. |
access_denied | The user clicked Deny on the consent screen. |
REST API errors
The /api/v1 endpoints signal failures with an HTTP status code.
| Status | What it means & what to do |
|---|---|
400 | The request body failed validation — check required fields, field formats, and free-plan limits. |
401 | No credentials, or the token or key is expired or invalid. Refresh the token, re-run the flow, or check your API key. |
403 | Authenticated, but the token lacks the scope this endpoint needs. (Personal keys aren't scoped.) |
404 | No resource with that id — e.g. an unknown task or calendar. |
429 | Too many requests. Back off and retry. |
500 | Something went wrong on our side. Retry; if it persists, contact support. |
Most REST errors carry the standard JSON body:
{ "statusCode": 403, "message": "...", "error": "Forbidden" }
A 400 from body validation is the exception: it returns a field-keyed tree — one _errors array per invalid field — that you can map straight onto your input.
// 400 — failed validation
{
"_errors": [],
"text": { "_errors": ["Required"] },
"date": { "_errors": ["Invalid date"] }
}
Back to Docs