For developers and agents
Établi API docs
Physical work from one API call. Send the work in plain English; Établi plans it into legs, one for each kind of workshop it needs, and independent workshops bid on each. Start with no key: one call returns the plan, and one more drafts it for the person to claim and publish.
- Base URL
https://api.etabli.io- No key needed
POST /v1/quotes/previewplans a request;POST /v1/projectsdrafts it to claim- MCP server
https://api.etabli.io/mcp(install)- For agents
- llms.txt · reference.md · OpenAPI
Quickstart: a plan, with no key
Send the work in plain English: what it is, how many, where it goes, by when, and the most the person would pay. Établi returns the plan: the legs, each with its spec and the questions still open. No key, no account, and nothing is ordered or sent to workshops.
curl -sX POST https://api.etabli.io/v1/quotes/preview \ -H "Content-Type: application/json" \ -d '{"request":"Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box."}'const response = await fetch("https://api.etabli.io/v1/quotes/preview", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request: "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", }),});const plan = await response.json();for (const leg of plan.legs) { console.log(leg.key, leg.title, `${leg.questions.length} open questions`);}import requests plan = requests.post( "https://api.etabli.io/v1/quotes/preview", json={"request": "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box."}, timeout=120,).json()for leg in plan["legs"]: print(leg["key"], leg["title"], len(leg["questions"]), "open questions")What comes back, from a real run of that request, abridged:
{ "title": "3,000 one-color logo mailer boxes, 3 warehouses", "summary": "Produce 3,000 custom corrugated mailer boxes printed with the buyer's logo in one color, delivered split across warehouses in New Jersey, Texas and Nevada within 4 weeks, at up to $1.20 per box.", "legs": [ { "key": "boxes", "title": "3,000 one-color printed mailer boxes", "role": "Manufacture and print the logo mailer boxes and deliver them split across the buyer's three warehouses.", "spec": { "price": { "ceilingCents": 120, "per": "unit", "includes": [ "boxes and board", "one-color printing", "printing plates, cutting die and setup", "digital proof", "packing flat on pallets or in cartons", "freight to all three warehouses" ], "excludes": [] }, "deadline": { "kind": "fixed_date", "businessDays": null, "date": "2026-11-06" }, "…": "work, output, proof, where, rules" }, "questions": [ { "field": "work", "text": "What inside dimensions, board grade/flute and board color should the boxes be?", "suggestedAnswer": "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." } ], "inputsFrom": [], "sourcing": { "countries": [ "US" ], "…": "lookFor, searchTerms" } } ], "engine": "claude:claude-opus-5-5"}Each leg's spec is the contract in eight fields: inputs, work, output, price (the most the buyer pays, in ceilingCents), deadline, proof, where and rules. A plan has no price of Établi's own: prices come from workshops' bids. The packaging guide shows this plan in full.
Answer the open questions
Ask the person each question; each comes with a suggested answer they can take. Then preview again with answers: each with its leg's key as leg, and the question's field and text exactly as returned.
curl -sX POST https://api.etabli.io/v1/quotes/preview \ -H "Content-Type: application/json" \ -d '{ "request": "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", "answers": [ { "leg": "boxes", "field": "work", "text": "What inside dimensions, board grade/flute and board color should the boxes be?", "answer": "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." }, { "leg": "boxes", "field": "rules.notes", "text": "Should the logo print on the outside only, and in which Pantone color?", "answer": "Outside only, lid top, Pantone Black C." } ] }'Draft and claim: the person publishes
When the person wants workshops to bid, draft the project: POST /v1/projects with no key, the same request and answers. The draft belongs to nobody until the person claims it on etabli.io, where they check the plan and its price ceilings, answer anything still open, and press Claim and publish. Nothing reaches a workshop, and nothing is charged, until they do.
curl -sX POST https://api.etabli.io/v1/projects \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "request": "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", "answers": [ { "leg": "boxes", "field": "work", "text": "What inside dimensions, board grade/flute and board color should the boxes be?", "answer": "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." }, { "leg": "boxes", "field": "rules.notes", "text": "Should the logo print on the outside only, and in which Pantone color?", "answer": "Outside only, lid top, Pantone Black C." } ], "agent": { "name": "your-agent" } }'const response = await fetch("https://api.etabli.io/v1/projects", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), // a retry with the same key returns the same draft }, body: JSON.stringify({ request: "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", answers: [ { leg: "boxes", field: "work", text: "What inside dimensions, board grade/flute and board color should the boxes be?", answer: "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." }, { leg: "boxes", field: "rules.notes", text: "Should the logo print on the outside only, and in which Pantone color?", answer: "Outside only, lid top, Pantone Black C." }, ], agent: { name: "your-agent" }, }),});const { project, projectKey } = await response.json();console.log(project.next.message); // show the person this line, word for word// Keep projectKey (shown once): it reads this project later.import uuid, requests draft = requests.post( "https://api.etabli.io/v1/projects", headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "request": "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", "answers": [ { "leg": "boxes", "field": "work", "text": "What inside dimensions, board grade/flute and board color should the boxes be?", "answer": "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." }, { "leg": "boxes", "field": "rules.notes", "text": "Should the logo print on the outside only, and in which Pantone color?", "answer": "Outside only, lid top, Pantone Black C." }, ], "agent": {"name": "your-agent"}, }, timeout=120,).json()print(draft["project"]["next"]["message"]) # show the person this line, word for wordproject_key = draft["projectKey"] # keep it: shown onceIdempotency-Key, any unique string: a retry with the same key within 24 hours returns the same draft, project key and code. 10 drafts an hour and 30 a day per address. Send contact (the person's name and email, to prefill the claim page) only if they agree.It responds 201 with the project, the project key and the claim link. Abridged:
{ "project": { "id": "prj_01k7…", "buyerId": null, "status": "ready", "claim": { "status": "unclaimed", "expiresAt": "… (7 days)", "claimedAt": null }, "draftedBy": { "name": "your-agent", "version": null }, "next": { "action": "show_claim_link", "message": "Show the person this line: To check and publish your project, open https://etabli.io/claim#KQ7M-4TRD (or go to etabli.io/claim and enter KQ7M-4TRD) within 15 minutes. Nothing goes to workshops until they do.", "url": "https://etabli.io/claim#KQ7M-4TRD", "checkAfter": "… (10 minutes)" }, "legs": [ { "key": "boxes", "tenderId": "tnd_…1", "status": "ready", "…": "spec, questions" } ] }, "projectKey": "etb_live_prj_…", "claim": { "url": "https://etabli.io/claim#KQ7M-4TRD", "code": "KQ7M-4TRD", "codeExpiresAt": "… (15 minutes)" }}Show the person `project.next.message` word for word, on its own line. It carries the claim link (https://etabli.io/claim#KQ7M-4TRD): the code rides in the fragment, lasts 15 minutes and works once. Only the person can claim: the API refuses a claim sent with a key.
The project key
projectKey(etb_live_prj_…) is shown once. Keep it out of code and chats: in an environment variable, or a git-ignored file.- With it, as
Authorization: Bearer:GET /v1/projects/{id}and its events; answering its legs' questions before the claim (POST /v1/tenders/{tenderId}/answers); a fresh claim code; discarding the draft (DELETE /v1/projects/{id}). After the claim: its offers, and answering workshops' questions if the person allowed it. - Publishing, awarding and ordering are the person's. Before the claim they answer 403
claim_required, with the claim page indetails.claimUrl; after it, Établi contacts the person to confirm the offers they choose and arrange payment. - The person can revoke the key on their status page. From then on it answers 401.
Claim codes
A code is eight letters and digits, like KQ7M-4TRD, read in any case, with or without the dash. It lasts 15 minutes, or up to 24 hours if you ask (ttlMinutes, for a run nobody is watching), and a new code voids the old one. The person can also type it at etabli.io/claim. Five wrong codes from one address lock it out for 15 minutes. A draft nobody claims lapses after 7 days.
What to do next, and when to look again
Every project response carries next: {action, message, url, checkAfter}. message is one or two sentences to repeat to the person. While a claim or bids are pending, the Retry-After header (in seconds) mirrors checkAfter: look again then, not sooner. Bids take days, so tell the person, and say they'll get a status link to bookmark when they claim.
| `next.action` | Where it stands | What to do |
|---|---|---|
show_claim_link | Drafted, not yet opened | Show the person the message; if the code lapsed, get a fresh one |
answer_questions | A leg needs answers before it can go out to bid | Ask the person, or show them the claim link: they can answer there |
wait_for_claim | The person has the claim page open | Wait: nothing goes out until they publish |
wait_for_bids | Published; bidding is open | Look again after checkAfter |
review_offers | Bidding has closed | Tell the person: Établi contacts them about the offers |
awarded | Every leg has a contract | Établi arranges payment and the first orders with the person |
expired, discarded | The draft lapsed, or was discarded | Draft again if the person still wants it |
Following it
# Where it stands: `next` says what to do; Retry-After says when to look again (-i shows it).curl -si https://api.etabli.io/v1/projects/$PROJECT_ID -H "Authorization: Bearer $ETABLI_PROJECT_KEY" # What's happened since the last event you saw (oldest first).curl -s "https://api.etabli.io/v1/projects/$PROJECT_ID/events?after=$LAST_EVENT_ID" \ -H "Authorization: Bearer $ETABLI_PROJECT_KEY" # The code lapsed before the person opened it: a fresh one (the old one stops working).curl -sX POST https://api.etabli.io/v1/projects/$PROJECT_ID/claim-codes \ -H "Authorization: Bearer $ETABLI_PROJECT_KEY" -H "Content-Type: application/json" \ -d '{"ttlMinutes": 60}'Events are in Établi's own words (drafted, claimed, published, a bid, a workshop's question, a contract), oldest first: pass the last id you've seen as after. GET /v1/projects/{id}/offers opens to the project key once the person has claimed.
Firm prices by email, instead
Without a claim page: send the same request and answers with the person's name and email to POST /v1/quote-requests. Établi puts each leg out to bid and emails them the workshops' offers. Nothing is ordered until they accept one.
Ask before you send it. This shares the person's name and email with Établi. Call it only once they have said yes.
# Only after the person has said yes to sharing their name and email.curl -sX POST https://api.etabli.io/v1/quote-requests \ -H "Content-Type: application/json" \ -d '{ "request": "Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.", "answers": [ { "leg": "boxes", "field": "work", "text": "What inside dimensions, board grade/flute and board color should the boxes be?", "answer": "9 x 6 x 3 in inside, 32 ECT E-flute, kraft board." }, { "leg": "boxes", "field": "rules.notes", "text": "Should the logo print on the outside only, and in which Pantone color?", "answer": "Outside only, lid top, Pantone Black C." } ], "name": "Their name", "email": "them@example.com" }'201 with {ok, id, plan}: id is the request's reference, to give the person.If no workshop on Établi fits a leg yet, Établi finds suitable ones and asks them to bid, which can take a few days. A person at Établi approves every message before it goes out.
Authentication
- The preview, drafts (
POST /v1/projectswithout a key), firm-price requests, support requests and workshop applications need no key. They are rate-limited per IP address; a 429 says how long to wait inRetry-After. - A draft's project key (
etb_live_prj_…) works on that one project, and so does the person's owner token (etb_live_own_…, in their status link). Both go inAuthorization: Bearer. - Everything else takes an account's key:
Authorization: Bearer etb_live_…, for a buyer, a workshop or an operator. - An agent working for one person needs no key: it drafts, and the person claims. Buyer keys are for companies that run the whole loop through the API themselves: request API access.
- A key is shown once. Keep it out of code and prompts: read it from an environment variable such as
ETABLI_API_KEY.
With a key: from a request to orders
The same request, as a project you run yourself:
# 1. Plan the request into legs; each leg is a tender with a spec and questions.curl -sX POST https://api.etabli.io/v1/projects \ -H "Authorization: Bearer $ETABLI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"request":"Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box."}' # 2. Answer each leg's questions (or take the suggested answers).curl -sX POST https://api.etabli.io/v1/tenders/$TENDER_ID/answers \ -H "Authorization: Bearer $ETABLI_API_KEY" -H "Content-Type: application/json" \ -d '{"acceptSuggested": true}' # 3. Publish: every ready leg goes out to bid.curl -sX POST https://api.etabli.io/v1/projects/$PROJECT_ID/publish -H "Authorization: Bearer $ETABLI_API_KEY" # 4. Offers per leg, all-in (Établi's fee included).curl -s https://api.etabli.io/v1/projects/$PROJECT_ID/offers -H "Authorization: Bearer $ETABLI_API_KEY" # 5. Award: one contract per leg. Ask the person first: this commits real money.curl -sX POST https://api.etabli.io/v1/projects/$PROJECT_ID/award \ -H "Authorization: Bearer $ETABLI_API_KEY" -H "Content-Type: application/json" -d '{}'Place orders
Each unit of work is one call: a customer's purchase, a recipient's parcel, a batch to one address, or a lot for a leg that supplies another (with no address, it ships to the next leg's workshop). Send an Idempotency-Key, so a retry returns the first order instead of placing a second.
# One customer's order, shipped to them.curl -sX POST https://api.etabli.io/v1/contracts/$CONTRACT_ID/orders \ -H "Authorization: Bearer $ETABLI_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042" \ -d '{ "quantity": 1, "reference": "order-1042", "address": { "name": "Sam Rivera", "line1": "100 Main St", "city": "Springfield", "region": "IL", "postalCode": "62701" } }'// One customer's order, shipped to them.const order = await fetch(`https://api.etabli.io/v1/contracts/${contractId}/orders`, { method: "POST", headers: { Authorization: `Bearer ${process.env.ETABLI_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "order-1042", // a retry never orders twice }, body: JSON.stringify({ "quantity": 1, "reference": "order-1042", "address": { "name": "Sam Rivera", "line1": "100 Main St", "city": "Springfield", "region": "IL", "postalCode": "62701" } }),}).then((response) => response.json());Then GET /v1/orders/{id} for its status and proof (photos, a tracking number), and GET /v1/contracts/{id}/statement?week= for a week's orders and charges.
Awarding a contract and placing an order commit real money. An agent confirms each with the person first.
Endpoints
The ones a buyer's agent uses. Every endpoint, with its parameters, body and responses, is in the API reference (markdown, generated from the OpenAPI document), and in the interactive reference.
| Endpoint | Key | Does |
|---|---|---|
POST /v1/quotes/preview | none | The plan: legs, each with its spec and open questions. Nothing is ordered. |
POST /v1/quote-requests | none | Firm prices by email: the request, answers, name and email. Ask the person first. |
POST /v1/projects | none, or buyer | Without a key, a draft for the person to claim: a project key and a claim link. With a buyer key, a project you run yourself. |
GET /v1/projects/{id} | project key, owner token or buyer | A project, with each leg's status, questions and contract, and next. |
GET /v1/projects/{id}/events | project key, owner token or buyer | What happened, oldest first; after for what's new. |
POST /v1/projects/{id}/claim-codes | project key | A fresh claim code and line to show; the old code stops working. |
DELETE /v1/projects/{id} | project key | Discard a draft the person doesn't want. |
POST /v1/tenders/{id}/answers | project key or buyer | Answer a leg's questions, or acceptSuggested. With a project key, before the claim. |
POST /v1/projects/{id}/publish | buyer | Put every ready leg out to bid. |
GET /v1/projects/{id}/offers | buyer, owner token, or project key once claimed | Offers per leg at all-in prices, and the recommended ones added up per finished unit. |
POST /v1/projects/{id}/award | buyer | One contract per leg; an empty body takes the recommendation. |
POST /v1/tenders | buyer | One job for one workshop: a single tender instead of a project. |
POST /v1/contracts/{id}/orders | buyer | Place an order: per customer, recipient, batch or lot. Takes Idempotency-Key. |
GET /v1/orders/{id} | buyer | An order and its proof. |
POST /v1/orders/{id}/cancel | buyer | Cancel an order before it ships or is done. |
GET /v1/contracts/{id}/statement | buyer | A week's orders, charges and fees. |
GET /v1/me | any | The account behind the key. |
POST /v1/support-requests | none | Write to Établi; there is no email address. |
Workshops bid with their own key or through the bid link Établi sends them, and operators run sourcing and outreach; their endpoints are in the reference too.
Conventions
- JSON in and out, with camelCase fields. Times are ISO 8601; responses are in UTC.
- IDs are prefixed:
prj_(project),tnd_(tender: one per leg),bid_,ctr_(contract),ord_(order). - Money is integer US cents:
buyerUnitPriceCents: 1250is $12.50. Prices on offers are all-in: Établi's fee is included. - Lists come newest first. Resources you can't see answer 404, never 403.
- Errors are
{"error": {"code", "message", "details"}}, with the status:
| Status | Code | When |
|---|---|---|
| 400 | validation | The body, query or a header fails its schema, or a rule (publishing with open questions) |
| 401 | unauthorized | No key where one is needed, or an unknown or revoked key |
| 403 | forbidden | The wrong kind of account or key for this action |
| 403 | claim_required | A draft's project key tried to publish, award or order: the person claims it first (details.claimUrl) |
| 404 | not_found | It doesn't exist, or it isn't yours |
| 409 | conflict, invalid_transition, no_eligible_bids | Not possible in its current state |
| 422 | policy_violation, above_ceiling | Work Établi won't take; a bid above the buyer's ceiling |
| 429 | rate_limited | Too many requests from one address: wait for Retry-After |
| 503 | spec_engine_unavailable | Planning is unavailable for a moment: retry |
MCP
Établi's tools for assistants and agents, over Streamable HTTP. The MCP page has every client and every tool.
| Endpoint | Key | Tools |
|---|---|---|
https://api.etabli.io/mcp | none | how_etabli_works, preview_quote, create_project (a draft for the person to claim), get_project, request_firm_prices; with a buyer key, read-only status. Nothing that spends money. |
https://api.etabli.io/mcp/full | buyer | The whole loop: post a project, publish, award, place orders. |
Claude Code
claude mcp add --transport http etabli https://api.etabli.io/mcpCodex
codex mcp add etabli --url https://api.etabli.io/mcpOr add it to
~/.codex/config.toml, which the Codex CLI, the IDE extension and the ChatGPT desktop app share:~/.codex/config.toml
[mcp_servers.etabli]url = "https://api.etabli.io/mcp"Cursor
Add to CursorOr open cursor.com/install-mcp in a browser.
VS Code (Copilot)
Add to VS CodeOr run
code --add-mcp '{"name":"etabli","type":"http","url":"https://api.etabli.io/mcp"}'.Gemini CLI
gemini mcp add --transport http etabli https://api.etabli.io/mcpSkill (70+ agents)
npx skills add https://etabli.ioInstalls Établi's skill,
order-physical-work, into the agents on your machine: when to use Établi and how to order with it.
From the command line
The etabli CLI and TypeScript SDK aren't on npm yet. Until they are, use HTTP: from a shell, curl and jq cover the whole flow.
REQUEST='Print 3,000 mailer boxes with our logo in one color, split across our warehouses in New Jersey, Texas and Nevada, within 4 weeks. Up to $1.20 a box.' curl -sX POST https://api.etabli.io/v1/quotes/preview \ -H "Content-Type: application/json" \ -d "$(jq -n --arg request "$REQUEST" '{request: $request}')" \ | jq '.legs[] | {key, title, questions: [.questions[] | {text, suggestedAnswer}]}' # Draft it for the person to claim, and print the line to show them. The saved# response holds the project key: keep the file out of git.curl -sX POST https://api.etabli.io/v1/projects \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d "$(jq -n --arg request "$REQUEST" '{request: $request}')" \ | tee .etabli-draft.json | jq -r '.project.next.message'To set up the agents on your machine, npx skills add https://etabli.io installs Établi's skill, and each agent's own command adds the MCP server (every client).
Good to know
- Orders go to US addresses and are priced in US dollars. Workshops can be in any country a leg allows.
- Établi has no price list: prices come from workshops' bids, and a request can say the most the person would pay, which caps the bids. Pricing.
- Online payment isn't live yet: while Établi is in early access, it arranges payment with each buyer and workshop directly.
- Établi won't take work that is illegal or unsafe: the API answers with 422
policy_violationand the reason. - Guides with a real plan each: houseplants on demand, packaging split across warehouses, manufacturing, an API for AI agents, three ways agents buy physical goods.
- For agents: llms.txt, and everything in one file at llms-full.txt. Questions: support.