Machine surface
REST API
A thin mirror of the MCP surface for callers that speak HTTP but not MCP. Both go through the same retrieval pipeline, so the entitlement and freshness guarantees are identical. There is no second, laxer path.
Authentication
Authorization: Bearer ctx_sk_... # or X-API-Key: ctx_sk_...
The key resolves to a member. Everything you retrieve is scored against that member's entitlement scopes. CORS is open, because the credential is the key rather than the origin.
Endpoints
POST /api/v1/search
The main call. Searches the workspace's condensed working memory and returns what this asker is entitled to see, refreshing anything past its freshness TTL from its system of record first.
body: {"query":"string","limit":"integer","skip_refresh":"boolean"}
GET /api/v1/memory/{id}/related
Walk from one memory object to its neighbours, ranked by shared entities, then topics, then keywords. Use it when one search result is the right thread and you want the rest of it. A neighbour outside the asker's scopes is never named or counted.
POST /api/v1/memory/{id}/expand
Read the full current record from the system of record a memory object was condensed from, for when the summary is not enough. No model is involved, so it returns exactly what the source says. Costs one source round-trip, metered against the monthly refresh allowance.
GET /api/v1/memory/{id}
Fetch one memory object by id. Returns not_found rather than forbidden when the asker is not entitled to it, so probing for ids cannot reveal what exists.
GET /api/v1/sources
List the connected systems of record with their freshness policies, sync status, and whether this asker is entitled to anything from each.
GET /api/v1/usage
Plan, metered usage and remaining allowance for the month. Call it before a batch run to decide whether to proceed or back off.
GET /api/v1/actions
The enabled actions the caller may invoke. Proposed and disabled actions never appear, and neither do actions whose required write scopes the caller does not hold.
POST /api/v1/actions/{name}/preview
Validates arguments and returns a rendered description plus the request hash, without contacting the source. It reports upstream_dry_run:false rather than simulating an effect, because a fabricated preview is indistinguishable from a real one.
body: {"name":"string","args":"object"}
POST /api/v1/actions/{name}/invoke
Performs one enabled action. Entitlement is checked at entry and again immediately before the effect; the allowance is charged only when it reaches upstream; and every attempt, refusals included, is recorded in the action log.
body: {"name":"string","args":"object","idempotency_key":"string"}
POST /api/v1/ingest
Read records from a connected source and condense them into memory objects. Owner or admin only, because ingesting spends the workspace's condensation budget.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/actions/discover
Enumerate the actions a connected source offers and record them as PROPOSED. Owner or admin only, and it enables nothing: each proposed action has to be enabled by hand, with the write scopes it requires. Deliberately not a tool, for the same reason /ingest is not.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/action-runs
Every invocation that ran and every invocation that was refused, newest first, with the refusal reason. Not gated behind the paid plan: what an agent changed in your systems is a safety artefact, not a compliance nicety.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/action-runs/{id}/approve
Perform an action whose policy holds it for approval. The APPROVER's entitlement is checked, because releasing it is authorising it. Owner or admin only, and deliberately not a tool: an agent holding a key must never see its own approval bypass in its tool list.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/action-runs/{id}/reject
Mark a held action rejected. Nothing upstream is contacted, then or ever.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org
Plan, subscription status, tenant count and the caps applied to a new tenant. The place to confirm a key is wired up correctly.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants
Idempotent on `external_ref`, which is your own identifier for the client. Call it on every one of your customer's logins rather than tracking which ones you have already created; `created` tells you which happened.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants
Every tenant this organization manages, newest first, with this period's usage.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants/{tenant_id}
The tenant, what it holds, and what it has spent this period.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}
Rename it, or change its caps. Raising a cap takes effect on the client's very next request, which is what makes a cap safe to set tightly.
body: {"source_id":"string","limit":"integer"}
DELETE /api/v1/org/tenants/{tenant_id}
Deletes the tenant's people, keys, sources and condensed memory objects. Usage already spent stays on this period's invoice, because it has already cost a source round trip and a model call.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}/members
The asker a retrieval is scored against. `scopes` is what your own permission model says this person may see, and it is enforced inside the ranking function rather than as a filter over the answer. No email is ever sent: these people do not have Contextely accounts and are never invited to make one.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants/{tenant_id}/members
Their roles and entitlement scopes. Never their memory.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}/members/{member_id}
Takes effect on their next retrieval. Narrowing a scope narrows every key that speaks as them, because a key is never more privileged than the person it belongs to.
body: {"source_id":"string","limit":"integer"}
DELETE /api/v1/org/tenants/{tenant_id}/members/{member_id}
Their API keys go with them, so an agent holding one stops working immediately.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}/keys
The returned `key` is shown once and never stored, so keep it. Every retrieval made with it is scored against that person's scopes, which is what makes it safe to hand to an agent.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants/{tenant_id}/keys
Prefixes and last use. The secret is not recoverable; mint a new one and revoke the old.
body: {"source_id":"string","limit":"integer"}
DELETE /api/v1/org/tenants/{tenant_id}/keys/{key_id}
Immediate. The next request carrying it is refused.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}/sources
Either an MCP server or a read-only SQL query. `required_scopes` is inherited by every memory object condensed from it, which is how a whole source is made visible to only part of a client's staff.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants/{tenant_id}/sources
Kind, status, last sync and last error.
body: {"source_id":"string","limit":"integer"}
POST /api/v1/org/tenants/{tenant_id}/ingest
Exists so you never have to mint a wildcard-scoped owner key per client purely to trigger a sync, which would be exactly the privilege manufacture the entitlement model is built to prevent.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/tenants/{tenant_id}/audit
Every query, and how many candidates were refused on entitlement grounds. This is the artefact your own customer's security reviewer asks for, and it is the reason to resell a permission-aware layer rather than a search index.
body: {"source_id":"string","limit":"integer"}
GET /api/v1/org/usage
What every client has spent this period against the pool, and what is over the included volume. Usage past the included amount bills as overage and keeps serving; only a client's own cap stops a request.
body: {"source_id":"string","limit":"integer"}
Example
curl -s https://www.contextely.com/api/v1/search \
-H "Authorization: Bearer ctx_sk_..." \
-H "Content-Type: application/json" \
-d '{"query":"acme renewal","limit":5}'Machine-readable spec
An OpenAPI 3.1 document is generated from the same tool catalogue this page renders, so it cannot describe an endpoint that does not exist. Point a client generator, an agent or Postman at it directly.
curl -s https://www.contextely.com/api/v1/openapi.json
Status codes
| 200 | Success. |
| 400 | bad_request: malformed arguments. The message says which. |
| 401 | unauthorized: missing, invalid or revoked API key. |
| 402 | quota_exceeded: the month's metered allowance is spent. Back off or upgrade. |
| 404 | not_found: no such object, or none this asker is entitled to. |
| 500 | internal: reported to us with a reference id you can quote. |
| 502 | upstream_failure: a system of record or the condenser could not be reached. |
| 503 | unconfigured: a capability this deployment has not been given credentials for. |
Every error body is {"error": {"code", "message"}}. Branch on code, never on the message text.