API overview
The Standcast API lets your systems read your organization's data and receive events. It is versioned under /v1 and described by an OpenAPI 3.1 document.
- Base URL:
https://api.standcast.app - OpenAPI document:
https://api.standcast.app/openapi.json - Interactive reference, with every operation, parameter, example and error: API reference
What the API offers
Read organization data with an API key:
Operation Scope Returns GET /v1/org/teamsteams:readThe teams in standcast.yml: name, time zone, workdays, times and membersGET /v1/org/dailiesdailies:readConfirmed dailies, filtered by dateandteamGET /v1/org/dailies/{id}dailies:readOne daily GET /v1/org/digestsdigests:readDigest deliveries, with who did not send the daily, filtered by team,fromandtoGET /v1/org/blockersblockers:readConfirmed blockers, filtered by dateandteamGET /healthnone Service health Receive events with webhooks:
daily.submitted,blocker.openedanddigest.sent.
The API returns only what people confirmed and what already appears in the digest. It never returns drafts, suggestions, per-person metrics, rankings or aggregates.
What the API does not offer
You cannot send a daily through the public API. AI agents send dailies through the MCP server or the Claude Code skill, and scripts through the CLI.
Why: Standcast never sends a daily on someone's behalf without their explicit confirmation. That rule lives in one place, the MCP server and the skill, which show the person the daily and send it only after they confirm. An API key belongs to the organization, not to a person, so it cannot carry that confirmation. The routes the CLI uses to send dailies are internal, authenticated with a person's session, and not part of the public contract.
Quick start
curl -H "Authorization: Bearer $STANDCAST_API_KEY" \
https://api.standcast.app/v1/org/teams{
"data": [
{
"name": "Alpha",
"timezone": "America/Sao_Paulo",
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"remindAt": "09:00",
"digestAt": "10:00",
"memberLogins": ["alice", "bob"]
}
],
"next_cursor": null
}Authentication
The public Standcast API authenticates with an API key. Send it as a bearer token on every request:
GET /v1/{resource} HTTP/1.1
Host: your-standcast-host
Authorization: Bearer sck_xxxxxxxxxxxxxxxxxxxxxxxxAPI keys are prefixed with sck_. Every operation under /v1/org requires one. Only sck_ keys are accepted there: a session token (sc_) sent to /v1/org fails with 401 unauthorized, and an API key sent to a session-only route fails the same way.
Creating a key
Only an organization admin can create keys. An admin signs in with GitHub, opens the Standcast setup page of the organization and uses the API keys section: name the key, pick its scopes and create it. For a personal account, the account owner is the admin. Each key belongs to one organization and sees only that organization's data.
The full secret is shown once, when the key is created. Standcast stores only a hash and cannot show it again, so copy it right away. The list shows each key's prefix (the first 8 characters after sck_), scopes, creation date and last use, which is updated at most once per minute.
Scopes
Grant the narrowest scope that works.
| Scope | Allows |
|---|---|
teams:read | GET /v1/org/teams |
dailies:read | GET /v1/org/dailies and GET /v1/org/dailies/{id} |
digests:read | GET /v1/org/digests |
blockers:read | GET /v1/org/blockers |
A request that needs a scope the key does not have fails with 403 insufficient_scope. The error names the missing scope in required_scope:
{
"error": {
"code": "insufficient_scope",
"message": "api key lacks the dailies:read scope",
"required_scope": "dailies:read"
}
}Missing, malformed, unknown and revoked keys fail with 401 unauthorized. Each key is limited to 600 requests per minute (see Rate limits).
Revoking a key
An admin can revoke a key at any time. Revocation takes effect immediately and later requests with that key fail with 401 unauthorized. Rotate a key by creating the new one first, deploying it, then revoking the old one.
Storing keys safely
- Keep keys in a secret manager or in environment variables, never in source control or client-side code.
- Use one key per integration so a leak can be revoked without breaking the others.
- Never log the
Authorizationheader. - If a key may have leaked, revoke it immediately.
Session tokens (sc_)
Tokens starting with sc_ are session tokens issued to the Standcast CLI, skill and MCP server after a GitHub login. They are not an integration method and the operations they unlock are not part of this reference.
Errors
Every error response uses the same envelope:
{ "error": { "code": "invalid_daily", "message": "invalid daily", "issues": [] } }code is the stable contract: branch on it, not on message. message is always English and may change. issues is present only on validation errors and lists the failing fields. required_scope is present only on scope errors.
| Code | HTTP status | What to do |
|---|---|---|
unauthorized | 401 | The credential is missing, invalid, expired or revoked. Authenticate again or use a valid key. |
invalid_signature | 401 | Only on the GitHub webhook receiver: the X-Hub-Signature-256 header is missing or does not match the body. Nothing was processed. |
invalid_request | 422 | The request is malformed. Check issues and fix the listed fields. A signed GitHub webhook body that is not JSON also returns this code. |
invalid_daily | 422 | The daily payload failed validation. Check issues against the schema. |
org_not_found | 404 | The organization does not exist or the Standcast app is not installed on it. Check the name and the installation. |
org_not_configured | 409 | The organization has no valid standcast.yml. Add or fix the file in the .github repository, then retry. |
not_in_team | 422 | The person is not listed in any team of this organization. Add them to a team in standcast.yml. |
forbidden | 403 | The credential is valid but the person or organization is not allowed to perform this action. |
insufficient_scope | 403 | The API key lacks the scope this operation needs. The error carries required_scope. Create a key with that scope. |
not_found | 404 | The resource does not exist in the key's organization. |
suggestion_already_decided | 409 | The suggested blocker was already decided the other way. Fetch the draft again; it no longer lists it. |
invalid_state | 400 | An authorization link expired or was tampered with. Start the flow again. |
installation_pending | 409 | The installation is still being processed. Retry in a few seconds. |
access_denied | 403 | The person denied the authorization. Nothing was changed. |
slack_oauth_failed | 502 | Slack rejected the connection. Try connecting again. |
linear_oauth_failed | 502 | Linear rejected the connection. Try connecting again. |
payload_too_large | 413 | Only on the GitHub webhook receiver: the body is larger than 1 MB. Nothing was processed. |
rate_limited | 429 | Too many requests. Wait Retry-After seconds, then retry (see Rate limits). |
internal | 500 | Unexpected server error. Retry with backoff and contact support if it persists. |
Pagination
List operations use opaque cursors.
| Parameter | Where | Description |
|---|---|---|
limit | query | Page size, from 1 to 100. Defaults to 50. |
cursor | query | The next_cursor of the previous page. Omit it to get the first page. |
A paginated response carries the items and a next_cursor:
{ "data": [], "next_cursor": "eyJpZCI6MTIzfQ" }When next_cursor is null there are no more pages. Treat the cursor as an opaque string: do not parse it, build it or store it for long. Keep the same filters while paginating.
An invalid cursor or a limit outside 1 to 100 fails with 422 invalid_request. Dailies and digests are ordered newest first. For blockers, limit counts blockers, not dailies.
Rate limits
Limits use a fixed 60-second window.
| Credential | Limit |
|---|---|
Session token (sc_) | 120 requests per minute |
API key (sck_) | 600 requests per minute |
| Unauthenticated requests | Counted per IP address |
| Starting a CLI session | 10 requests per minute per IP address |
Limits apply per credential, so one noisy integration does not use up another's budget.
Headers
Rate-limited routes return these headers on every response:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the window. |
RateLimit-Remaining | Requests left in the current window. |
RateLimit-Reset | Seconds until the window resets. |
When you exceed the limit
The response is 429 with code rate_limited and a Retry-After header holding the seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 23Wait Retry-After seconds before retrying, and spread retries with jitter.
Versioning
The API is versioned in the path. /v1 is the current version. The info.version of the OpenAPI document is the Standcast release (for example 0.2.0).
- Public beta. While Standcast is in 0.x,
/v1can still change in incompatible ways. Such changes are listed in the changelog before they ship, with as much notice as possible. The 90-day notice below applies from 1.0. - New fields may be added to responses at any time. Ignore fields you do not recognize.
- New optional request parameters and new operations may be added to
/v1without notice. - A change that breaks existing clients ships only in
/v2. - Before
/v1behavior is removed or changed incompatibly, the change is announced in the changelog at least 90 days in advance.
Every release is listed in docs/api/CHANGELOG.md, under the same version as the GitHub release.
See the changelog for every release.