Docs

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.

Tweek connecting to your app over the API

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:

Base URL
https://tweek.so/api/v1

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'
NoteTreat a personal key like a password — anyone who has it gets full access to your Tweek. Keep it server-side, never in client code or a public repo. To revoke it, delete it in API Settings; it stops working immediately.

Connect with Tweek

Tweek uses OAuth 2.0, the Authorization Code flow with PKCE. A few things to know first:

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
NoteGenerate 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
NoteRefresh tokens rotate — each one works once. Save the new one from every response and drop the old.

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.

CheckValue
SignaturePublic 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.

ScopeLets your app
tasks:readSee the user's tasks
tasks:writeCreate, update, and delete tasks
calendars:readSee the user's calendars
colors:readSee the user's custom colors
offline_accessGet 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.

MethodPathScopeUse it to
GET/api/v1/taskstasks:readList tasks in a calendar
GET/api/v1/tasks/{id}tasks:readGet one task
POST/api/v1/taskstasks:writeCreate a task
PATCH/api/v1/tasks/{id}tasks:writeUpdate a task
DELETE/api/v1/tasks/{id}tasks:writeDelete a task
GET/api/v1/calendarscalendars:readList calendars
GET/api/v1/custom-colorscolors:readList 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).

FieldTypeNotes
idstringReturned by the API; you don't send it
calendarIdstringRequired. Which calendar the task is in
textstringRequired. The task title
donebooleanRequired. Whether it's checked off
datestring · nullYYYY-MM-DD — a regular dated task
listIdstring · nullList id, used instead of date for a task in a list (e.g. a someday list)
colorstring · nullA color id — see Default colors
notestring · nullFree text
notifyAtstring · nullReminder time, ISO 8601. Paid plan.
checklistarray · nullSubtasks, each { id, text, done }. Paid plan.
freqnumber · nullHow the task repeats — see Recurring tasks. Paid plan.
recurrencestring · nullAn RRULE, only for a custom repeat (freq: 7). Paid plan.
dtStartstring · nullAnchor date a repeat counts from, YYYY-MM-DD. Paid plan.
gcalbooleanOn create only — also add the task to the user's connected Google Calendar. Default false.
NoteA task needs a placement or it won't show up anywhere. Set either date (a dated task) or listId (a task in a list).
NoteFree plans take a subset of fields. Reminders (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.

freqRepeats
0Never — a one-off task (the default)
1Daily
2Weekly
3Monthly
4Annually
5Every weekday (Mon–Fri)
6Every two weeks
7Custom — 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" }
NoteThe API returns the stored task documents — a repeating task is one base task carrying 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:

ColorIdFill
blankNo highlight
pink#CD2C54
yellowish#FDEF5D
black#000000
grey#C2C2C2
cornflower#5167F4
mango#FFAF23
greenish#21FFA1
lilac#D95CFE
NoteFree accounts can use 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

GET/api/v1/calendarscalendars:read

Lists the user's calendars. Each has at least id, name, and isDefault. You need a calendar's id to list or create tasks.

GET/api/v1/custom-colorscolors:read

Lists the user's custom colors.

Errors

OAuth errors

The authorize and token endpoints return errors as { "error": "...", "error_description": "..." }.

ErrorWhat it means & what to do
invalid_requestA parameter is missing or wrong — often PKCE isn't S256. Check the authorize URL.
invalid_grantThe code or refresh token is wrong, expired, or already used. Codes last 5 minutes; refresh tokens rotate. Start over with the latest one.
invalid_clientThe client_id isn't registered. Double-check it.
access_deniedThe user clicked Deny on the consent screen.

REST API errors

The /api/v1 endpoints signal failures with an HTTP status code.

StatusWhat it means & what to do
400The request body failed validation — check required fields, field formats, and free-plan limits.
401No credentials, or the token or key is expired or invalid. Refresh the token, re-run the flow, or check your API key.
403Authenticated, but the token lacks the scope this endpoint needs. (Personal keys aren't scoped.)
404No resource with that id — e.g. an unknown task or calendar.
429Too many requests. Back off and retry.
500Something 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