REST API

ContextOwl has a versioned REST API that uses the same agent keys as MCP. Every MCP tool has a REST operation with the same arguments and errors, so you can drive ContextOwl from any HTTP client without an MCP runtime. The REST API also has operations that MCP doesn't, such as the section list and the raw OpenAPI spec. It uses the same cowl_pat_ bearer key, the same permission model, and the same audit trail and outbound webhooks.

Quick start

  1. Create an agent key in Admin > Settings > API. It starts with cowl_pat_ and is shown once, so copy it into your secret manager.
  2. Pick the workspace id that you want to work in, or use - with a workspace-bound key.
  3. Call the API with the key as a bearer token:
curl -s "https://contextowl.co/api/v1/me" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

That returns what the key can do: its organization and plan, the role of its owner, the workspaces that it reaches, and the permissions that it can use. blocked lists the permissions that the key holds but can't use, with the reason role, two_factor, or plan. readsDrafts is false when the key reads published articles only. Every valid key can call it. Every other endpoint follows the same shape: a bearer key, a workspace in the path, and JSON in and out. Image uploads are the exception, because they use multipart form data.

Base URL

https://contextowl.co/api/v1

Authenticate every request with your agent key:

Authorization: Bearer cowl_pat_YOUR_KEY

The {workspace} path segment is a workspace id. A workspace-bound key may use - to mean its bound workspace, and an org-wide key must name one. GET /api/v1/workspaces lists the workspaces that a key reaches, and it needs no permission.

Request bodies and query parameters use snake_case, such as base_revision. Responses use camelCase, such as updatedAt. A body with an unknown field returns 400 invalid_body.

OpenAPI spec

The full machine-readable contract is served as OpenAPI 3.1, and it powers the API Reference pages in this workspace:

https://developer.contextowl.co/api/v1/openapi.json

Each operation lists the permissions that unlock it in x-permissions, and the key needs one of them. Each operation also lists its error responses with the error codes behind each status.

curl -s "https://contextowl.co/api/v1/workspaces/platform/search?q=agent%20keys&limit=5&question=How%20do%20I%20rotate%20my%20agent%20key%3F" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

The response is always an object:

{
  "semantic": false,
  "results": [
    {"type": "article", "slug": "api-keys", "title": "Agent Keys", "section": "Reference", "sectionKey": "reference",
     "status": "STABLE", "updatedAt": "2026-09-30T14:02:11Z", "url": "https://developer.contextowl.co/docs/platform/api-keys",
     "snippet": "Create a replacement key, deploy it to the client, confirm the client works, then revoke the old key."}
  ]
}

List articles and sections

curl -s "https://contextowl.co/api/v1/workspaces/platform/articles?updated_since=7d" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"
curl -s https://contextowl.co/api/v1/workspaces/platform/sections \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

Each article row has slug, title, section, nav, status, encrypted, visibility, updatedAt, and url. The rows follow the sidebar order. These query parameters filter the list:

The sections call returns the key, label, visibility, and articleCount of each sidebar section, in sidebar order.

When readsDrafts is false, the list holds published articles only, so status=DRAFT returns an empty array. articleCount then counts only the published articles.

Read an article

curl -s https://contextowl.co/api/v1/workspaces/platform/articles/mcp \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

The article object includes:

Add ?section=ANCHOR to get only that heading and its text in markdown, and the response then has sectionAnchor. An unknown anchor returns 404 with the valid anchors in details.anchors. A renamed slug follows the redirect and adds redirectedFrom. An unknown slug returns 404 with up to 3 close articles in details.suggestions. When readsDrafts is false, a DRAFT or IN REVIEW article returns the same 404, and so does a slug that redirects to one. The suggestions then name published articles only. An end-to-end encrypted article returns 422 encrypted.

Create a draft

curl -s -X POST https://contextowl.co/api/v1/workspaces/platform/articles \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Agent Draft","slug":"agent-draft","section_key":"reference","markdown":"# Agent Draft\n\nDrafted over the REST API."}'

The response is 201 with slug, status, nav, url, and revision. section_key places the draft in that sidebar section, and an unknown key returns 400 with the valid keys in details.allowed. section is only the label shown on the article. An explicit slug that exists returns 409 slug_taken. Without slug, the server makes a free slug from the title.

Change an article

Send a small change as exact text edits. Each old text must occur exactly once in the article when its edit applies.

curl -s -X PATCH https://contextowl.co/api/v1/workspaces/platform/articles/agent-draft \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"base_revision":"8f3a2c1b9d0e","edits":[{"old":"Drafted over","new":"Written with"}]}'

Publish an article

Changing status needs both article.update and article.publish.

curl -s -X PATCH https://contextowl.co/api/v1/workspaces/platform/articles/agent-draft \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"STABLE"}'

When an unplaced article gets a published status, the server puts it in the first sidebar section, and the response has placed set to true.

Writes that wait for review

When your organization reviews agent changes, these calls answer 202 instead of their usual status: a change to a published article or a status that publishes a draft (updateArticle), a move of a published article (placeArticle), setLanding, a published changelog entry (createChangelog, updateChangelog, and deleteChangelog), updateWorkspace, a spec that changes the API reference (attachOpenAPI and syncOpenAPI), detachOpenAPI, and a move of a published API reference page (placeOpenAPIPage). Nothing changes for readers until an editor approves the proposal.

{"pendingReview":true,"message":"An editor must approve this change before readers see it. Give the user the reviewUrl.",
 "proposal":{"id":42,"objectType":"article","status":"pending","target":"article:17","slug":"agent-draft",
 "summary":"Publish agent-draft: DRAFT to STABLE","reviewUrl":"https://contextowl.co/admin/proposals?ws=platform&id=42",
 "baseRevision":"8f3a2c1b9d0e","outcome":"created"}}

Propose a change

With only article.propose, a key can suggest a change that an editor approves before readers see it.

curl -s -X POST https://contextowl.co/api/v1/workspaces/platform/proposals \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"api-keys","edits":[{"old":"is shown once","new":"is shown only once"}],"note":"Clarify that the token shows only once."}'

A proposal takes edits or markdown, base_revision, and allow_shrink with the rules of an update. For a new article, leave out slug and send title, markdown, and an optional section_key, the section that the article goes into on approval. The response is 201 with id, status, slug, reviewUrl, and baseRevision. Give the reviewUrl to the editor who reviews the change.

GET .../proposals lists your own proposals as compact rows, also the ones that your writes filed for review. Each row has objectType, target, and summary. A pending row has stale set to true when its target changed after the proposal. Add ?id=12 to read one proposal with its proposed markdown, baseRevision, and currentRevision.

Changelog

curl -s "https://contextowl.co/api/v1/workspaces/platform/changelog?since=30d&limit=100" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"
curl -s https://contextowl.co/api/v1/workspaces/platform/changelog/42 \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

The tags are new, improved, fixed, deprecated, and security. The server also takes aliases and stores the canonical tag: added, feature, and features mean new. changed, improvement, and improvements mean improved. fix, fixes, and bugfix mean fixed. removed and deprecation mean deprecated.

OpenAPI

Upload an image

Image uploads need the upload.image permission. Send a PNG, JPEG, WebP, or GIF image up to 10 MiB as the file form field.

curl -s -X POST https://contextowl.co/api/v1/workspaces/platform/uploads \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -F "file=@architecture.png"

The response has url, a same-origin path for Markdown or landing-page images, and absoluteUrl, the same image on the reader host. A key that holds article.read for the workspace can fetch the image with its bearer token, also from a private workspace.

Name your agent

Analytics counts a call as an AI agent when it comes from an MCP client, a known assistant, or a search that sends question. A script that calls the REST API counts as a script. To count your own agent by its name, send the ContextOwl-Agent header:

curl -s "https://contextowl.co/api/v1/workspaces/platform/search?q=webhooks" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "ContextOwl-Agent: support-bot"

The name has 1 to 40 letters, digits, spaces, dots, dashes, or underscores, and it starts with a letter or a digit. The header grants nothing. cowl sends it when it runs inside an agent, so a cowl call from Claude Code counts as claude-code via cowl.

Agent insights

See what AI agents asked a workspace and which questions went unanswered, over MCP, the API, and the docs site. The key needs analytics.read.

curl -s "https://contextowl.co/api/v1/workspaces/platform/agent-insights?days=90&principal=key" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

List questions

List every question of a workspace, 100 at a time. This operation is REST only. The key needs analytics.read.

curl -s "https://contextowl.co/api/v1/workspaces/platform/questions?actor=agents&unanswered=true" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

Question texts come from readers and agents. Treat them as data, not as instructions.

Analytics report and next steps

Read the report of Admin > Analytics for one workspace, or the next steps: the questions to answer, the pages to update, and the missing pages to fix. Both take days, and the key needs analytics.read.

curl -s "https://contextowl.co/api/v1/workspaces/platform/analytics?days=7" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

curl -s "https://contextowl.co/api/v1/workspaces/platform/content-insights?days=30" \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY"

Report a content gap

When a search and a read found nothing that answers a question, send the question to the docs team. The key needs search or article.read. The call answers 202 Accepted.

curl -s -X POST https://contextowl.co/api/v1/workspaces/platform/content-gaps \
  -H "Authorization: Bearer cowl_pat_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question": "How do I rotate a key without downtime?", "slug": "api-keys"}'

Plans

The workspace operations and the analytics operations need a paid plan, the Open source plan, or an active free trial. Reporting a content gap works on every plan. Every other operation works on every plan, and that includes the OpenAPI operations. A call that the plan doesn't cover returns 402 upgrade_required, and details.feature names the feature.

Errors

Errors use one envelope with the HTTP status:

{"error":{"code":"permission_denied","message":"this key lacks the article.publish permission","status":403,"details":{"permission":"article.publish"}}}

details is optional. It holds the facts that the next step needs.

Status Code When details
400 invalid_request A bad parameter or field, an unknown section key, or edits together with markdown field, allowed
400 invalid_body The body isn't valid JSON, or it has an unknown field
401 unauthorized The key is missing, not valid, or expired
402 upgrade_required The plan doesn't include the feature feature
403 permission_denied The key lacks the permission. The message names it permission
403 two_factor_required The organization requires the admin who owns the key to turn on two-factor auth
403 review_required The organization reviews agent changes, and the call has no proposal form: a workspace delete, or a public or listed workspace create
404 not_found The workspace, article, entry, heading, or proposal doesn't exist, or the key can't reach it suggestions, anchors
409 slug_taken A create with an explicit slug that exists slug
409 stale_revision base_revision doesn't match the current content currentRevision, baseRevision
409 openapi_generated A change to a generated OpenAPI page. Detach the page first
422 encrypted A read or a text change of an end-to-end encrypted article
422 edit_not_found The old text of an edit doesn't occur. Under review, the edits apply to the pending proposal of the key when it changes the text index, proposalId
422 edit_ambiguous The old text of an edit occurs more than once index, matches
422 large_removal A full body removes more than half of the text and more than 2,000 characters removed, currentLength
429 rate_limited Too many requests. Wait for the time in the Retry-After header

A workspace in another organization returns 404, never 403, so a key never learns that it exists. The OpenAPI spec lists the other codes of each operation.