API docs
Send a URL, get back cited business context. Poll it while it builds, so your customer can watch.
Quick start
-
Key
Sign in to the dashboard with your email and create an API key. It starts with
utc_and is shown once. -
Build
curl https://urltocontext.com/v1/contexts \ -H "Authorization: Bearer $UTC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com"}' -
Watch
curl https://urltocontext.com/v1/contexts/ctx_… \ -H "Authorization: Bearer $UTC_API_KEY" -
Use
When
statusisready, the same response holds the context. Most contexts are ready in one to three minutes.
Keys
Send your key on every request: Authorization: Bearer utc_…. Keys belong on your
server; never put one in browser or app code. Revoke a key in the dashboard and it stops working
at once. An account can have 10 active keys.
The base URL is https://urltocontext.com. Requests and responses are JSON, except the
Markdown files. Times are UTC in ISO 8601, like 2026-09-25T03:31:28Z.
Endpoints
POST /v1/contexts-
Build a context. Body:
{"url": "https://example.com"}; a bare domain works too. Returns202with the context (statusqueued) and aLocationheader.Send an
Idempotency-Keyheader (up to 200 characters) to retry safely: the same key returns the context it already created, with200, and never builds or charges twice.Only public websites on ports 80 and 443 are accepted.
GET /v1/contexts/<id>The context's status, every status change with its time, the error if it failed, and the finished context once ready.
GET /v1/contextsYour contexts, newest first, without their bodies.
?limit=1 to 100, default 20.GET /v1/contexts/<id>/<file>.mdOne part of a ready context as Markdown:
brief,product,icp,positioning,brand,notesorquestions. Returns409 not_readyuntil the context is ready.GET /v1/meYour account: free contexts left, credits, contexts built and how many can build at once.
Context
{
"id": "ctx_3nx1iyWFJ57qu-lL",
"url": "https://example.com/",
"status": "ready",
"created_at": "2026-09-25T03:31:28Z",
"updated_at": "2026-09-25T03:32:40Z",
"events": [{"status": "queued", "at": "2026-09-25T03:31:28Z"}, …, {"status": "ready", "at": "…"}],
"error": null,
"context": {
"business_name": "Example",
"brief": "Example makes invoicing tools for plumbers. [ch_…]",
"pillars": {"product": "### Offering\n…", "icp": "…", "positioning": "…", "brand": "…"},
"notes": "### Evidence limits\n…",
"questions": [{"field": "icp.buyers", "purpose": "gap", "text": "Who approves the purchase?",
"reason": "The site does not say.", "source_ids": ["ch_…"]}],
"claims": [{"pillar": "product", "kind": "website_claim", "text": "Example sells invoicing software.",
"evidence": [{"source_id": "ch_…", "quote": "Invoicing for plumbers"}]}],
"sources": [{"id": "ch_…", "url": "https://example.com/", "title": "Example"}]
}
}
pillars are Markdown. Every statement cites its evidence as [ch_…],
the id of an entry in sources. claims hold the exact
quotes behind each pillar. questions are what the website could not answer, to ask
the business owner. notes say what the evidence can and cannot support.
The context describes what the website says. It is AI-generated from public pages, so treat it as a well-sourced draft and check what matters.
Statuses
queued- Waiting for a worker.
exploring- Reading the website's public pages.
synthesizing- Ranking the evidence and writing the pillars from it.
building- Assembling and filing the context.
ready- Done. The response holds the context.
failederror.codeisunreachable(no readable public pages),timeoutorfailed. A failed context costs nothing.
Poll every 3 to 5 seconds. Each change is in events with its time, so you can
show your customer each step as it happens.
Errors
Errors are {"error": {"code": "…", "message": "…"}}. The message is written for
people and may change; match on the code.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not {"url": "…"}. |
| 400 | invalid_url | Not a public http or https website. |
| 401 | missing_api_key | No Authorization: Bearer utc_… header. |
| 401 | invalid_api_key | The key is wrong or revoked. |
| 402 | out_of_credits | No free contexts or credits left. |
| 404 | not_found | No such context on your account. |
| 409 | not_ready | The file exists once the context is ready. |
| 429 | too_many_active | Wait for a context to finish before starting another. |
| 503 | busy | We are at capacity. Try again in a few minutes. |
Limits and credits
Every account gets 3 free contexts. After those, one credit builds one context: packs of 100 credits for $13 and 500 credits for $25. A context that fails costs nothing.
A free account builds one context at a time; an account with credits builds three.
GET /v1/me shows what is left.
Examples
Build a context and wait for it.
Python
import json, os, time, urllib.request
API, KEY = "https://urltocontext.com", os.environ["UTC_API_KEY"]
def call(method, path, body=None):
request = urllib.request.Request(API + path, method=method,
data=json.dumps(body).encode() if body else None,
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
with urllib.request.urlopen(request) as response:
return json.load(response)
context = call("POST", "/v1/contexts", {"url": "https://example.com"})
while context["status"] not in ("ready", "failed"):
time.sleep(4)
context = call("GET", f"/v1/contexts/{context['id']}")
print(context["status"])
print(context["context"]["pillars"]["product"] if context["status"] == "ready" else context["error"])
JavaScript (Node 18+)
const API = "https://urltocontext.com";
const headers = { Authorization: `Bearer ${process.env.UTC_API_KEY}`, "Content-Type": "application/json" };
let context = await (await fetch(`${API}/v1/contexts`, {
method: "POST", headers, body: JSON.stringify({ url: "https://example.com" }),
})).json();
while (!["ready", "failed"].includes(context.status)) {
await new Promise(r => setTimeout(r, 4000));
context = await (await fetch(`${API}/v1/contexts/${context.id}`, { headers })).json();
console.log(context.status);
}
console.log(context.status === "ready" ? context.context.brief : context.error);