Skip to navigation

Errors and pagination

Errors

Every non-2xx response has the same shape:

{
"error": {
"code": "steps_incomplete",
"message": "3 required steps still need a decision.",
"details": { "steps": [{ "id": "stp_1", "name": "Privacy Review", "status": "NOT_STARTED" }] }
}
}
StatusCodeMeaning
401unauthorizedMissing, invalid, expired or revoked key
402subscription_requiredApp not active, trialing or past due: details says why
402checkout_requiredNo subscription to change yet: details.url is the Checkout
403insufficient_scopeThe key lacks the scope this endpoint needs
403read_only_sessionA signed-in session cannot make this change: use a key
403seat_requiredA signed-in person without a seat on the app
403admin_requiredSigned in, not an admin of the workspace or app it changes
403viewer_onlySigned in as a Viewer of the app: it can be read, not changed
403google_sign_in_requiredThe workspace requires a session signed in with Google
403forbiddenNot a member of that workspace, or not one who may change this
403person_requiredA person’s own (a docs tool export): sign in, not a key
404not_foundNo such resource in your organization
409conflict, steps_incomplete, draft, seat_limit_reached, …The request conflicts with current state
409workspace_adminA workspace owner’s or admin’s app roles cannot be changed
409launchedA launched launch cannot go back to being a draft
409ticketing_not_connectedThe ticketing system is not connected to the workspace
409ticketing_reconnectThe ticketing system turned the connection down: connect again
409docs_not_connectedThe person has not connected their account in that docs tool
409docs_reconnectThe docs tool turned the person’s connection down: connect again
409docs_tool_unavailableThis deployment does not export to that docs tool
409docs_destination_invalidThat place in the docs tool cannot take a page, or is out of reach
409artifact_emptyThe artifact has nothing written in it to export
415unsupported_media_typeAn upload not sent as multipart/form-data
422validation_errorThe body failed validation; details holds the issues
422below_floorFewer seats than the app’s minimum or the seats handed out
422domain_not_allowedEmail domains off the allow-list: details.domains names them
429rate_limitedA rate limit is used up: wait Retry-After seconds
502billing_failed, domain_setup_failedStripe or Vercel turned it down; nothing was saved
502upstream_errormemegen.link, a meme’s background, a ticketing system or a docs tool did not answer

An endpoint’s own codes are listed in its reference.

Rate limits

Every authenticated request is counted, after the key or session is checked and before anything else. Reads are GET and HEAD; everything else is a write. Limits are a sliding window: a request counts for one minute after it is made, so there is no moment when a whole new allowance arrives at once.

LimitCounted perReads / minuteWrites / minute
api-key-read, api-key-writeAPI key600120
session-read, session-writeSigned-in person30060
mcp-read, mcp-writeMCP connection (person and client)600120
workspace-read, workspace-writeWorkspace: every key and connection3,000600
searchCaller120
dory-exportCaller10
meme-render, memegen-link-searchCaller60, 30
okr-bulkCaller20
ticketing-syncCaller20
docs-export, docs-browseSigned-in person6020

search is GET /search and the MCP servers’ search tool; dory-export is GET /dory/events/{eventId}/export and the export_questions tool; meme-render is drawing a meme (GET /memes/{memeId}/image, POST /memes/preview and the tools that draw one) and memegen-link-search searching memegen.link (GET /meme-formats and its tool); okr-bulk is POST /key-results/bulk-score and POST /objectives/import and the bulk_score and bulk_create_objectives tools; ticketing-sync is linking a launch to what it follows in a ticketing system (a Linear project, a Jira epic, a GitHub project or milestone, an Asana project, a ClickUp list or task) and syncing it (PUT /launches/{launchId}/ticketing-project, POST /launches/{launchId}/ticketing-project/sync and their tools), which read the ticketing system; docs-export is exporting an artifact to Notion, Confluence or Google Docs (POST /launches/{launchId}/artifacts/{type}/exports and the export_artifact tool), and docs-browse listing where one can go (GET /docs-tools/{tool}/destinations and list_docs_destinations). A request its own key refuses is not counted against its workspace, so a key that is being refused does not use up what the workspace’s other keys share.

A key result’s metric webhook (POST /api/okrhub/metrics/{sourceId}, outside the versioned API and authenticated by the metric’s own secret rather than a key) has limits of its own: metric-webhook-ip, 600 requests per 10 minutes per address before the secret is checked, and metric-webhook, 60 an hour per metric once it is. A metric moves daily, not by the second: post when the number changes. See Metric webhooks.

Every answer to an authenticated request carries the IETF rate limit headers (draft-ietf-httpapi-ratelimit-headers), one item per limit the request was counted against: the quota q in a window of w seconds, how many remain (r) and how many seconds until the window resets (t).

RateLimit-Policy: "api-key-read";q=600;w=60, "workspace-read";q=3000;w=60
RateLimit: "api-key-read";r=598;t=41, "workspace-read";r=2871;t=41

Over a limit, the answer is 429 with Retry-After in seconds:

HTTP/1.1 429 Too Many Requests
Retry-After: 7
RateLimit-Policy: "api-key-read";q=600;w=60
RateLimit: "api-key-read";r=0;t=7
{
"error": {
"code": "rate_limited",
"message": "Too many requests: the api-key-read limit is 600 per minute. Try again in 7 seconds.",
"details": { "policy": "api-key-read", "limit": 600, "windowSeconds": 60, "retryAfter": 7 }
}
}

Nothing was done, so the request is safe to send again. Wait Retry-After seconds first: requests sent while refused are counted too, so retrying at once keeps a client shut out. A client that watches r and slows down as it nears zero never sees a 429. For bulk work, spread requests out rather than sending them in bursts, and use a separate key for each integration so one does not use up another’s allowance.

Updates

PATCH changes only what the body names. A field left out keeps its value, whatever default it had when the resource was created, so renaming a launch does not touch its status. Send null to clear an optional field. A list (such as a key result’s ownerUserIds) is replaced as a whole when sent.

Drafts

Objectives, key results, launches and go links take a draft boolean, false unless sent. A PATCH with "draft": false publishes, and the list endpoints for each (objectives, an objective’s key results, launches and go links) filter with ?draft=true (only drafts) or ?draft=false (only what is published). What a draft holds back depends on the app:

ResourceWhile it is a draftBack to draft
Objective, key resultNothing: it is a label. A draft is seen, scored, rolled up and checked in on like any other.Yes
LaunchAnnounces nothing and notifies nobody, stays out of review queues, reminders and the weekly digest, and cannot be launched (409 draft). Publishing announces it to its stakeholders.Yes, until it has launched (409 launched)
Go linkHolds its slug, so nobody else can take it, but does not redirect: following it lands on Go Links, which says it is a draft.Yes

A launch gets its template’s steps once it has a planned date, draft or not: when it is created with plannedLaunchDate, or, for a draft created without one, when a PATCH first sets it or publishes it. A draft’s reviewers hear of their steps when it is published. Send generateSteps: false to leave the steps off.

Dory events have no draft field: their times open and close them. An event with a startsAt or questionsOpenAt is scheduled until the earlier of them and then opens by itself (status LIVE, phase scheduled, then live). One with neither is Not started (status DRAFT, phase draft): its join code finds nothing until a host starts it with status: LIVE.

Pagination

List endpoints are cursor-paginated:

curl "https://api.taktoria.com/api/v1/launches?limit=50&cursor=lch_7f3a" \
-H "Authorization: Bearer $TAKTORIA_API_KEY"
{ "data": [], "nextCursor": "lch_9c1b", "hasMore": true }

Pass nextCursor as the next request’s cursor. limit defaults to 50 and caps at 200.

Searching

The list endpoints whose page in the app has a search box take q, the same text that box sends, and match it the same way, so the API and the app find the same things:

curl "https://api.taktoria.com/api/v1/launches?q=billing%20eu" \
-H "Authorization: Bearer $TAKTORIA_API_KEY"
  • The query is trimmed and cut to 100 characters, then split into words on spaces, at most 8 of them.
  • Every word must appear somewhere in one of the fields the endpoint searches, in any order: billing eu finds a launch named “EU billing”, not one that mentions only billing.
  • Case and accents are ignored (cafe finds “Café”), and a word matches part of a field (bill finds “billing”). % and _ are matched as themselves.
  • q narrows the list alongside the endpoint’s other filters, and pages the same way. Results are never wider than the list without q: a private objective, an event you may not run, or a draft go link’s destination is not found by anybody who could not already see it.
EndpointSearches
GET /launchesName, description, key customers, owners’ names
GET /reviewsThe step’s name, its launch’s name
GET /objectivesTitle, description, key results’ titles, owner’s and team’s names
GET /dory/eventsTitle, join code, description
GET /dory/events/{eventId}/questionsThe question, and who asked it unless they asked anonymously
GET /golinksShort name, description, destination
GET /memesCaptions, the template’s name
GET /meme-templatesName
GET /membersName, email address
GET /teams/organizationalName

/launches, /dory/events, /golinks and /memes also accept search, the parameter’s name before q. It is deprecated, matched the same way, and ignored when q is sent.

Searching a whole app

GET /search?app=<slug>&q= searches everything in one app at once, as its top bar does, and returns each kind of thing as a group: how many match (total) and the first five (items).

curl "https://api.taktoria.com/api/v1/search?app=okrhub&q=onboarding" \
-H "Authorization: Bearer $TAKTORIA_API_KEY"
appGroups, and the scope each needs
launchcallaunches (launches:read), steps (steps:read)
okrhubobjectives (objectives:read), keyResults (keyResults:read), teams (teams:read)
doryevents (events:read), questions (questions:read)
golinkslinks (golinks:read)
memegenmemes (memes:read), templates (memeTemplates:read)

A group the key cannot read is left out; a key that can read none of an app’s groups gets 403 insufficient_scope. A missing or unknown app is 400 invalid_app, and an app the workspace does not pay for is 402 subscription_required.