Runs onWrit CloudDesktop
On this page
Quickstart
From a recording to a running endpoint.
Writ turns any website into an API your software - and your AI agents - can call. This page covers the core concepts, then walks you from zero to structured JSON on both surfaces: the free local agent on your machine, and the published endpoint on Writ Cloud.
the layer
What Writ is.
Writ is the API and MCP layer for sites that have no API. You author a workflow - a recorded or AI-described sequence of browser actions - and publish it as a managed REST endpoint at /v1/{slug}/{path} and as an MCP tool. From then on, a single HTTP call (or an MCP tool invocation) does the work and returns structured data.
Writ runs on your own accounts, with your own credentials and data, on sites you are authorized to use.
vocabulary
Core concepts.
| Concept | What it is |
|---|---|
| Workflow | A sequence of steps (navigate, fill, click, extract, AI actions, ...) that runs in a real browser. |
| AI session | Describe a goal in natural language and let the AI brain drive the browser to author or run a workflow. |
| Monitor | Watches a page for change as fast as every 10 seconds and can fire a workflow the instant it changes. |
| Persona | A reusable, encrypted login (with TOTP or mailbox OTP) so workflows act on your own authorized accounts. |
| Agent | The browser runner: your local/BYO machine (no compute charge) or the Writ cloud fleet (metered by running time). |
| Managed endpoint | Your published workflow exposed as a REST endpoint and MCP tool at /v1/{slug}/{path}. |
| $ wallet | A prepaid balance. Cloud running time and AI tokens draw from it; local runs are free of compute. |
quickstart
Four steps to your first calls.
You will create an account, author a workflow, run it from code on your own machine, then publish it and call it from anywhere.
1. Create an account and install Writ
Sign up at app.usewrit.app/register, then install the desktop app. It keeps the local agent daemon, writ-agentd, running - the same API surface on http://127.0.0.1:8131 that every SDK targets. The Free tier runs on your own machine and needs no card.
2. Record or describe a workflow
Record a short workflow in a real browser, or describe a goal and let an AI session author it. Either way you end up with a runnable workflow: inputs in, extracted rows out.
3. Run it from code - locally
Install an SDK and run the workflow against the daemon on your own machine. The client discovers the running agent on 127.0.0.1 - no URL, no token to paste:
run.ts
import { WritAgent, runRowId } from "@usewrit/agent-sdk";
const client = new WritAgent(); // discovers the running agent + token
const { data: workflows } = await client.workflows.list();
const run = await client.workflows.runAndWait(workflows[0].id, {
inputs: { city: "Paris" },
});
const { data: rows } = await client.runs.data(runRowId(run));
console.log(run.status, rows); run.py
from writ_agent import WritAgent, run_row_id
with WritAgent() as client: # discovers the local daemon
run = client.workflows.run_and_wait(3, inputs={"city": "Paris"})
print(run["status"], run["rows_extracted"])
print(client.runs.data(run_row_id(run))["data"]) # extracted rows run.go
client, err := writ.Discover(ctx) // find the running agent
page, _ := client.Workflows.List(ctx, nil)
item, _ := client.Workflows.RunAndWait(ctx, page.Data[0].ID, nil)
rowID, _ := item.RowID()
csv, _ := client.Runs.DataCSV(ctx, rowID) // extracted rows as CSV
fmt.Println(item.Status, "
", csv) run.rs
use writ_client::{RunOptions, WritAgent};
let agent = WritAgent::discover().await?; // find the running daemon
let workflows = agent.workflows().list().await?;
let wf = &workflows.data[0];
let outcome = agent.workflows().run_and_wait(wf.id, &RunOptions::default()).await?;
let rows = agent.runs().data(outcome.run.row_id().unwrap()).await?;
println!("{} → {}: {}", wf.name, outcome.run.status, rows.data); Local surface. This code talks to writ-agentd over loopback, authenticated with the local token families wlt_ / wlk_ / wlo_ - not your cloud key. Your local/BYO agent does the browsing, and nothing here is metered.
4. Publish it and call it from software
Publish the workflow as a managed endpoint - publishing gives it a slug and a path on your tenant. From then on, any language, scheduled job or AI agent can call it as plain REST:
call.sh
curl -X POST https://api.usewrit.app/v1/acme/price-check \
-H "Authorization: Bearer $WRIT_CONSUMER_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/product/42"}' call.py
import os, requests
res = requests.post(
"https://api.usewrit.app/v1/acme/price-check",
headers={"Authorization": f"Bearer {os.environ['WRIT_CONSUMER_KEY']}"}, # csk_...
json={"url": "https://example.com/product/42"},
timeout=120,
)
res.raise_for_status()
payload = res.json()
print(payload["run_id"], payload["data"]) call.ts
const res = await fetch("https://api.usewrit.app/v1/acme/price-check", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WRIT_CONSUMER_KEY}`, // csk_...
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com/product/42" }),
});
if (!res.ok) throw new Error(`Writ call failed: ${res.status}`);
const { run_id, data } = await res.json();
console.log(run_id, data); call.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]string{"url": "https://example.com/product/42"})
req, _ := http.NewRequest("POST", "https://api.usewrit.app/v1/acme/price-check", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("WRIT_CONSUMER_KEY")) // csk_...
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
RunID string `json:"run_id"`
Data json.RawMessage `json:"data"`
}
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out.RunID, string(out.Data))
} call.rs
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = std::env::var("WRIT_CONSUMER_KEY")?; // csk_...
let res: Value = reqwest::Client::new()
.post("https://api.usewrit.app/v1/acme/price-check")
.bearer_auth(key)
.json(&json!({ "url": "https://example.com/product/42" }))
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("{} {}", res["run_id"], res["data"]);
Ok(())
} Response
{
"run_id": "run_7Qd2",
"status": "succeeded",
"data": { "price": "$129.00", "in_stock": true }
} That is the canonical endpoint response: a run_id, a status, and your extracted data. A run that fails answers with status: "failed" - a result to read, not an HTTP error.
Cloud surface. The published door answers at https://api.usewrit.app/v1/{slug}/{path} with a wt_ key as the Bearer token - this is the one call in this quickstart that crosses to Writ Cloud. Cloud runs are metered by running time from your $ wallet; route the run to your own local/BYO agent instead and compute stays free. Door mechanics live in managed endpoints.
conventions
Two surfaces, one grammar.
Everything you just did used the same conventions:
| Surface | Base URL | Auth |
|---|---|---|
| Local agent (writ-agentd) | http://127.0.0.1:8131 | Bearer wlt_ · wlk_ · wlo_ |
| Writ Cloud | https://api.usewrit.app | Bearer wt_ · X-Writ-Client-Id header (keyless) |
- JSON in, JSON out. Requests and responses are
application/json. Errors are{ "error": "…", "code": "…" }with a stable code - and a few 4xx paths answer plain text, so tolerate non-JSON when reading errors. - Async by default.
POST /v1/workflows/{id}/runreturns at dispatch; add?wait=trueto block for the result (timeout in seconds, clamped 1-3600, default 120). - A failed run is a result. You get the run back with its status; keep exception handling for transport and auth problems.
- Talk to
127.0.0.1, notlocalhost. The daemon checks Host and Origin against DNS rebinding, and an HTTPS twin listens on:8132.
The endpoint-by-endpoint reference - every method, path, error code and list envelope on both surfaces - is the API reference.
faq
Quickstart questions, answered.
Do I need a credit card to start?
What do the key prefixes mean?
What does a published endpoint return?
Do I have to use the cloud?
what next
Keep going.
- Authentication - keys, scopes, sessions, MFA, OAuth, consumer keys.
- Workflows - the object, the 30+ step types, and how runs work.
- SDKs - the four published clients: TypeScript, Python, Go, Rust.
- API reference - both surfaces, endpoint by endpoint.
- managed endpoints - publishing, input mapping and quotas for the REST door.
- Billing & usage - how cloud runs are metered and how to add funds.