Conventions
Field casing
Section titled “Field casing”All request and response fields use snake_case — e.g. due_date, task_status_id.
Response envelopes
Section titled “Response envelopes”Successful responses are wrapped in a data envelope:
{ "data": { "id": "task_6a2918cff58b0a1d387866ee" } }List endpoints add a pagination object:
{ "data": [ /* … */ ], "pagination": { "next_cursor": "…", "has_more": true }}Errors use a separate envelope — see Errors.
Pagination
Section titled “Pagination”List endpoints are cursor-paginated. Pass the next_cursor from the previous response to fetch the next page; has_more tells you when to stop. Treat the cursor as opaque.
Filtering
Section titled “Filtering”GET /public/v1/tasks accepts filters as query parameters: priority, task_status_id, state (open / closed), task_type_id, assignee_id, due_date_from / due_date_to / due_date, tag_id. Without filters it returns every task in the scope — the same set you see in the project’s table view in the web app.
curl "https://api.bordio.com/public/v1/tasks?project_id=proj_6594a1b2c3d4e5f6a7b8c9d1&state=open&priority=high,critical" \ -H "Authorization: Bearer brd_sk_live_..."How filters combine:
- Different parameters → AND.
state=open&priority=highreturns open tasks that are also high-priority. - Multiple values within one parameter → OR. Values are comma-separated:
priority=high,criticalmatches either priority.tag_id=tag_a,tag_bmatches tasks that have any of the listed tags. - Ids use their public form (
task_status_…,task_type_…,user_…,tag_…). A malformed value returns400 validation_error; a well-formed id that matches nothing simply yields an empty page. The exception isproject_id: a project outside your key’s scope returns404 not_found_error. priorityalso acceptsnone— tasks with no priority set (priority=none,low= no priority or low).assignee_idaccepts the special valuenone— tasks with no assignee. It can be mixed with user ids:assignee_id=user_6594a1b2c3d4e5f6a7b8c9d1,none(either that user, or nobody).due_date_from/due_date_toacceptYYYY-MM-DD(interpreted as the start / end of that day, UTC) or a full ISO-8601 datetime. Tasks without a due date never match a range.due_date=nonereturns only tasks without a due date. It cannot be combined withdue_date_from/due_date_to(400 validation_error).
Recipes for common queries (unassigned tasks, overdue view, tasks with no priority) live in Filtering tasks. The full parameter list with schemas lives in the API Reference.
Clearing fields
Section titled “Clearing fields”PATCH endpoints distinguish two things: an omitted field is left unchanged, and a field sent as null clears its value — but only where “no value” makes sense:
| Endpoint | Fields that accept null |
Effect |
|---|---|---|
PATCH /tasks/:id |
priority, due_date, description, assignee_ids, reporter_ids, tag_ids |
Clears the value (assignee_ids: null = nobody; tag_ids/reporter_ids: same as []) |
PATCH /tasks/:id/subtasks/:id |
assignee_id, due_date |
Unassigns / removes the due date |
PATCH /scheduled-events/:id |
description, location, reminder_minutes_before |
Clears the text / removes the reminder |
Everything else is non-clearable: sending null to a required field (title, task_status_id, start_at, …) returns 400 validation_error instead of being silently ignored. To reset a numeric estimate, send 0; to clear a single custom field value, send its custom_fields item with a null value.
Note the difference from filters: in GET /tasks query parameters, JSON null doesn’t exist, so the special string value none plays the same role (assignee_id=none, priority=none, due_date=none) — see Filtering tasks.
Idempotency
Section titled “Idempotency”POST requests (creates) accept an Idempotency-Key header. Send a unique key (up to 255 characters — a UUID works well) so a retried request can’t create a duplicate:
curl -X POST https://api.bordio.com/public/v1/tasks \ -H "Authorization: Bearer brd_sk_live_..." \ -H "Idempotency-Key: 6a2918cf-f58b-0a1d-3878-66ee00000001" \ -H "Content-Type: application/json" \ -d '{ "title": "New task" }'How it behaves:
- Same key, same body → the original result is replayed; nothing new is created.
- Same key, different body →
409 conflict_error(already_exists). - Same key while the first request is still in flight →
409 conflict_error— retry once the first one finishes. - Keys are remembered for 24 hours, then expire — reusing a key after that is treated as a brand-new request.
- A key is scoped to its endpoint (and workspace): the same key on a different endpoint is independent.
- Only
POSTis idempotent via this header.PATCH/DELETEare not — aPATCHis naturally safe to repeat, since it sets fields to the values you send.