Runs onWrit Cloud
On this page
Any workflow, a REST route.
Publish a workflow — or a saved page-extraction job — and Writ serves it at /v1/{slug}/{path}: no /api prefix, no server of yours to run, and callers hold their own consumer keys, never your credentials.
route ▸ how a call resolves
The route, resolved.
The gateway has no fixed paths of its own: the slug names your tenant, the path matches an endpoint you registered, and anything that does not resolve is a 404.
| Part | How it resolves |
|---|---|
{slug} | Your tenant’s public_id (canonical) or its vanity slug. The gateway also answers on the {slug}.api.usewrit.app subdomain and on verified custom domains. |
{path} | Matched against your registered endpoints on (method, path) — a literal like /products, or a pattern like /search/{query}. |
Methods | GET · POST · PUT · DELETE · PATCH |
Backend | A saved workflow run, or a scrape_job — a saved page-extraction job. |
Miss | 404 — unknown tenant, or no endpoint registered on that (method, path). |
call ▸ post, read data
The first call.
POST the inputs, read the data back — these samples are the whole client:
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(())
} Callers authenticate this lane with a csk_ consumer key you mint per caller; your own wt_ key stays on the /api management surface and never needs to reach them. Mint, cap, suspend and rotate keys under consumer keys.
wait ▸ sync by default
Synchronous by default, async on request.
There is no wait= parameter on this lane. A call runs synchronously up to the endpoint’s timeout_seconds (5–300, default 120) and answers 200 with the result inline; past the budget it answers 504 — still carrying the run handle, so nothing is lost.
200 with the result inline, up to timeout_seconds.
Send Prefer: respond-async (RFC 7240) or ?async=true → 202 plus a run handle.
GET /v1/{slug}/_runs/{run_id} — answers with Retry-After: 2 while the run is non-terminal.
Freshness
Send Cache-Control: max-age=N or ?max_age=N per call. 0 forces a fresh run; when absent, the endpoint’s own cache_ttl_seconds (0–86400) decides.
Control parameters never leak into your workflow: async and max_age are stripped before the remaining query string merges into the run’s inputs.
shape ▸ response_format
One of three envelopes.
Each endpoint chooses how its payload is wrapped:
| response_format | Shape |
|---|---|
raw | The run output, unwrapped. |
json_wrapped | The default — {"success":true,"data":…}. |
with_metadata | {"data":…,"metadata":{endpoint_id,latency_ms,cached,timestamp}}. |
Errors never vary with the format: always {"success":false,"error":…,"detail":…}.
order ▸ the gates
The gate order, exactly.
Every call walks the same ordered checks. Knowing the order tells you which limit you hit and which header to read:
- 01Tenant
Unknown
{slug}→404. - 02Endpoint
No registered endpoint on this (method, path) →
404. - 03Consumer key
Missing or invalid
Bearerconsumer key →401. - 04Per-key rate limit
A sliding 60-second window — the key’s
rate_limit_per_minute, else the endpoint’srate_limit_override, else 60/min. Over it →429withX-RateLimit-*andRetry-After: 60. - 05Daily fair-use
An org-wide daily ceiling on relay calls to published endpoints — 2,000/day on Free up to 250,000/day on Enterprise. Over it →
429withRetry-After: 3600. - 06Per-key monthly quota
The key’s
monthly_quota, counted before dispatch — over it →429“Used {n}/{quota} calls this month”. - 07Org monthly quota
Your plan’s monthly managed-API call quota (
managed_api_calls_per_month). - 08Cache
A cached result younger than the allowed age returns here, without starting a run.
- 09Dispatch
The workflow — or saved extraction job — runs on its configured venue.
- 10Usage
The call lands in per-key and per-endpoint usage analytics.
Quotas by plan
Published routes are capped as objects; calls are capped monthly per org and daily as fair use. Unknown-key and 404 traffic never counts against you:
| Plan | Published endpoints | Calls · month | Relay calls · day |
|---|---|---|---|
| Free | 2 | 10,000 | 2,000 |
| Starter | 5 | 50,000 | 10,000 |
| Pro | 15 | 250,000 | 25,000 |
| Growth | 40 | 1,000,000 | 50,000 |
| Scale | 100 | Unlimited | 100,000 |
| Enterprise | Unlimited | Unlimited | 250,000 |
faq
Questions, answered.
What is the difference between an endpoint and an MCP tool?
Which key do callers use?
What happens when a run outlives the timeout?
Where does the run execute?
go ▸ publish
Publish your first endpoint.
Pick a workflow, register a route, and hand your callers a URL that answers in one POST.