Skip to main content
Task sync is a set of API features for keeping tasks in a Space and the same work in another system consistent in both directions. You write to Quiva with an idempotent upsert, protect against lost updates with a version, and read what changed with a cursor-based feed or signed webhooks. Everything on this page uses the base URL https://api.quiva.ai and JSON bodies. Authenticate with an integration token.
The task routes return the object itself, for example a task or {"results": [...]}. Only the accounts routes for managing integrations wrap success in {"data": ...}. Every error is {"error": "<text>", "code": "<code>"}.

The pieces

Versions and conditional writes

Every task has an integer version. It is 1 when the task is created and goes up by one on every write to the task, including a status change made by ticking a task action. It appears in every response and every event. Comments, time logs, adding or deleting an action and activity stamps do not change version. They move activity_at instead. Send If-Match: <version> on PATCH or DELETE of a task, or on the upsert, to say “only apply this if the task is still at the version I read”.
A quoted value such as "4" is accepted, and If-Match: 0 matches an older task that has no version yet.
Without If-Match, a write that keeps losing to concurrent writers can still answer 409 version_conflict after five attempts. Retry it. A conditional DELETE checks the version and then deletes, so a delete that races an update can win. Use If-Match to protect updates, not as a lock around deletes.

External ids and the upsert

An external_id is your id for a task: up to 256 bytes of UTF-8, any characters except /. Characters such as : and . are fine. Percent-encode the id in the URL path.
The body is the same as creating or updating a task. The space and external id come from the path, and a body value for either is ignored.
  • The id belongs to one Space. The same external id in another Space is a different task.
  • An integration has its own namespace of ids. Callers that are not integrations share one.
  • A task that is mapped keeps its external id. A PATCH that changes or clears it returns 409 external_id_immutable. Sending the same value is harmless.
  • You can find a task by id: GET /workspaces/space/OPS/tasks?external_id=ticket-9041 is an exact match.
  • Setting external_id on a task through create or PATCH without the upsert stores a plain attribute. It does not claim the id and is not unique.
Deleting a task, by anyone, marks its external ids as deleted for good. Upserting that id again returns 410 task_deleted and never recreates the task. Use a new id, or create the task with POST /workspaces/task. Resetting a staging Space is the one exception.

Strict ids for integrations

When an integration writes, a status, priority or tag that the Space does not define is refused with 400 unknown_status, unknown_priority or unknown_tag. Ids are compared, not names. An empty value clears the field. On a move, the destination Space’s lists apply.

Activity and listing

activity_at equals updated_at after a task write, and also moves when a comment, time log or action is written. Sort by it to find recent work:
The list returns {"results": [...], "results_total": n}. results_total is the number of tasks on this page, not a total. When more tasks exist, the response includes "has_more": true. The field is left out when there are no more, so treat a missing has_more as false. Page with offset. The default limit is 300.

Read the changes feed

GET /workspaces/space/{space_id}/changes returns every task, comment and action write in a Space, deletes included, in order. It needs tasks:read.
  • task is always the whole task after the change, never a delta.
  • When has_more is true, call again straight away with next_cursor.
  • An empty page still returns a next_cursor. Store it, so an idle Space’s cursor stays inside the retention window.
  • History is kept for 30 days. An older cursor returns 410 cursor_expired with a fresh next_cursor and retention_days. The events in the gap are gone, so list the Space to catch up, then resume from the new cursor.
  • The feed does not hide your own writes. Skip events whose origin is your integration.
  • A feed scoped to a Space also receives task-moved when a task leaves it.

Event types

Who made the change

origin says where a write came from, which lets you ignore your own.

A minimal sync loop

This loop pulls changes from Quiva, pushes your system’s changes with the upsert, and avoids echoing its own writes.
Run pull on a timer, or trigger it from a webhook. Test the whole loop against a staging Space first.

Connector sync engine

If your other system is an item tracker, you may not need to write a sync at all. A connector with a sync block keeps a Space and the tracker in step in both directions, using these same features underneath: external ids, versioned writes, the changes feed and signed webhooks. A connected tracker has its own integration and task subscription, which you can see in the integration’s delivery log. A task subscription can deliver to a connector’s sync address instead of your own endpoint; the engine verifies it and runs the push for you.

Task webhooks

Push the same events to your endpoint, signed.

Connect a tracker

The no-code option for a supported tracker.

Errors and limits

Every code the sync routes return.

Workspaces API

The full route reference.

Tasks

Fields on a task and how they work in the app.