Public API
Threada’s public developer surface is the public BFF. The plain REST API is for tenant-scoped WorkItem intake and follow-up. First-party shells use typed gRPC-web, and agent clients use the MCP or A2A JSON-RPC facades.
The OpenAPI contract for the API-key REST surface is generated at
/openapi/public-api.openapi.yaml. The
committed source copy lives next to this guide as public-api.openapi.yaml and
is regenerated with task quality:openapi:generate; task quality:docs:validate fails
when it drifts from the BFF router or public API wire structs.
What is live
- Create, list, read, and update WorkItems through
/api/v1/public/work-items. - Append and list WorkItem messages through
/api/v1/public/work-items/{work_item_id}/messages. - Create governed action proposals through
/api/v1/public/work-items/{work_item_id}/action-proposals. - Execute or dry-run existing governed action proposals through
/api/v1/public/action-proposals/{proposal_id}/execute. - List and read action records through
/api/v1/public/actions. - Call data projection boundary routes under
/api/v1/public/data/.... These validate requests, enforce tenant API-key scopes, and return structured data snapshots when a backing source is configured. Otherwise they returnprojection_unavailable. - Discover the agent surface at
/.well-known/agent-card.json. - Use MCP at
/api/v1/mcpor A2A at/api/v1/a2afor governed proposals and agent task orchestration.
Direct action execution through
POST /api/v1/public/work-items/{work_item_id}/actions is intentionally
disabled. It returns 403 with authorization_error; use governed proposals via
the public action-proposals routes, MCP/A2A, or first-party Work/Studio flows
for side-effecting actions.
Authentication
Send a tenant API key in the X-Api-Key header:
curl https://api.threada.ai/api/v1/public/work-items \
-H "X-Api-Key: $THREADA_API_KEY"
API keys are tenant-scoped and validated by identity-tenancy. The tenant is resolved from the credential, never from request headers or body fields.
Create, list, and revoke tenant API keys from the Admin governance API-key
surface. The underlying authenticated BFF methods are ListIdentityApiKeys,
CreateIdentityApiKey, and RevokeIdentityApiKey; the secret is returned once
at creation time.
Scopes
Use the smallest scope set needed for the integration.
| Scope | Allows |
|---|---|
work.read | List/get WorkItems and messages. |
work.write | Create/update WorkItems and append messages. |
work.actions | Create and execute governed action proposals, list/get action records, and authenticate agent-equivalent action flows. |
catalog.read | Read bounded data projection views and entities. |
source.read | Read bounded source health/freshness projection routes. |
Agent endpoints also accept scoped delegation bearer tokens. Existing API keys
with work.read, work.write, or work.actions are mapped to the agent scope
taxonomy where the key has equivalent authority.
Keys minted before the scope vocabulary was unified may carry the legacy
runtime_automation.read/.write/.actions names. These are accepted at
creation and normalized to their work.* equivalents (and canonicalized again on
validation), so existing integrations keep working without re-issuing keys.
Data Projection Boundary
The bounded data projection routes are API-key authenticated and tenant-scoped
in the same way as WorkItem routes. Requests are validated, authorized, and
rate-limited by API key. When a backing source is configured, the routes return
the latest structured snapshot; when the source is missing or unavailable, they
return 503 with error code projection_unavailable.
curl "https://api.threada.ai/api/v1/public/data/views/racing_catalog?limit=20" \
-H "X-Api-Key: $THREADA_API_KEY"
Supported boundary routes:
| Route | Scope | Required query |
|---|---|---|
GET /api/v1/public/data/views/{view_key} | catalog.read | none |
GET /api/v1/public/data/entities/{entity_type}/{entity_id} | catalog.read | view_key |
GET /api/v1/public/data/sources/{source_key}/health | source.read | view_key |
data/views/{view_key} returns a cursor-paged view summary: up to limit
complete groups with their events, evidence, aggregate health, per-source
health, schema metadata, typed extension attributes, and advertised
capabilities. Pass the returned opaque next_cursor unchanged to request the
next page. Full entity payloads can be loaded through
data/entities/{entity_type}/{entity_id}?view_key=... using generic entity
types such as group, event, or participant; both legacy source IDs and
canonical IDs resolve during migration. Event rows include start_label as a
provider display label, UTC starts_at, source timezone, and
localized_starts_at converted to the validated request timezone. Group
attributes may include
domain-neutral display metadata such as location, country, and
meeting_class; integrations should use location/state values for timezone and
filter logic, and keep class/type labels as display metadata. filter_key
supports all, today, upcoming, live, and finished; date applies an
exact local date. Both use the requested IANA timezone (UTC by default).
Source health requests require the stable registered {source_key} and return
404 when the optional date is outside that source revision’s coverage.
Clients should feature-detect optional data such as form, scratchings, calendar,
and results through capabilities rather than assuming every provider supplies
it.
racing_catalog is the canonical reusable racing projection. The older
customer_feed key remains a compatibility alias and should not be used by new
integrations. A view key identifies a stable projection contract, not a screen
in a consuming application.
WorkItem Flow
Create a WorkItem with a subject, source channel, and initial message:
curl https://api.threada.ai/api/v1/public/work-items \
-H "Content-Type: application/json" \
-H "X-Api-Key: $THREADA_API_KEY" \
-d '{
"subject": "Password reset",
"priority": "high",
"channel": "email",
"channel_id": "support-inbox",
"tags": ["billing", "vip"],
"contact": {
"name": "Sam Customer",
"email": "sam@example.com",
"locale": "en-US"
},
"initial_message": {
"role": "user",
"content": "I need help resetting my password.",
"external_message_id": "msg-123"
}
}'
initial_message.role may be omitted or set to user. When
initial_message.channel is supplied, it must match the top-level channel.
initial_message.external_message_id is forwarded to runtime automation as the
WorkItem source reference.
List WorkItems with cursor pagination:
curl "https://api.threada.ai/api/v1/public/work-items?status=new&limit=50" \
-H "X-Api-Key: $THREADA_API_KEY"
Use the returned next_page_token as page_token on the next request. Empty
cursor responses are returned as null.
Append a follow-up message:
curl https://api.threada.ai/api/v1/public/work-items/work-item-123/messages \
-H "Content-Type: application/json" \
-H "X-Api-Key: $THREADA_API_KEY" \
-d '{
"message": {
"role": "user",
"content": "Adding the latest invoice number: INV-1001."
}
}'
Follow-up messages support content plus optional role: "user". Per-message
agent identity, channel, and external message IDs are not accepted on append
because the runtime comment contract does not persist those fields.
Accepted channel and status labels are defined by the generated OpenAPI
WorkItemChannel and WorkItemStatus schemas. Use those schemas as the source
of truth for live enum values; documented status aliases are on the list
WorkItems query parameter in the same contract.
Actions And Proposals
Create governed action proposals through REST when an external system needs to ask Threada to perform a side effect under runtime policy:
curl https://api.threada.ai/api/v1/public/work-items/work-item-123/action-proposals \
-H "Content-Type: application/json" \
-H "X-Api-Key: $THREADA_API_KEY" \
-d '{
"integration_id": "integration-1",
"approval_reason": "Customer requested a refund",
"resource_type": "order",
"action_operation": "shopify_refund",
"payload": {
"type": "shopify_refund",
"order_id": "gid://shopify/Order/1001",
"amount": "42.00",
"currency": "USD",
"reason": "requested_by_customer"
}
}'
The response includes the persisted proposal_id, runtime status, proposed
action type, and the API-key identity stamped as proposed_by.
Execute an approved or auto-approved proposal by id:
curl https://api.threada.ai/api/v1/public/action-proposals/proposal-123/execute \
-H "Content-Type: application/json" \
-H "X-Api-Key: $THREADA_API_KEY" \
-d '{
"dry_run": false
}'
The response includes the updated proposal and the persisted action record. Set
"dry_run": true to have runtime validate the proposal and policy without
performing side effects, creating an action record, or changing proposal status.
List action records after governed execution:
curl "https://api.threada.ai/api/v1/public/actions?work_item_id=work-item-123" \
-H "X-Api-Key: $THREADA_API_KEY"
For agent-native flows, use one of the governed paths:
- MCP
tools/listdiscovers the exact tool catalog and JSON Schemas. - MCP
tools/callsupportscreate_work_item,get_work_item,propose_action,approve_action,execute_action,get_evidence, andexport_packet. - A2A maps
message/send,tasks/get,tasks/cancel,message/stream, andtasks/resubscribeonto WorkItem task lifecycle operations.
Execution is policy-gated. Approval requirements, dry-run support, and risk annotations are advertised by the agent card and MCP tool catalog. Real execution re-checks policy at execution time and uses idempotency keys inside the governed action path so retries do not double-execute.
Errors
REST errors use this envelope:
{
"error": {
"type": "validation_error",
"message": "Validation error: subject is required",
"code": "validation_error",
"correlation_id": "",
"retryable": false
}
}
Common statuses:
| Status | Meaning |
|---|---|
400 | Invalid query, path, or JSON body. Unknown request fields are rejected. |
401 | Missing or invalid API key. |
403 | API key is valid but lacks the required scope, or direct action execution is disabled. |
404 | WorkItem or action was not found. |
429 | Public or agent rate limit exceeded. |
503 | Required downstream service is unavailable. |