Errors and pagination
Errors
Every non-2xx response has the same shape:
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.
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).
Over a limit, the answer is 429 with Retry-After in seconds:
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:
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:
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:
- 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 eufinds a launch named “EU billing”, not one that mentions only billing. - Case and accents are ignored (
cafefinds “Café”), and a word matches part of a field (billfinds “billing”).%and_are matched as themselves. qnarrows the list alongside the endpoint’s other filters, and pages the same way. Results are never wider than the list withoutq: 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.
/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).
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.