Tidigar public API

Use plain HTTPS with a personal token to list, read, create, update and delete whole plans. Cloud's base URL is https://app.tidigar.com/api/v1. A customer-operated Server uses https://<server-host>/api/v1 at its editor's origin.

Authentication

Sign in to Tidigar, open the account menu, choose Agent access, select an organisation and create a token with a name, scope and expiry. Copy the secret once into secure local storage; never paste it into a chat or commit it. Tokens expire after 90 days by default (1–365 days selectable); an account may have 20 active tokens. A token belongs to one account and one organisation. Expiry stops access at once. Revocation, membership loss, role and subscription changes, and account deletion stop access within a minute on Tidigar Business and at once on Tidigar Server; GET /token always reflects them at once.

Send Authorization: Bearer tdg_… on every request. Do not send session cookies with it. Read scope permits reads, validation and self-revocation. Write scope also needs an owner, coordinator or editor role and current write entitlement. Check the plan's writable before writing; the organisation's writable alone does not grant a protected plan's write access. On Tidigar Business, an organisation that has neither a running trial nor a current subscription keeps its tokens, but every request for its plans, reads included, answers 402 subscription_required until it is paid; a trial or payment lifts this within a minute, with the same token. Personal spaces and Tidigar Server are not affected. Token requests never create or renew browser sessions.

This API serves agents, scripts and command-line clients. Cross-site browser access has no CORS support. Mutations with a foreign Origin or a Sec-Fetch-Site other than same-origin are refused.

Versions and compatibility

The public API version is in /api/v1. Changes within v1 are additive only: new optional request fields, response fields, routes and error codes. Clients must ignore unknown response fields and branch on error.code, never on the human-readable message. Removing or renaming a field, changing its type or meaning, or requiring a previously optional field needs /api/v2, with at least six months during which v1 continues to work.

The project format has its own version, defined by the model specification. An optional model field does not change the API version. Project normalization keeps only the model's supported fields; the API's unknown-response-field rule does not make arbitrary project fields persistent.

Only the method/path combinations below are public. Other /api/v1 routes are internal and may change; tokens receive 401 token_not_allowed on those routes, including unknown paths, token management and HEAD variants.

Requests and responses

The examples use shell variables BASE, TOKEN, ORG and PLAN_ID for the base URL, secret, organisation ID and project UUID. Set them from your own account and plan; the example organisation is org. JSON bodies use Content-Type: application/json. Save the following canonical project as project.json and its enclosing object as body.json ({ "project": … }). Generate a fresh UUID for each new plan; keep it when retrying that create.

{
  "manifest": {
    "projectId": "11111111-1111-4111-8111-111111111111",
    "formatVersion": 1,
    "name": "Launch",
    "description": "",
    "dimensions": [],
    "year": 2026,
    "schedule": "calendar-days"
  },
  "items": [],
  "sharedViews": [],
  "milestones": [],
  "periodIndicators": []
}

A plan object contains id (the project's manifest.projectId), name, integer revision, size (normalized JSON bytes), createdAt, createdBy, updatedAt, updatedBy, url and project. Times are Unix milliseconds; creator and updater are account IDs. Answers carrying url also include owner (the plan owner's account ID), ownerName (their display name, or null), access (open or protected) and writable (this caller's effective permission to write this plan). Open plans allow organisation editors, coordinators and owners to write. Protected plans allow their plan owner, invited editors and organisation owners to write, subject to the organisation role, subscription and token scope: an organisation viewer or read-only token never writes. Everyone in the organisation can read both levels. The same fields accompany create, update, merge, restore and stale-plan answers; trash entries omit them. Access management uses the signed-in app; bearer tokens and assistants cannot change levels, ownership or invitations. Access belongs to the organisation, outside project; copies, share links, files and Markdown carry none. Private selection is not available yet. Stored private plans are omitted unless the caller is the plan owner, invited to it or an organisation owner; single-plan reads otherwise answer 404 no_such_plan. Tokens and assistants use the same access function. Access changes take effect at normal authorization-cache and list-refresh boundaries; they have no real-time propagation guarantee. url is the link a member opens the plan by in Tidigar, the same link Tidigar copies: https://open.tidigar.com/p/<code> for Cloud and <origin>/p/<code> for Server, where <code> is ten random letters and digits that the server keeps for the plan. It requires signing in and membership; it carries no plan data or access. To find the plan a link names, list the organisation's plans and match url. thumbnailAt is an optional timestamp. usedIn is present only when other plans of the organisation take in one of the plan's milestones as an incoming milestone: a list of { "milestone", "plan", "name", "incoming" } with this plan's milestone ID, the other plan's ID and name, and that plan's milestone ID. A receiving private plan the caller cannot see instead has plan: "", incoming: "" and name: "a private plan". List entries omit project. Single-plan and stale responses send a quoted revision in ETag. JSON API responses are not cached (Cache-Control: no-store).

Create accepts exactly one of { "plan": … } (the simple plan-v1 format) or { "project": … } (canonical project). Plan input uses the link builder's conversion, including inferred dimensions and colors, under fresh IDs. Canonical input preserves its IDs. Updates accept canonical projects only.

Example timestamps, sizes and identifiers below are illustrative. Requests are independent examples; run the update against the revision you last read.

GET /token

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/token"

Response: 200.

{
  "id": "token",
  "name": "Automation",
  "scope": "write",
  "expiresAt": 1800144000000,
  "account": {
    "email": "owner@example.com"
  },
  "organization": {
    "id": "org",
    "name": "Example"
  },
  "role": "owner",
  "writable": true,
  "readable": true
}

readable is false while the organisation's plans answer 402 subscription_required; GET /token itself still answers.

GET /organizations/:org/plans

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans"

Response: 200.

{
  "plans": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Launch",
      "revision": 1,
      "size": 237,
      "createdAt": 1792368000000,
      "createdBy": "account",
      "updatedAt": 1792368000000,
      "updatedBy": "account",
      "url": "https://open.tidigar.com/p/Xb4kQ9mT2w"
    }
  ],
  "usage": {
    "plans": 1,
    "bytes": 237,
    "maxPlans": 1000,
    "maxBytes": 1073741824
  },
  "writable": true,
  "memberCount": 1
}

GET /organizations/:org/plans/:id

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans/$PLAN_ID"

Response: 200.

{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "Launch",
  "revision": 1,
  "size": 237,
  "createdAt": 1792368000000,
  "createdBy": "account",
  "updatedAt": 1792368000000,
  "updatedBy": "account",
  "url": "https://open.tidigar.com/p/Xb4kQ9mT2w",
  "owner": "account",
  "ownerName": null,
  "access": "open",
  "writable": true,
  "project": {
    "manifest": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "formatVersion": 1,
      "name": "Launch",
      "description": "",
      "dimensions": [],
      "year": 2026,
      "schedule": "calendar-days"
    },
    "items": [],
    "sharedViews": [],
    "milestones": [],
    "periodIndicators": []
  }
}

POST /organizations/:org/plans

curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data-binary @body.json "$BASE/organizations/$ORG/plans"

Response: 201.

{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "Launch",
  "revision": 1,
  "size": 237,
  "createdAt": 1792368000000,
  "createdBy": "account",
  "updatedAt": 1792368000000,
  "updatedBy": "account",
  "url": "https://open.tidigar.com/p/Xb4kQ9mT2w",
  "owner": "account",
  "ownerName": null,
  "access": "open",
  "writable": true,
  "project": {
    "manifest": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "formatVersion": 1,
      "name": "Launch",
      "description": "",
      "dimensions": [],
      "year": 2026,
      "schedule": "calendar-days"
    },
    "items": [],
    "sharedViews": [],
    "milestones": [],
    "periodIndicators": []
  }
}

PUT /organizations/:org/plans/:id

curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H 'If-Match: "1"' --data-binary @body.json "$BASE/organizations/$ORG/plans/$PLAN_ID"

Response: 200.

{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "Launch",
  "revision": 1,
  "size": 237,
  "createdAt": 1792368000000,
  "createdBy": "account",
  "updatedAt": 1792368000000,
  "updatedBy": "account",
  "url": "https://open.tidigar.com/p/Xb4kQ9mT2w",
  "owner": "account",
  "ownerName": null,
  "access": "open",
  "writable": true,
  "project": {
    "manifest": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "formatVersion": 1,
      "name": "Launch",
      "description": "",
      "dimensions": [],
      "year": 2026,
      "schedule": "calendar-days"
    },
    "items": [],
    "sharedViews": [],
    "milestones": [],
    "periodIndicators": []
  }
}

An identical normalized project at the current revision is a no-op. A changed project creates the next revision. Updates always require If-Match.

GET /organizations/:org/plans/:id/revisions/:rev

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans/$PLAN_ID/revisions/1"

Response: 200.

{
  "revision": 1,
  "size": 237,
  "createdAt": 1792368000000,
  "createdBy": "account",
  "project": {
    "manifest": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "formatVersion": 1,
      "name": "Launch",
      "description": "",
      "dimensions": [],
      "year": 2026,
      "schedule": "calendar-days"
    },
    "items": [],
    "sharedViews": [],
    "milestones": [],
    "periodIndicators": []
  }
}

Returns one kept revision of a plan with its canonical project, for example the base of a three-way merge (see Conflicts). Read scope suffices. Every revision is kept for at least 24 hours, then the last revision of each day for 30 days; the current revision is always kept. A revision no longer kept returns 404 no_such_revision.

POST /organizations/:org/plans/:id/merge

Saves a change that went stale: send { "base": 3, "project": … } with the revision you read and your whole edited project, after PUT answered 409 stale_revision. The server merges three ways: the base revision, your project and the latest plan. Changes to different records or fields are combined and saved as the next revision, and the response is the plan, as for update.

Where both sides changed the same thing, nothing is saved. The response is 409 merge_conflicts with the latest plan (without its project), the revisions involved and each conflict described in English:

{
  "error": { "code": "merge_conflicts", "message": "…" },
  "plan": { "id": "11111111-1111-4111-8111-111111111111", "revision": 4, "…": "…" },
  "revisions": { "base": 3, "latest": 4 },
  "conflicts": [
    {
      "id": "5f0c2a9e41b37d68",
      "kind": "field",
      "about": "Activity: Build",
      "change": "Title",
      "base": "Build",
      "mine": "Build the app",
      "latest": "Build the site",
      "choices": ["mine", "latest"]
    }
  ]
}

kind is field, add-add (added on both sides; base is null), delete-edit, order or domain (the combination would not be valid). Ask the user about each conflict; never choose for them. Then send the same body with "choices": { "<id>": "mine" | "latest" } for every conflict. The server merges again on the plan as it is then, so new conflicts can appear, including domain conflicts that show only once earlier choices are made; answer those the same way and keep sending the earlier choices. A conflict's id depends on both values, so a choice for values that have since changed no longer applies and that conflict is listed again.

Write scope is required. A base no longer kept returns 404 no_such_revision: read the plan and redo the change on it.

POST /organizations/:org/plans/validate

Send the same body as create: exactly one of { "plan": … } or { "project": … }. Returns 200 { "project": …, "summary": "…" } with the normalized canonical project and the link builder's text summary, using activities[index], milestones[index], periods[index] and views[index]. Read scope and current membership suffice, including viewers; an unpaid organisation answers 402 subscription_required as for reads. The route stores nothing and checks the plan size, without reserving space or promising that a later create will succeed.

For example, { "plan": { "name": "Launch", "activities": [ { "title": "Build", "start": "2026-11-02" } ] } } returns a new canonical project and summary Launch (2026)\nactivities[0] Build: 2026-11-02 – 2026-11-02. To keep these generated IDs for creation, send the returned project as { "project": … }.

DELETE /organizations/:org/plans/:id

curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" -H 'If-Match: "1"' "$BASE/organizations/$ORG/plans/$PLAN_ID"

Response: 204 with no body.

Moves the plan to the organisation's trash, where it stays for 30 days and can be restored; after that it is deleted for good. Nothing deletes a plan at once. Deleting always requires If-Match with the revision you read: if the plan has changed since, the answer is 409 stale_revision with the latest plan, and nothing moves. Confirm with the user before deleting a plan.

GET /organizations/:org/trash

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/trash"

Response: 200.

{
  "plans": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Launch",
      "revision": 1,
      "size": 237,
      "createdAt": 1792368000000,
      "createdBy": "account",
      "updatedAt": 1792368000000,
      "updatedBy": "account",
      "deletedAt": 1792368000000,
      "purgesAt": 1794960000000
    }
  ]
}

Plans in the trash, most recently changed first. They have no url and cannot be read until they are restored.

POST /organizations/:org/trash/:id/restore

curl -sS -X POST -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/trash/$PLAN_ID/restore"

Response: 200.

{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "Launch",
  "revision": 1,
  "size": 237,
  "createdAt": 1792368000000,
  "createdBy": "account",
  "updatedAt": 1792368000000,
  "updatedBy": "account",
  "url": "https://open.tidigar.com/p/Xb4kQ9mT2w",
  "owner": "account",
  "ownerName": null,
  "access": "open",
  "writable": true,
  "project": {
    "manifest": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "formatVersion": 1,
      "name": "Launch",
      "description": "",
      "dimensions": [],
      "year": 2026,
      "schedule": "calendar-days"
    },
    "items": [],
    "sharedViews": [],
    "milestones": [],
    "periodIndicators": []
  }
}

Brings the plan back at the revision it had when it was deleted. A plan that is not in the trash answers 404 no_such_plan.

GET /organizations/:org/plans/:id/screens

A screen shows one saved view of a plan to anyone with its link, without an account: on a wall screen in a full browser window, or in a frame on a web page. It follows the plan as it changes, about once a minute. Screens are managed by tokens with write access; an organisation's owners may keep making screens and reading their links to people (agent_screens_off). The screen examples also use VIEW_ID, a saved view's id from the project's sharedViews, and SCREEN_ID, a screen's id.

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans/$PLAN_ID/screens"

Response: 200.

{
  "screens": [
    {
      "id": "44444444-4444-4444-8444-444444444444",
      "view": "33333333-3333-4333-8333-333333333333",
      "name": "Team room",
      "story": "",
      "joinCode": false,
      "createdAt": 1792368000000,
      "createdBy": "account",
      "publishedAt": null,
      "publishedRevision": null
    }
  ],
  "limit": 10
}

A screen never carries its token; read its link for that. publishedAt and publishedRevision say when the screen last showed a new version of the view, or null before the first.

POST /organizations/:org/plans/:id/screens

curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "{\"view\":\"$VIEW_ID\",\"name\":\"Team room\"}" "$BASE/organizations/$ORG/plans/$PLAN_ID/screens"

Response: 201.

{
  "screen": {
    "id": "44444444-4444-4444-8444-444444444444",
    "view": "33333333-3333-4333-8333-333333333333",
    "name": "Team room",
    "story": "",
    "joinCode": false,
    "createdAt": 1792368000000,
    "createdBy": "account",
    "publishedAt": null,
    "publishedRevision": null
  }
}

Send view and optionally name (1–100 characters, the view's name by default), story and joinCode. A story of up to 65,536 characters plays on the screen, written in the story language; null or "" removes it. joinCode shows a QR code by which members open the plan; it grants no access. A plan has at most ten screens, several of one view if wanted. Confirm with the user before making a screen: anyone holding its link sees the view.

PATCH /organizations/:org/plans/:id/screens/:screen

curl -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"Reception","joinCode":true}' "$BASE/organizations/$ORG/plans/$PLAN_ID/screens/$SCREEN_ID"

Response: 200.

{
  "screen": {
    "id": "44444444-4444-4444-8444-444444444444",
    "view": "33333333-3333-4333-8333-333333333333",
    "name": "Reception",
    "story": "",
    "joinCode": true,
    "createdAt": 1792368000000,
    "createdBy": "account",
    "publishedAt": null,
    "publishedRevision": null
  }
}

Send any of name, story and joinCode. A screen shows one view by its id: to change what it shows, update that view in the project and keep its id. Deleting the view, or moving the plan to the trash, ends its screens.

When a request to make or change a screen sends a story, the answer also has storyWarnings: the story is played against the plan as the screen shows it, and each call a reader's screen would skip is listed with its line, such as {"line": 4, "message": "update skipped: no activity or milestone or period is called \"Pilot\""}. The story is saved either way; correct it and send it again until the list is empty. Calls after a loop() are not checked.

DELETE /organizations/:org/plans/:id/screens/:screen

curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans/$PLAN_ID/screens/$SCREEN_ID"

Response: 204 with no body.

The screen's link stops working for good; a deleted screen cannot be brought back. Confirm with the user first, naming the screen.

curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/organizations/$ORG/plans/$PLAN_ID/screens/$SCREEN_ID/link"

Response: 200.

{
  "token": "UNVYfhP4iSvgEwr-UCkMGBTBY-MXWnGzx7lggPQXY38",
  "url": "https://embed.tidigar.com/#screen=UNVYfhP4iSvgEwr-UCkMGBTBY-MXWnGzx7lggPQXY38"
}

url is the screen's public address: Cloud's embed page, or /embed/ at a Server's origin. The token is the key to the view: anyone holding the address sees it until the screen is deleted, also after the token that read it expires. Give it only to the user; never put it in a plan, a log or a public place.

Calendar subscription of a screen

The lowercase hexadecimal SHA-256 of the screen token addresses its published snapshot. Append .ics to that snapshot address to subscribe to its calendar: https://data.tidigar.com/<hash>.ics on Cloud, https://data-test.tidigar.com/<hash>.ics on staging, and <server-origin>/api/v1/screens/<hash>.ics on Server. The original token is never sent to the server. Treat the calendar address as a secret read link: anyone holding it can read the screen's published view.

The response is text/calendar; charset=utf-8, with the screen's activities and milestones as all-day events, stable event identities, and links back to the original plan's saved view. Descriptions and other project metadata are omitted. This is a read-only publication, not the calendar project storage format. An empty view produces an empty calendar. Subscribe to the URL rather than importing a downloaded file to receive later publications.

The same publishing queue updates and deletes the snapshot and calendar. Server supports GET and HEAD with ETag revalidation and a one-minute public cache; Cloud serves the file from its data domain. Ended calendars return 404 after deletion and cache expiry. Calendar providers determine when to refresh, which can take hours or longer. Ending a link does not erase events already downloaded by a subscriber.

DELETE /token

curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/token"

Response: 204 with no body.

Limits

A normalized plan may be at most 1,048,576 bytes (1 MiB); the JSON request body may be at most 2,097,152 bytes. An organisation may keep 1,000 plans, including trash, and 1,073,741,824 bytes (1 GiB) across retained revisions, including trash. usage reports current counts and limits.

Each token may make 600 reads and 60 writes per minute. Writes are create, update, merge, delete and restore, and making, changing and deleting screens; every other public request is a read, and the two count apart. Tidigar Business counts approximately, per region, so a short burst may pass the limit before it applies. A request over the limit stores nothing and answers 429 rate_limited with Retry-After in whole seconds, at most 60; wait that long, then send it again unchanged. Requests that fail authentication do not count.

Errors

Errors are { "error": { "code": "…", "message": "…" } }. A stale update also includes plan, the latest complete plan, and its ETag. The table lists errors reachable from public token requests, including missing credentials and malformed addresses. Session-only management errors are outside this contract. No response body is available for HEAD.

HTTPCodeMeaning and action
400ambiguous_credentialsRemove session cookies when sending a bearer token.
400invalid_bodySend exactly one of plan or project for create/validate; project for update; base and project for merge; the documented screen fields.
400invalid_choicesMap each conflict id to mine or latest, with choices that fit together.
400invalid_planCorrect the named record or field in the plan-v1 input.
400invalid_revisionName one revision in If-Match, such as "3".
400invalid_projectCorrect the project according to the model specification.
400plan_id_mismatchKeep the project ID equal to the address ID.
401invalid_tokenUse a valid, unexpired, unrevoked token.
401token_not_allowedUse a public method and path.
401not_signed_inPlan request had no bearer token or valid session; send the token.
402subscription_requiredThe organisation needs write entitlement, or, on Tidigar Business, is not paid: its plans cannot be read or written until it is.
403read_only_tokenUse write scope for mutations.
403read_only_roleThe account role cannot write.
403agent_screens_offThe organisation's owners keep making screens and reading their links to people.
403plans_keptThe organisation keeps its plans to itself and shares no screens.
403cross_originSend mutations from the same origin or a non-browser client without origin headers.
404no_such_organizationThe token cannot access that organisation.
404no_such_planThe UUID is invalid, absent or in trash.
404no_such_revisionThat revision is no longer kept; merge without its base.
404no_such_screenThat plan has no such screen.
404no_such_viewName a saved view from the project's sharedViews.
404not_foundThe address contains invalid percent encoding.
409plan_existsThat project ID already exists; read it to check a retried create.
409plan_in_trashRestore the existing plan from the trash before writing it.
409stale_revisionRead the latest plan and reconcile before another update.
409screen_limitA plan has at most ten screens; delete one first.
409merge_conflictsAsk the user about each listed conflict and send their choices.
413plan_too_largeReduce the normalized plan or request body.
428revision_requiredSend the base revision in If-Match.
429rate_limitedWait the Retry-After seconds, then repeat the request; nothing was stored.
500internalThe server failed; check write outcome before retrying.
501screens_unavailableThis server cannot publish screens.
507quota_exceededFree space or reduce retained plans and history in Tidigar.

Conflicts

The server never chooses between two people's changes. When an update answers 409 stale_revision, either send the same project to POST /organizations/:org/plans/:id/merge with the revision you read as base, or merge yourself: use the base (from your read, or GET /organizations/:org/plans/:id/revisions/<base>), your project and the latest plan from the 409 response, settle conflicting changes with the user, and send the result with If-Match set to the latest revision.

Safe retries

For create from plan-v1, validate first and then create with the returned canonical project. Direct creates from plan-v1 generate fresh IDs on every request and cannot safely be retried after a lost response. For canonical create, persist manifest.projectId before sending the request. After a lost response, GET that ID. If it exists, compare the returned project with your intended normalized project; 409 plan_exists alone does not prove the content matches. If it is absent, retry the same ID. A trashed ID returns plan_in_trash on create and is not readable until it is restored from the trash.

For update, retain the base revision and intended project. After a lost response, GET the plan and compare it. Repeating a changed write with the old If-Match receives 409 stale_revision after the first write lands. An unchanged project at the current revision stays a no-op. Reconcile concurrent changes explicitly; the server never merges plans. Never retry with the latest revision merely to force an overwrite. Reads can be repeated; after a lost self-revocation response, GET /token answering invalid_token confirms that secret no longer works. For a server failure, use these outcome checks before retrying a mutation.