For sites and accounts you are authorized to access.
Any website. Behind any login. Callable.
Most of the web has no API — and the part that matters to your company has no API and a login. Record the task once. Writ hands you back an endpoint.
Watch Writ record a workflow — then take over.
Sign-in, 2FA, extraction, download — on a demo portal. Click anywhere in the page to take over.
You just made a site callable. Now do it with yours.
- 9 actions recorded
- 4 steps after optimization
- 1 callable endpoint
This demo’s workflow is not saved: it lives in this page. No credit card.
New workflow
Set up what gets recorded
Run typeEditRun it, get a result, then stop. Ideal for an extraction, a form submission or a data pull — which is what most workflows need.
Entry URLThe page where recording will start.
Run onChoose where the browser runs. Local agents use your own machine’s browser and network.
Portal sign-in
Client area · billing
Two-step verification
Enter the 6-digit code from your app
Invoices
Client area · Acme Supply
/ INV-2291
INV-2291
Due 12 Jul 2026 · Overdue
Payments
Settlements over the last 12 months
Settings
Billing preferences
Requests appear as you browse.
Proposed changes (3) — review before applying
The form posts to /api/client/session — one call authenticates, with no DOM driving.
The page reads /api/client/invoices; calling the JSON is faster and does not break when the layout changes.
The API call does not need the intermediate page.
This login uses 2FA. A persona signs in and enters the codes automatically on every run.
Ask AI for anything — extract data, find an element, build an extractor. It drives the browser and adds the step for you.
To record your site, you need Writ.
This demo replays an already-recorded session. To record your own site — including behind a login — open a real Writ browser.
Unlimited runs on your own machine · no credit card
No step type matches.
Endpoint
POST https://api.usewrit.app/api/v1/workflows/353/runs curl -X POST "https://api.usewrit.app/api/v1/workflows/353/runs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# → 200 {"success": true, "data": {...}} — blocks up to 120 s for the result.
# Add ?wait=false for {"task_id": ...} right away, then GET the same URL + /{task_id}. Quick edits — reorder, rename or delete. For anything visual, edit in the recorder.
Run on
This workflow signs in / uses credentials, so on Writ Cloud it runs ISOLATED — a fresh sandboxed browser process, destroyed after every run. Stronger isolation; a small surcharge. (Runs on your own agent are unaffected.)
Run it once to confirm everything works.
Run a testStart with what you need
Start in one line.
Pick what you want to start — record and call it, watch a page, or crawl a site — then pick your language. Each one is a single call, through the same door: plain HTTP, one line for your assistant, or a published SDK.
One call turns a site into an API. Writ checks your own workflows and ready-made listings first — both free — then its agent drives a real browser and saves the workflow. Poll the build id; what you get back has its own endpoint.
# One call. Writ's agent drives the browser and saves the workflow.
curl -X POST https://api.usewrit.app/api/v1/website-to-api \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.example",
"goal": "return a SKU'"'"'s price and stock"}' { "build_id": 2291,
"status": "queued",
"goal": "return a SKU's price and stock" }The slug exists because you published that workflow, and the key is a consumer key minted with it — not your account key.
One line, and the site becomes an API: say what it should return and Writ opens a real browser, works out the calls behind the page, and saves the result as a workflow.
claude mcp add writ-cloud \
-e WRIT_API_KEY=wt_your_api_key \
-- npx -y writ-mcp
# then, one line in the session:
writ_website_to_api({
url: "https://acme.example",
goal: "return a SKU's price and stock",
}) Config-file clients — Claude Desktop · Cursor · Windsurf
The saved workflow becomes its own run_<name> tool, replays at zero AI cost, and can be scheduled. Prefer REST? The cURL tab is the same start as POST /api/v1/website-to-api.
npm install github:usewrit/writ-sdks --workspace typescript
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); These drive the LOCAL daemon on your own machine, where runs are free and unmetered. The cURL tab above is the published cloud endpoint.
pip install "git+https://github.com/usewrit/writ-sdks#subdirectory=python"
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 These drive the LOCAL daemon on your own machine, where runs are free and unmetered. The cURL tab above is the published cloud endpoint.
go get github.com/usewrit/writ-sdks/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) These drive the LOCAL daemon on your own machine, where runs are free and unmetered. The cURL tab above is the published cloud endpoint.
cargo add writ-client --git https://github.com/usewrit/writ-sdks
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); These drive the LOCAL daemon on your own machine, where runs are free and unmetered. The cURL tab above is the published cloud endpoint.
A monitor is one POST with a key. No install, no agent to run — Writ checks the page on your cadence and records every change.
# Watch a page on Writ Cloud — no install, just the key.
curl -X POST https://api.usewrit.app/api/targets \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com",
"check_type": "content",
"selector": "h1",
"check_period_ms": 300000}' A check interval below your plan’s minimum is refused outright, naming the floor — it is never quietly slowed down.
Point your assistant at Writ and it can create and wire monitors itself — watch a page, then run something when it moves.
claude mcp add writ-cloud \
-e WRIT_API_KEY=wt_your_api_key \
-- npx -y writ-mcp
# then, one line in the session:
writ_create_monitor({ url: "https://example.com",
selector: "h1", interval_minutes: 5 })
writ_wire_monitor({ monitor_id: 47, action: "notify",
channels: ["email"] }) Config-file clients — Claude Desktop · Cursor · Windsurf
The same monitors appear in the app, the API and the SDKs; the venue changes, the monitor does not.
npm install github:usewrit/writ-sdks --workspace typescript
import { CloudApi } from "@usewrit/agent-sdk";
const cloud = new CloudApi({ apiKey: process.env.WRIT_API_KEY }); // wt_…
const mon = await cloud.monitors.create({
url: "https://example.com",
selector: "h1",
check_period_ms: 300_000,
});
const changes = await cloud.monitors.changes(mon.id, { limit: 50 }); pip install "git+https://github.com/usewrit/writ-sdks#subdirectory=python"
from writ_agent import Cloud
cloud = Cloud(api_key=os.environ["WRIT_API_KEY"]) # wt_… — no daemon needed
mon = cloud.monitors.create({
"url": "https://example.com",
"selector": "h1",
"check_period_ms": 300_000,
})
changes = cloud.monitors.changes(mon["id"], limit=50) go get github.com/usewrit/writ-sdks/go
client := writ.New(writ.WithAPIKey(os.Getenv("WRIT_API_KEY")))
mon, err := client.Cloud.Monitors.Create(ctx, writ.CloudMonitorParams{
URL: "https://example.com",
Selector: writ.Ptr("h1"),
CheckPeriodMs: writ.Ptr(int64(300_000)),
})
changes, _ := client.Cloud.Monitors.Changes(ctx, mon.ID, writ.Ptr(50)) cargo add writ-client --git https://github.com/usewrit/writ-sdks
use serde_json::json;
use writ_client::CloudClient;
let cloud = CloudClient::from_env()?; // reads WRIT_API_KEY
let mon = cloud.monitors().create(json!({
"url": "https://example.com",
"selector": "h1",
"check_period_ms": 300_000
})).await?;
let changes = cloud.monitors().changes(mon.id, Some(50)).await?; A whole-site crawl is one POST with a key. Writ fans the seed across the fleet and lands the result as a callable dataset.
# No account, no key — a few same-domain pages, one level deep.
curl -X POST https://api.usewrit.app/v1/keyless/crawl \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "limit": 3}'
# With a key: the whole site, as a job you poll.
curl -X POST https://api.usewrit.app/api/crawl \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "max_depth": 3, "page_budget": 500}'
curl https://api.usewrit.app/api/crawl/8125 -H "Authorization: Bearer $WRIT_API_KEY" No account needed to try it: the keyless tier crawls a few same-domain pages, one level deep, against a daily allowance. Whole-site scope, logins and a callable dataset need a key.
Point your assistant at Writ and it can crawl a site directly, then read the rows back without you writing a parser.
claude mcp add writ-cloud \
-e WRIT_API_KEY=wt_your_api_key \
-- npx -y writ-mcp
# then, one line in the session:
writ_crawl_site({ url: "https://example.com",
intent: "product pages with specs",
page_budget: 500, save_as: "example-catalog" })
writ_crawl_status({ crawl_id: 8125 }) Config-file clients — Claude Desktop · Cursor · Windsurf
Pass save_as and the crawl becomes re-runnable and callable, so later runs can reuse recent data instead of re-crawling.
npm install github:usewrit/writ-sdks --workspace typescript
import { CloudApi } from "@usewrit/agent-sdk";
const cloud = new CloudApi({ apiKey: process.env.WRIT_API_KEY });
const job = await cloud.crawl({ url: "https://example.com", max_depth: 3, page_budget: 500 });
const status = await cloud.crawlStatus(job.id);
console.log(status.pages_done, "/", status.pages_discovered); pip install "git+https://github.com/usewrit/writ-sdks#subdirectory=python"
from writ_agent import Cloud
cloud = Cloud(api_key=os.environ["WRIT_API_KEY"])
job = cloud.crawl("https://example.com", max_depth=3, page_budget=500)
status = cloud.crawl_status(job["id"])
print(status["pages_done"], "/", status["pages_discovered"]) go get github.com/usewrit/writ-sdks/go
client := writ.New(writ.WithAPIKey(os.Getenv("WRIT_API_KEY")))
job, err := client.Cloud.Crawl(ctx, writ.CrawlStartParams{
URL: "https://example.com",
MaxDepth: writ.Ptr(int64(3)),
PageBudget: writ.Ptr(int64(500)),
})
status, _ := client.Cloud.CrawlStatus(ctx, job.ID) cargo add writ-client --git https://github.com/usewrit/writ-sdks
use writ_client::{CloudClient, CrawlStartParams};
let cloud = CloudClient::from_env()?;
let job = cloud.crawl(&CrawlStartParams {
url: "https://example.com".into(),
max_depth: Some(3),
page_budget: Some(500),
..Default::default()
}).await?;
let status = cloud.crawl_status(job.id).await?; It signs in as you — including 2FA.
A persona holds the credentials, the fingerprint, the egress route and a session that survives between runs. Use personas only with sites and accounts you are authorized to access.
hunter2-prod-9f {{secret:password}} substituted at execution, never in a transcript sid=8f3a…c21 cookie: [redacted] redacted in every captured header Stored encrypted · resolved at run · never returned in clear.
One row. Five doors.
The moment it saves it is already a REST endpoint, an MCP tool, an OpenAI-compatible endpoint, a signed webhook and a scheduled job.
curl -X POST "https://api.usewrit.app/api/v1/workflows/353/runs" \
-H "Authorization: Bearer wt_…" \
-H "Content-Type: application/json" -d '{}'{
"mcpServers": {
"writ-cloud": {
"type": "http",
"url": "https://api.usewrit.app/mcp"
}
}
}POST https://api.usewrit.app/hooks/{path}
X-Writ-Signature: sha256=…0 7 * * 1-5 → workflow 353# chat with a live streaming session —
# your handler script answers, not a model
base_url = "https://api.usewrit.app/v1"
model = "writ/acme-support-chat" Same row, five ways in. The interface stays the same; its URL depends on where the workflow runs — locally, through Uplink, self-hosted or in Writ Cloud. Publishing exposes the workflow; Automations decide when and why it runs.
Two machines. Each reaches what the other can’t.
Your machine is already inside your network. Ours is always on. Link them and the cloud becomes the front door for both.
What this required from your firewall: no inbound rule · no public hostname · no port opened
Nothing dials in. The agent holds one outbound connection, and the call rides back down it. There is no inbound rule to write and no port to open.
Your own machine — free, no account, runs while it’s awake; only the result leaves.
The agent dials out. Nothing dials in — a system behind your VPN becomes callable without opening a single inbound port. Local callers can stay entirely local; add Uplink only when a remote caller needs to reach a workflow behind your firewall.
Our open-source build cannot bill you — the billing code is not compiled into it.
Built with AI. Run without it.
Building is where the intelligence goes, and it costs what it costs — once. After that there is no model in the loop.
- a step fails: no valid selector
- the failing step + the live DOM go out
- the candidate is validated against the live page — or rejected
- the run completes
Auto-repair is opt-in, off by default, and a cloud capability — turning it on pins that workflow to the cloud.
A change is a trigger, not just an alert.
Point a monitor at a value. When it moves, that is an event — and the event runs a workflow.
The reaction starts from the persona’s warm session — no cold login. If the action fails, Writ logs the run and applies the retry policy you configured.
A whole site, as a dataset.
Describe what you want in plain language. Writ compiles it into a scope, crawls it, and hands you a dataset that is itself callable.
Without a session the frontier dead-ends at /login. Everything past it is undiscoverable — not slow to reach, undiscoverable: those URLs appear in no sitemap and nothing links to them from the public site.
Illustrative topology. A real Harvest runs inside the scope you set — includes, excludes and rate caps — and respects robots.txt.
Read the code, then read the limits.
Everything above is a claim until you can check it. So here is the code that backs it, the guarantees that hold because of how it is built, and the things Writ does not do.
The engine that runs your workflows is public. You can read it, run it, and check that it does what this page says.
Also public: changelog · status · docs · trust center
Not promises — consequences of how it is built. Each one names its mechanism.
- A run on your own machine cannot be billed: the billing code is not compiled into the open-source build.
- An intranet system needs no inbound port: the agent holds an outbound connection, and only the workflow’s result travels back.
- A recorded workflow replays without a model: the steps are already decided, so a replay spends no AI tokens at all.
The edges, stated plainly. If one of these is your case, you will know before you sign up rather than after.
- It does not defeat access controls. It signs in as you, with your credentials, on sites you are authorized to use.
- It is not a general web index. You point it at sites you choose; nothing is collected in the background.
- Managed cloud runs in one region today — OVHcloud in Canada. We will name others when they exist.
One pool. Any mix.
Cloud runs are metered by runtime and spend one credit pool — on compute, premium egress or AI, in whatever mix you choose.
Runs on your own machine are free and unmetered, on every plan. When the pool runs dry, cloud runs pause until you top up — local never stops. Prices in USD, billed monthly. Example: one pool can be spent on browser runtime, premium egress or optional AI repair — local runs use no credits at all.
Questions people actually ask
Is it really free?
How is this different from a normal web integration?
Does my data leave my computer?
Can I use my own AI keys?
What happens when a site changes?
Is this allowed?
Where do my credentials live?
Can I take my workflows elsewhere?
Does Writ bypass 2FA?
What happens when my credit pool runs out?
What exactly does Writ Cloud receive?
Can I delete my stored credentials and sessions?
Turn your first website into an endpoint.
Free to start. Unlimited on your own machine, forever.