Press / to search

All documentation
docs Call it from your software REST API reference

Runs onWrit CloudDesktopSelf-hosted

api ▸ full surface

API reference

The whole surface, endpoint by endpoint.

Writ answers in two places: writ-agentd, the agent daemon on your own machine at http://127.0.0.1:8131, and Writ Cloud at https://api.usewrit.app. Same JSON grammar and Bearer header on both — different token families, and only one of the two is metered.

Writ runs on your own accounts, with your own credentials and data, on sites you are authorized to use.

surfaces ▸ local + cloud

Two surfaces.

The local daemon is the software the desktop app (or the self-hosted agent) keeps running: loopback-only and free. Writ Cloud is the hosted surface a wt_ key unlocks. The SDKs discover the first and can carry the second.

SurfaceBase URLAuth
Local agent (writ-agentd)http://127.0.0.1:8131Bearer wlt_ · wlk_ · wlo_
Writ Cloudhttps://api.usewrit.appBearer wt_ · X-Writ-Client-Id header (keyless)

Use 127.0.0.1, not localhost — the daemon checks Host and Origin against DNS rebinding. An HTTPS twin listens on https://127.0.0.1:8132 with a per-install CA at ~/.writ/tls/ca.pem, and WRIT_PORT overrides the port.

A published workflow is its own door: POST /v1/{slug}/{path} on Writ Cloud, authenticated with a csk_ consumer key you mint for its callers, documented on managed endpoints. The MCP server (JSON-RPC at /mcp) and signed webhook deliveries have their own pages too: MCP server, webhooks.

auth ▸ five prefixes

Token families.

One header everywhere: Authorization: Bearer …. What changes is the token family — each prefix is confined to its surface. Rotation and scope detail live in authentication.

TokenSurfaceRole
wlt_LocalRuntime token — the full surface. The only family that can mint scoped keys.
wlk_LocalScoped key minted via POST /v1/keys; scopes are a CSV of read|run|admin.
wlo_LocalOAuth 2.1 token carrying the run scope.
wt_CloudAPI key for metered cloud calls on api.usewrit.app.
X-Writ-Client-IdCloudNot a token — a device-id header for the keyless routes and their fixed allowance.

Minting a wlk_ key requires the wlt_ runtime token — so a leaked scoped key can never widen itself:

const key = await client.keys.create({ name: "ci-runner", scopes: "read,run" });

errors ▸ stable codes

One error shape.

Errors are small JSON objects: {"error": "…", "code": "…"} — a human sentence and a stable code. The codes:

CodeHTTPMeaning
bad_request400Malformed JSON, a missing field, or a parameter outside its allowed range.
unauthorized401No Bearer token, or one the daemon does not recognize.
captcha_required402The operation hit a verification step that needs a human to finish.
forbidden403The token is valid but its scopes do not cover this operation.
not_found404No resource at that id or path.
device_capacity409The device is at its capacity limit for this resource.
vault_locked423The encrypted vault is locked; unlock it and retry.
too_many_requests429Too many requests in a short window; space them out and retry.
internal500Unexpected failure inside the daemon.

A few 4xx paths answer text/plain rather than JSON. When you read error bodies, tolerate non-JSON.

runs ▸ the contract

Run semantics.

The one contract to understand before wiring anything:

  • Async by default. POST /v1/workflows/{id}/run answers as soon as the run is dispatched, with the run id.
  • Or block for the result. Add ?wait=true — the call holds until the run settles. timeout is in seconds, clamped to 1–3600, default 120.
  • A failed run is a result, not an error. You get the run back with status: "failed"; keep error handling for transport and auth problems.
  • Two id shapes. The run feed returns composite ids like workflow-3; every /v1/runs/{id}/* call takes the numeric row id.

reference ▸ 98 operations

The local surface.

Every operation of the local daemon, exactly as the OpenAPI description states it — 98 operations in 16 groups, all under /v1 on loopback, all Bearer-authenticated.

Every call is this shape — one Bearer header, JSON in, JSON out:

monitor.sh

# Local agent daemon — loopback, wlt_/wlk_ token (use 127.0.0.1, not localhost)
curl -X POST http://127.0.0.1:8131/v1/monitors \
  -H "Authorization: Bearer $WRIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/pricing"}'

List envelopes vary by design. Most lists answer {"data": [...], "count": n}; /v1/runs adds "total"; monitors, selectors, extractors, automations and /v1/changes/recent answer bare arrays.

Agent

MethodPathSummary
GET/v1/agentLightweight agent status
GET/v1/healthDeep health probe

Workflows

The run semantics above apply to POST /v1/workflows/{id}/run. The session pair manages the browserless HTTP-lane session a workflow can hold.

MethodPathSummary
GET/v1/workflowsList workflows
POST/v1/workflowsCreate a workflow
GET/v1/workflows/{id}Get one workflow
PATCH/v1/workflows/{id}Update a workflow
DELETE/v1/workflows/{id}Delete a workflow
POST/v1/workflows/{id}/runRun a workflow (async by default, or wait for the result)
POST/v1/workflows/{id}/cancelCancel the newest live run of a workflow
GET/v1/workflows/{id}/sessionBrowserless HTTP-lane session status
DELETE/v1/workflows/{id}/sessionClear the persisted session

Runs

GET /v1/runs/{id}/events streams live progress over SSE. Cancelling a run that has already settled answers 409 — with the run itself as the body.

MethodPathSummary
GET/v1/runsList runs (enriched feed)
GET/v1/runs/{id}Get one run
GET/v1/runs/{id}/resultsRaw run result payload
GET/v1/runs/{id}/dataExtracted data of one run
GET/v1/runs/{id}/eventsLive run event stream (SSE)
POST/v1/runs/{id}/cancelCancel a live run by run id

Monitors

MethodPathSummary
GET/v1/monitorsList monitors
POST/v1/monitorsCreate a monitor
GET/v1/monitors/capacityDevice check-capacity meter
GET/v1/monitors/{id}Get one monitor
PATCH/v1/monitors/{id}Update a monitor
DELETE/v1/monitors/{id}Delete a monitor
POST/v1/monitors/{id}/runRun a monitor check now
GET/v1/monitors/{id}/changesChange + uptime history of a monitor
GET/v1/changes/recentRecent changes across all monitors

Selectors

MethodPathSummary
GET/v1/monitors/{id}/selectorsList a monitor's selectors
POST/v1/monitors/{id}/selectorsAdd a selector to a monitor
GET/v1/monitors/{id}/selectors/{selector_id}Get one selector
PATCH/v1/monitors/{id}/selectors/{selector_id}Update a selector
DELETE/v1/monitors/{id}/selectors/{selector_id}Delete a selector
POST/v1/monitors/{id}/selectors/{selector_id}/toggleToggle a selector's enabled flag
POST/v1/monitors/{id}/selectors/{selector_id}/testProbe a selector against the live page
POST/v1/monitors/{id}/selectors/{selector_id}/set-baselineCapture the selector's baseline
POST/v1/monitors/{id}/selectors/{selector_id}/clear-baselineClear the stored baseline

Extractors

Note the toggle: PATCH, not POST as on selectors.

MethodPathSummary
GET/v1/selectors/{selector_id}/extractorsList a selector's extractors
POST/v1/extractorsCreate an extractor
GET/v1/extractors/{extractor_id}Get one extractor
PATCH/v1/extractors/{extractor_id}Update an extractor
DELETE/v1/extractors/{extractor_id}Delete an extractor
PATCH/v1/extractors/{extractor_id}/toggleToggle an extractor's enabled flag
POST/v1/extractors/{extractor_id}/testTest a saved extractor

Automations

MethodPathSummary
GET/v1/automationsList automations
POST/v1/automationsCreate an automation
GET/v1/automations/{id}Get one automation
PATCH/v1/automations/{id}Update an automation
DELETE/v1/automations/{id}Delete an automation
POST/v1/automations/{id}/enableEnable / disable an automation
POST/v1/automations/{id}/runFire an automation now

Personas

MethodPathSummary
GET/v1/personasList personas
POST/v1/personasCreate a persona
GET/v1/personas/{id}Get one persona
PATCH/v1/personas/{id}Update a persona
DELETE/v1/personas/{id}Delete a persona
GET/v1/personas/{id}/runsRecent runs that acted as this persona
POST/v1/personas/validate-totpValidate a TOTP seed
POST/v1/personas/{id}/test-2faSmoke-test the persona's 2FA

Secrets

Metadata only — no endpoint ever returns a secret value.

MethodPathSummary
GET/v1/secretsList secrets (metadata only)
POST/v1/secretsCreate a secret
GET/v1/secrets/{key}Get one secret's metadata
DELETE/v1/secrets/{key}Delete a secret

Vault

MethodPathSummary
GET/v1/vault/statusApp-lock status
POST/v1/vault/lockLock the vault now
POST/v1/vault/unlockUnlock the vault

Files

MethodPathSummary
GET/v1/filesList file handles
POST/v1/filesUpload a file (multipart)
POST/v1/files/from-dataExport workflow data into a file
GET/v1/files/{id}Get one file handle
DELETE/v1/files/{id}Delete a file
GET/v1/files/{id}/contentDownload file bytes

Data

MethodPathSummary
GET/v1/dataData-explorer workflow picker
GET/v1/workflows/{id}/dataAggregated extracted-data table
DELETE/v1/workflows/{id}/dataDelete extracted-data rows
GET/v1/workflows/{id}/data/runsData snapshot index
GET/v1/workflows/{id}/data/facetsPer-column facets
GET/v1/workflows/{id}/data/exportExport the extracted-data table

Datasets

?format=json|csv|markdown|html — any non-json format answers rendered text instead of a JSON body.

MethodPathSummary
GET/v1/datasetsThe unified dataset catalogue
GET/v1/datasets/searchGlobal full-text search across every dataset
GET/v1/datasets/{id}Dataset metadata + inferred schema
GET/v1/datasets/{id}/recordsPage through a dataset's records
GET/v1/datasets/{id}/exportDownload a dataset's full records
GET/v1/datasets/{id}/searchFull-text search within one dataset

Crawl

Definitions are saved, callable crawls. POST /v1/crawl/definitions/{ref}/run accepts max_age — a recent-enough previous crawl is reused instead of fetched again.

MethodPathSummary
GET/v1/crawlList crawls
POST/v1/crawlStart a crawl
GET/v1/crawl/{id}Get one crawl
POST/v1/crawl/{id}/cancelRequest cancellation of a crawl
GET/v1/crawl/definitionsList saved crawls
POST/v1/crawl/definitionsSave a crawl configuration
GET/v1/crawl/definitions/{ref}Get one saved crawl
PATCH/v1/crawl/definitions/{ref}Update a saved crawl
DELETE/v1/crawl/definitions/{ref}Delete a saved crawl
POST/v1/crawl/definitions/{ref}/runRun a saved crawl (with optional freshness reuse)
GET/v1/crawl/definitions/{ref}/dataRead what a saved crawl already collected

Keys

Minting requires the wlt_ runtime token.

MethodPathSummary
GET/v1/keysList API keys
POST/v1/keysMint a scoped API key
GET/v1/keys/{id}Get one key record
DELETE/v1/keys/{id}Delete a key record

WebSocket tickets

MethodPathSummary
POST/v1/ws-ticketMint a single-use WebSocket ticket

cloud ▸ metered + keyless

The cloud surface.

Writ Cloud is the hosted, metered surface at https://api.usewrit.app. The spec declares four REST operations there: one-page Scrape with a wt_ key, plus a keyless tier identified only by a device id. Everything else on the cloud host — published endpoints, MCP, webhooks — is documented on its own page.

MethodPathSummary
POST/api/v1/website-to-apiTurn a website into an API
GET/api/v1/website-to-api/{id}Poll a website-to-API build
POST/api/crawl/scrapeScrape one page (metered)
POST/api/crawlStart a whole-site crawl
GET/api/crawl/{id}Poll a crawl
GET/api/targetsList monitors
POST/api/targetsCreate a monitor
GET/api/targets/{id}Get a monitor
PATCH/api/targets/{id}Update a monitor
DELETE/api/targets/{id}Delete a monitor
PATCH/api/targets/{id}/togglePause or resume a monitor
POST/api/targets/{id}/runCheck a monitor now
GET/api/targets/{id}/changesA monitor's change history
GET/api/targets/changes/recentRecent changes across all monitors
POST/v1/keyless/crawlCrawl a few pages (keyless)
POST/v1/keyless/scrapeScrape one page (keyless)
POST/v1/keyless/mapMap a site's URLs (keyless)
GET/v1/keyless/quotaRemaining keyless allowance

Keyless answers 429 keyless_rate_limited when the allowance is spent; metered answers 402 insufficient_credits when the credit pool is empty.

cloud.ts

const cloud = new CloudApi({ apiKey: process.env.WRIT_API_KEY }); // wt_…
const page = await cloud.scrape("https://example.com");
const site = await cloud.map("https://example.com", { search: "pricing", limit: 20 });
console.log(cloud.tier); // "metered" | "keyless"

faq

API questions, answered.

Is this the API the SDKs call?
Yes. The four published SDKs are thin clients over exactly these operations, generated against the same OpenAPI description (writ-agent.yaml). Anything the tables list, the SDKs can do — and plain curl can too.
How do I wait for a run to finish?
POST /v1/workflows/{id}/run returns immediately by default. Add ?wait=true to block until the run settles — timeout is in seconds, clamped to 1–3600, default 120. Or take the run id and subscribe to GET /v1/runs/{id}/events (SSE). A run that fails comes back as a result with its status, not as an HTTP error.
Why did this list return a bare array?
By design. Most lists answer {"data": [...], "count": n} and /v1/runs adds "total"; monitors, selectors, extractors, automations and /v1/changes/recent answer bare arrays. Each shape is stable — read the list you called by its documented envelope.
Which token goes where?
Local daemon: a Bearer token from the wlt_ (runtime), wlk_ (scoped) or wlo_ (OAuth 2.1) family, sent to 127.0.0.1. Writ Cloud: a wt_ Bearer key for metered calls, or the X-Writ-Client-Id header on the keyless routes. Published endpoints are a separate lane, authenticated with csk_ consumer keys.
Does calling the local API cost anything?
No. The local daemon is your own machine — runs, monitors, crawls and data reads there carry no compute charge. Only Writ Cloud calls are metered, from your credit pool.

end ▸ ship

Point something at it.

The quickstart takes you from account to first call in minutes; the SDKs wrap this whole page in typed clients.