API v1

Your lists, tasks
and comments —
over HTTP.

One token, eleven endpoints. Anything you build against this sees exactly what your account sees, and nothing else.

11endpoints
120requests / minute
200per page, max
your first call
curl -H "Authorization: Bearer wabi_…" \
     https://app.wabi.do/api/v1/lists
200 OK
{
  "data": [
    {
      "id": 3,
      "name": "חשוב ודחוף",
      "task_count": 28,
      "members": [ … ]
    }
  ],
  "pagination": { "limit": 50, "has_more": false }
}

The whole surface

Eleven endpoints

Every call, and what a token needs to make it. A read-only token can reach the 5 marked read; the rest answer 403 insufficient_scope unless the token was created to write.

GET /lists read Every list you are a member of.
POST /lists read_write Create a list. You become its owner and first member.
GET /lists/{id} read One list.
PATCH /lists/{id} read_write Rename a list.
GET /lists/{id}/tasks read The tasks in a list.
POST /lists/{id}/tasks read_write Add a task to a list.
GET /tasks/{id} read One task.
PATCH /tasks/{id} read_write Change a task. Send only the fields you are changing.
GET /tasks/{id}/comments read The conversation on a task, oldest first.
POST /tasks/{id}/comments read_write Comment on a task.
PATCH /comments/{id} read_write Edit a comment you wrote.

Getting a token

  1. Open Wabi and click your avatar, at the bottom of the sidebar.
  2. Choose API Tokens, then Create new token.
  3. Pick a scope — Read only unless it needs to change things.
  4. Copy it. It is shown once and cannot be read again afterwards.
A token acts as you. Within its scope it can reach everything your account can, so treat it like a password: keep it on a server, never in a browser or a mobile app, and revoke it from the same screen the moment it is no longer in use.

Authentication

One header, on every request.

Authorization: Bearer wabi_1a2b3c…

Without it, or with a token that has been revoked, you get 401 and nothing else. Tokens do not expire on their own; they last until you delete them.

Scopes

read May GET. Anything that would change something answers 403 insufficient_scope. This is the default — a token you did not think about cannot damage anything.
read_write May also create and edit. Ask for it when you need it.

A scope cannot be changed after the fact. Widening one silently would mean a credential you already handed out quietly gaining powers, so instead you create a new token and delete the old one.

Rate limit

120 requests per minute per account. Over it you get 429 with a Retry-After header in seconds. It is set well above what an integration needs and is really there to stop a polling loop that forgot to sleep.

How the token is stored

It is not. We keep a SHA-256 of it and the first few characters, which is why the value is shown once when you create it and never again — and why nobody here can read it back to you. If you lose it, delete the token and create another.

Conventions

Envelope Every success answers { "data": … }. Lists also carry pagination.
Lists What the product calls a list is a list here. If you see workspace anywhere, that is the database's word and not part of this contract.
Names snake_case throughout.
Times ISO 8601 in UTC, always. Send any offset you like; you get UTC back.
PATCH Changes only what you send. null clears a field that can be cleared.
Paging limit and offset, ceiling 200, so one call cannot ask for an entire account.
What we promise. Fields get added; they do not get renamed or removed under /api/v1. Build against the fields you use and ignore the rest, and an upgrade on our side stays invisible on yours.

Errors

Every failure is the same object, so you can handle it in one place.

{ "error": { "code": "invalid_request", "message": "A task needs a title", "field": "title" } }
StatusCodeMeans
401unauthorizedNo token, or a token that has been revoked.
403forbiddenYour token is valid, but this is not yours to change.
403insufficient_scopeA read-only token tried to change something. Issue a read_write token instead.
404not_foundNo such record — or one you have no access to. The two are the same answer on purpose.
422invalid_requestThe request was understood and the contents were not accepted. field names the culprit.
429rate_limitedToo many requests. Retry-After says how many seconds to wait.
500server_errorOur fault. Retrying is reasonable.

Lists

What the product calls a list is a workspace in the database. This layer says list, because that is the word on the screen.

GET/api/v1/listsread

Every list you are a member of.

limitHow many to return. Default 50, maximum 200.
offsetHow many to skip. Default 0.
Response
{
  "data": [
    {
      "id": 3,
      "name": "חשוב ודחוף",
      "image": null,
      "created_at": "2026-03-11T08:12:00.000Z",
      "updated_at": "2026-08-05T17:40:00.000Z",
      "task_count": 28,
      "members": [
        { "id": 5, "name": "Noa Barak", "email": "[email protected]" }
      ]
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 12, "has_more": false }
}
POST/api/v1/listsread_write

Create a list. You become its owner and first member.

Request
{ "name": "Q3 launch" }
Response
{
  "data": {
    "id": 41,
    "name": "Q3 launch",
    "image": null,
    "created_at": "2026-08-05T18:02:11.000Z",
    "updated_at": "2026-08-05T18:02:11.000Z",
    "task_count": 0,
    "members": [ … ]
  }
}
GET/api/v1/lists/{id}read

One list.

Response
{ "data": { "id": 3, "name": "חשוב ודחוף", "task_count": 28, "members": [ … ] } }
PATCH/api/v1/lists/{id}read_write

Rename a list.

Request
{ "name": "Q3 launch — shipped" }
Response
{ "data": { "id": 41, "name": "Q3 launch — shipped", … } }

Tasks

A task belongs to exactly one list. Moving it between lists is a field on the task, not a separate call.

GET/api/v1/lists/{id}/tasksread

The tasks in a list.

Private tasks belong to whoever wrote them: a token sees its own, never a colleague's, even in a list you share.

statusopen or complete. Omit for both.
limitDefault 50, maximum 200.
offsetDefault 0.
Response
{
  "data": [
    {
      "id": 902,
      "list_id": 3,
      "title": "Send the quote to Dana",
      "status": "open",
      "due_date": "2026-08-09T21:00:00.000Z",
      "is_private": false,
      "assignee": { "id": 5, "name": "Noa Barak", "email": "[email protected]" },
      "tags": [ { "id": 2, "name": "urgent", "color": "#ee1c4e" } ],
      "created_by": 5,
      "created_at": "2026-08-02T06:30:00.000Z",
      "updated_at": "2026-08-05T11:20:00.000Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 28, "has_more": false }
}
POST/api/v1/lists/{id}/tasksread_write

Add a task to a list.

title is the only required field. An assignee has to already be a member of the list.

Request
{
  "title": "Send the quote to Dana",
  "due_date": "2026-08-10T00:00:00+03:00",
  "assignee_id": 5,
  "is_private": false
}
Response
{ "data": { "id": 902, "list_id": 3, "title": "Send the quote to Dana", … } }
GET/api/v1/tasks/{id}read

One task.

Response
{ "data": { "id": 902, "list_id": 3, "status": "open", … } }
PATCH/api/v1/tasks/{id}read_write

Change a task. Send only the fields you are changing.

Send due_date: null or assignee_id: null to clear either one. list_id moves the task to another list you belong to.

Request
{ "status": "complete" }
Response
{ "data": { "id": 902, "status": "complete", "updated_at": "2026-08-05T18:14:02.000Z", … } }

Comments

The conversation on a task. Wabi's own entries — “status changed to complete” — are not comments and do not appear here.

GET/api/v1/tasks/{id}/commentsread

The conversation on a task, oldest first.

limitDefault 50, maximum 200.
offsetDefault 0.
Response
{
  "data": [
    {
      "id": 5512,
      "task_id": 902,
      "content": "Sent it this morning.",
      "author": { "id": 5, "name": "Noa Barak", "email": "[email protected]" },
      "created_at": "2026-08-05T09:02:00.000Z",
      "updated_at": "2026-08-05T09:02:00.000Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 3, "has_more": false }
}
POST/api/v1/tasks/{id}/commentsread_write

Comment on a task.

@mentions are parsed and notified exactly as they are when someone types them in the app.

Request
{ "content": "Sent it this morning." }
Response
{ "data": { "id": 5512, "task_id": 902, "content": "Sent it this morning.", … } }
PATCH/api/v1/comments/{id}read_write

Edit a comment you wrote.

Yours only. Being in a list lets you read its conversation; it does not let you rewrite what someone else said.

Request
{ "content": "Sent it this morning, quote #4471." }
Response
{ "data": { "id": 5512, "content": "Sent it this morning, quote #4471.", … } }