# For coding agents (https://trywool.com/docs/agents) If a coding agent is writing your integration, it needs the same things you do, in a form it can read whole. Every page here is self-contained and served as plain Markdown. | What | Where | | ---------------------- | ------------------------------------------------------------------------------------------------- | | Index of every page | [`/llms.txt`](/llms.txt) | | Every page in one file | [`/llms-full.txt`](/llms-full.txt) | | Any page as Markdown | Its URL with `.md` appended, for example `/docs/quickstart.md` | | The API contract | [`https://api.trywool.com/v1/openapi.yaml`](https://api.trywool.com/v1/openapi.yaml), OpenAPI 3.1 | The contract is the source of truth for routes, fields, statuses, and error codes. Generate a client from it rather than transcribing the reference. ## A block for your repository [#a-block-for-your-repository] Paste this into the `AGENTS.md` or `CLAUDE.md` of a project that integrates WOOL: ```markdown ## WOOL - WOOL runs agents on a user's own website accounts, from prompts, after the user authorizes the app on a WOOL connect page. Docs: https://trywool.com/llms.txt. Contract: https://api.trywool.com/v1/openapi.yaml. - Connect a user with the auth link at https://connect.trywool.com/v1/oauth/authorize, exchange the code at POST https://api.trywool.com/v1/oauth/token, and call executions with `Authorization: Bearer `. - Send `Idempotency-Key` on every execution you create. Poll the execution until `status` is COMPLETED or FAILED. - `COMPLETED` means the run succeeded and `result` holds the answer. `FAILED` means it didn't; branch on `terminal.status` for the reason and log `terminal.reason`. - Never store or log the user's website credentials; WOOL never returns them. ``` A run succeeded only when `status` is `COMPLETED`. Check that your agent never reports `final_text` from a `FAILED` run as done. ## Next steps [#next-steps] Point your agent at the [Quickstart](/docs/quickstart) and the contract, and read [Handling failures](/docs/guides/failures) yourself so you can check its work. # Changelog (https://trywool.com/docs/changelog) If you generate a client from the contract, read this page before you regenerate it. ## 2026-09-04 [#2026-09-04] * Executions now carry `usage` with the credits the run cost and whether it was charged. `result` no longer includes internal run metrics. Creating a run with an empty balance returns `402 INSUFFICIENT_CREDITS`. [Billing](/docs/guides/billing) explains credits. ## 2026-09-03 [#2026-09-03] * PKCE is now optional for public applications. If the auth link omits `code_challenge`, your app can omit `code_verifier` from the code exchange. A link with a challenge still requires the matching verifier. See [PKCE in a browser app](/docs/guides/pkce). * Added `GET /v1/applications/{id}/executions`, which lists a connection's executions newest first, with `limit`, `after`, and `has_more`. See [Polling and history](/docs/guides/polling-and-history). * Execution objects now carry `prompt`, so a history view can label each run without storing the prompt. * The contract is served at `GET /v1/openapi.yaml`. # Console (https://trywool.com/docs/console) The [console](https://trywool.com/console) is where you create applications, find credentials, build auth links, watch connections, and test prompts on a connected account before you write code. Sign in with an email code or a Google account. ## Applications [#applications] Create an application, then open it for its credentials and settings: * **Client ID** is the application's id and the `client_id` on every auth link. * **Client secret** appears once, for confidential applications only, because WOOL keeps only a hash of it. Copy it before you leave the page. You can rotate it later from the application's overview; the new secret is shown once and the previous one stops working immediately. * **Redirect URIs** are matched exactly. Register every callback your app uses, including a `localhost` one. The console's own callback needs no registration: it works for every application, and a connection made through it can only be completed by you, signed in to the console. On a public application, a `localhost` or plain `http` redirect is for development only: anyone can run that address on their own machine, complete a connection, and every sign-in and run settles to your balance. Remove it before real users connect. A server application is safe here, because its code cannot be exchanged without the client secret. * **Icon** is what users see when they authorize. Use a square JPEG, PNG, or WebP of at most 512 KiB. The client type locks once the application has connection history. [Applications and clients](/docs/concepts/applications) explains the choice. ## Auth link [#auth-link] The auth link is the URL your user opens to connect an account. The application page builds one for you: choose the website, pick a redirect URI (one you registered, or the console's own callback), and copy the URL. Opening it runs a real connection through the selected callback. In your app, generate a fresh `state` value to check when the user returns and, if you use PKCE, a new verifier and challenge for each authorization. The [Quickstart](/docs/quickstart) explains how to generate these values and build the link in your app. ## Connections [#connections] Connections belong to your application. To end the active connection in the playground, choose **Revoke connection**. Every token for that connection stops accepting new runs, and WOOL deletes the saved sign-in. Past executions stay readable. A run already in progress may still finish. Your app can revoke a single token at `POST /v1/oauth/revoke`; that leaves the connection and saved sign-in in place. You can delete an application after its active runs and sign-ins finish. Deleting it removes all its connections and execution history. ## Playground [#playground] Select a token you saved from a callback, write a prompt, and run it. The playground shows that connection's execution history, newest first, with the status, the reason if the run failed, and the result JSON. [Testing with the playground](/docs/guides/playground) walks through a session. ## Account [#account] Your email, name, and role. Signing out ends the session in every tab. ## Next steps [#next-steps] With an application created, follow the [Quickstart](/docs/quickstart) to connect a user's account, exchange the authorization code, and run a prompt on that account. # Overview (https://trywool.com/docs) Each user authorizes your product once, on a page WOOL hosts called the connect page. After that, your product sends a prompt, WOOL runs an agent in the cloud inside that user's account on the website they authorized, and you get back the result, or the reason the run failed. Nothing runs on the user's computer, and your product never sees their password. WOOL handles the whole range, from a single lookup to chained, multi-step automation and monitoring, on one website or across several sites the user has connected. Your product coordinates work across sites by sending a separate run to each site's connection. ## Build with a coding agent [#build-with-a-coding-agent] These docs are written to be read whole by a coding agent, and the line below tells it where to start. Paste it into Claude Code, Cursor, or whichever agent you use, and it reads the docs and the API contract, then sets up the connect flow and executions in your project. [For coding agents](/docs/agents) lists everything it reads. ## Isn't this just ChatGPT agent, or Grok? [#isnt-this-just-chatgpt-agent-or-grok] Those are agents a person runs on their own accounts, from a chat window. WOOL lets *your* users authorize *your* product to run agents on *their* accounts, through an API. Without that layer, an agent gets into someone else's account the way a stranger would, with their password. You would have to store that password, the user could not limit or revoke it, and nothing would record what the agent did in their name. With it, the user connects an account the way they'd "Sign in with Google", your product receives a token for that one connection, and every agent you run with the token acts as that user, in an isolated session, with evidence of what it did. Most websites have no API. Your users' bank, email, supplier portal, and team tools each have a login page and nothing else. WOOL is how your product works inside those accounts with the user's permission, without holding their credentials, and with confirmation that the agent did what you asked. ## What you can rely on [#what-you-can-rely-on] * **The user is in control.** They authorize your product on `connect.trywool.com`, sign in there themselves, and can revoke the connection at any time. Your product never receives their password. * **Successful connections help later users.** WOOL reuses routes to a website's sign-in flow across applications, reducing repeated discovery work when the page still matches. See [Faster sign-in discovery over time](/docs/concepts/connections#faster-sign-in-discovery-over-time). * **Every agent runs isolated, in the cloud.** An agent acts only as the connected account, on the connected website, in a session that exists for that run alone. * **A run succeeds or fails, and says why.** WOOL confirms every result against the page before it calls the run a success. A failed run tells you the reason, and every result includes evidence: the pages the agent visited and the actions it took. * **An agent won't cross lines.** It doesn't sign in, doesn't solve CAPTCHAs, and doesn't reach for sites or accounts the user didn't connect. ## Connect, exchange, and run [#connect-exchange-and-run] 1. **Connect.** Send the user to the auth link, a URL on `connect.trywool.com` carrying your `client_id` and the website to connect. On the connect page it opens, they approve your product and sign in to the website, and WOOL redirects back to you with a code. 2. **Exchange.** Trade the code at `POST /v1/oauth/token` for a connection access token, plus a `connection_id` and a `user_id` to keep. 3. **Run.** Start an execution with a prompt and poll it until it's `COMPLETED` or `FAILED`. A completed run holds the result, and a failed one holds the reason. ## The three objects [#the-three-objects] ## Where to go next [#where-to-go-next] Start with the [Quickstart](/docs/quickstart) for a browser or native app, or the [server-side quickstart](/docs/quickstart-server) for a backend that can keep a secret. # Server-side quickstart (https://trywool.com/docs/quickstart-server) This guide connects a user's Gmail account and reads invoice emails from a backend. A confidential application keeps a client secret on your server and uses it to authenticate the code exchange. It can also tag each connection with your own user id. The [public-client quickstart](/docs/quickstart) uses PKCE instead of a client secret. ## 1. Create a confidential application [#1-create-a-confidential-application] In the [console](https://trywool.com/login), create an application with the client type set to confidential. The console shows `clientSecret` once; store it with your other secrets. You can rotate it later from the application's overview. The previous secret stops working immediately, so update your backend with the new one. Register the callback URL your backend will handle. The examples use `https://app.example/callback`; replace it with your registered URL in both the auth link and the code exchange. ## 2. Build the auth link [#2-build-the-auth-link] Your backend builds the link per request. Generate a fresh `state`, a random string you'll check when the user returns, store it against the signed-in user, and add `external_user_id`, your own id for that user. Replace `APPLICATION_ID`, `STATE`, and `USER_42` with your values. This link asks the user to connect Gmail: ```text https://connect.trywool.com/v1/oauth/authorize ?client_id=APPLICATION_ID &redirect_uri=https://app.example/callback &response_type=code &state=STATE &website=mail.google.com &external_user_id=USER_42 ``` Send the user to this link. After they approve and sign in, WOOL redirects to your callback with `code` and `state`. Check `state` against the value you stored before accepting the code. If the callback carries `error`, handle the declined or failed authorization before attempting an exchange. PKCE (Proof Key for Code Exchange) is optional for confidential applications. If you include its challenge on the link, you must send the matching verifier when exchanging the code; see the [PKCE guide](/docs/guides/pkce). ## 3. Exchange the code [#3-exchange-the-code] Use the `code` from the callback as `CODE`. Authenticate with HTTP Basic, using the application id as the username and the client secret as the password: ```bash curl -X POST https://api.trywool.com/v1/oauth/token \ -u "APPLICATION_ID:CLIENT_SECRET" \ -d grant_type=authorization_code \ -d code=CODE \ -d redirect_uri=https://app.example/callback ``` The response carries `access_token` for execution requests, `connection_id` for the connected Gmail account, and `user_id` for the user within your WOOL application. Store these values against your own user record. The token is long-lived and doesn't refresh. ## 4. Run and poll [#4-run-and-poll] Use the token from the exchange as `ACCESS_TOKEN`. This run looks for unpaid invoice emails in the connected Gmail inbox. The `output_schema` asks for invoice numbers and totals as structured data: ```bash curl -X POST https://api.trywool.com/v1/applications/APPLICATION_ID/executions \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "prompt": "List the unpaid invoices in my inbox with their invoice numbers and totals.", "output_schema": { "type": "object", "properties": { "invoices": { "type": "array", "items": { "type": "object", "properties": { "number": { "type": "string" }, "total": { "type": "number" } } } } } } }' ``` The create response contains `execution.id`. Use it in `GET /v1/applications/{id}/executions/{executionId}`, with the same bearer token, and poll every 3 to 5 seconds until `status` is `COMPLETED` or `FAILED`. With an `output_schema`, `result.extracted` holds the data in the shape you asked for, and the run completes only when the result matches that shape. On failure, read `terminal.status` and `terminal.reason`. The command generates a new `Idempotency-Key` each time you run it. If you retry a request after a timeout, reuse its original key and body to avoid creating a second run; see [Idempotency](/docs/guides/idempotency). ## Next steps [#next-steps] [Confidential flow on a server](/docs/guides/confidential) covers several connections per user and the secret. [Structured results](/docs/guides/structured-results) has the rules for `output_schema`. # Quickstart (https://trywool.com/docs/quickstart) By the end of this page you'll have connected a real account, run an agent on it from a prompt, and read the result. We'll use a public client, the right choice for a browser or native app because it holds no secret, and PKCE (Proof Key for Code Exchange), which proves that the app exchanging the code is the app that started the flow. PKCE is optional, and recommended for public clients. If you have a backend that can keep a secret, the [server-side quickstart](/docs/quickstart-server) shows how to authenticate the code exchange with a client secret and associate connections with your own user ids. ## 1. Create an application [#1-create-an-application] Sign in to the [console](https://trywool.com/login) and create an application. Its `id` is the `client_id` on the auth link, and its redirect URIs are the addresses WOOL may send users back to. Redirect URIs match exactly, and `http:` is allowed only for `localhost`, so register both your local and production callbacks. ## 2. Build the auth link [#2-build-the-auth-link] The auth link is the URL your user opens to connect an account. Before you build it, generate `state`, a random string you check when the user returns so you accept only callbacks you started. For PKCE, also generate a verifier, a random secret the app keeps, and its challenge, the SHA-256 hash of the verifier that goes on the link. [PKCE in a browser app](/docs/guides/pkce) has a helper for these values. `website` is the site the user will sign in to, as a hostname or an HTTPS URL. This example connects a Gmail account, which we'll use for the email summary in step 5. Replace `APPLICATION_ID`, `STATE`, and `CHALLENGE` with your values, and use a registered callback for `redirect_uri`. ```text https://connect.trywool.com/v1/oauth/authorize ?client_id=APPLICATION_ID &redirect_uri=https://app.example/callback &response_type=code &state=STATE &website=mail.google.com &code_challenge=CHALLENGE &code_challenge_method=S256 ``` The user sees your application's name and icon and the website you asked to connect. If they approve, they sign in to that website on the connect page, the page the auth link opens, in a session isolated from your product, and WOOL redirects to your `redirect_uri` with `code` and `state`. If they decline, the redirect carries `error=access_denied` and `state` instead. An authorization expires 15 minutes after it starts, and a code must be exchanged within 30 minutes. ## 3. Receive the code [#3-receive-the-code] Your app needs a callback at the `redirect_uri`. This Node example handles it on a server while using the public client type, so it needs no client secret. `/start` generates the PKCE pair, remembers the verifier by `state`, and sends the user to the auth link; the callback checks `state`, handles an authorization error, and exchanges the code. Save `callback.mjs` alongside `pkce.mjs` from the [PKCE guide](/docs/guides/pkce). Set `APPLICATION_ID` and `REDIRECT_URI` in the environment, then run `node callback.mjs`. For a local test, register and use `http://localhost:3001/callback`, then open `http://localhost:3001/start`. ```js title="callback.mjs" // A minimal server for a public client: /start sends the user to the auth link, /callback exchanges the code. import { createServer } from 'node:http'; import { authLink, createPkce } from './pkce.mjs'; const API = process.env.WOOL_API_ORIGIN ?? 'https://api.trywool.com'; const { APPLICATION_ID, REDIRECT_URI, WEBSITE = 'mail.google.com' } = process.env; const callbackPath = new URL(REDIRECT_URI).pathname; // Verifiers waiting for their callback, keyed by state. Use your session store in a real app. const pending = new Map(); const reply = (response, status, body) => { response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); }; const server = createServer(async (request, response) => { const url = new URL(request.url, REDIRECT_URI); if (url.pathname === '/start') { const { challenge, state, verifier } = await createPkce(); pending.set(state, verifier); const location = authLink({ applicationId: APPLICATION_ID, challenge, redirectUri: REDIRECT_URI, state, website: WEBSITE, }); response.writeHead(302, { Location: location }); return response.end(); } if (url.pathname === callbackPath) { const state = url.searchParams.get('state'); const verifier = pending.get(state); pending.delete(state); if (!verifier) return reply(response, 400, { error: 'unknown state' }); if (url.searchParams.get('error')) return reply(response, 200, { declined: url.searchParams.get('error') }); const token = await fetch(`${API}/v1/oauth/token`, { body: new URLSearchParams({ client_id: APPLICATION_ID, code: url.searchParams.get('code'), code_verifier: verifier, grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, }), method: 'POST', }).then(result => result.json()); // Store token.access_token with token.connection_id and token.user_id against your user here. return reply(response, 200, { connection_id: token.connection_id, user_id: token.user_id }); } reply(response, 404, { error: 'not found' }); }); server.listen(Number(process.env.PORT ?? 3001), () => console.log(`listening on ${server.address().port}`)); ``` The verifier is looked up by `state` and deleted once used, so a callback you didn't start gets `400`. A callback carrying `error` is answered without an exchange. Store the returned token where the example's comment indicates; the response sent to the browser contains only the connection and user ids. ## 4. Exchange the code [#4-exchange-the-code] The callback example already exchanges the code. If you're implementing your own callback, this is the request it needs to send. Use the `code` from the redirect, the verifier you stored before authorization, and the same `redirect_uri`. Each code can be exchanged only once. ```bash curl -X POST https://api.trywool.com/v1/oauth/token \ -d grant_type=authorization_code \ -d client_id=APPLICATION_ID \ -d code=CODE \ -d code_verifier=VERIFIER \ -d redirect_uri=https://app.example/callback ``` The token response looks like this: ```json { "access_token": "…", "token_type": "Bearer", "connection_id": "…", "user_id": "…" } ``` Store the access token and the two ids: * `access_token` is the only credential executions need. It's long-lived and doesn't refresh. * `connection_id` identifies this one account on this one website. * `user_id` identifies the person within your application, across every website they connect. ## 5. Run an agent [#5-run-an-agent] An execution is one agent run on the connected account, from your prompt. Use the `access_token` from the exchange as `ACCESS_TOKEN`. This request asks for a summary from the Gmail account connected in step 2: ```bash curl -X POST https://api.trywool.com/v1/applications/APPLICATION_ID/executions \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Summarize my three newest emails." }' ``` The response is `202` with the execution as `QUEUED`. Send an `Idempotency-Key` on every run you create: a retry with the same key and body returns the same execution instead of starting a second one. The command above generates a new key each time you run it; save and reuse the original key when retrying a request, as described in [Idempotency](/docs/guides/idempotency). ## 6. Poll for the result [#6-poll-for-the-result] Runs take from tens of seconds to a few minutes. Use `execution.id` from the create response as `EXECUTION_ID` and poll every 3 to 5 seconds with the same access token: ```bash curl https://api.trywool.com/v1/applications/APPLICATION_ID/executions/EXECUTION_ID \ -H "Authorization: Bearer ACCESS_TOKEN" ``` When `status` is `COMPLETED`, the run succeeded: `result.final_text` holds the answer and `result.evidence` shows what the run saw and did. When it's `FAILED`, `terminal.status` names the reason, and [Handling failures](/docs/guides/failures) says what to do for each. ## The same flow in Node [#the-same-flow-in-node] This script combines the exchange, run, and polling requests. It reads `APPLICATION_ID`, `CODE`, `CODE_VERIFIER`, and `REDIRECT_URI` from the environment. Use a fresh authorization code that your callback hasn't already exchanged, together with that authorization's verifier and redirect URI. ```js title="run.mjs" // Exchange a code, start an execution, and poll it. Runs against any WOOL API origin (tested in CI). const API = process.env.WOOL_API_ORIGIN ?? 'https://api.trywool.com'; const { APPLICATION_ID, CODE, CODE_VERIFIER, REDIRECT_URI } = process.env; // code_verifier goes with the code only when the auth link carried a PKCE challenge. const token = await fetch(`${API}/v1/oauth/token`, { body: new URLSearchParams({ client_id: APPLICATION_ID, code: CODE, ...(CODE_VERIFIER ? { code_verifier: CODE_VERIFIER } : {}), grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, }), method: 'POST', }).then(response => response.json()); const headers = { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json' }; const started = await fetch(`${API}/v1/applications/${APPLICATION_ID}/executions`, { body: JSON.stringify({ prompt: 'Summarize my three newest emails.' }), headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() }, method: 'POST', }).then(response => response.json()); let execution = started.execution; while (execution.status === 'QUEUED' || execution.status === 'RUNNING') { await new Promise(resolve => setTimeout(resolve, 3000)); execution = await fetch(`${API}/v1/applications/${APPLICATION_ID}/executions/${execution.id}`, { headers }) .then(response => response.json()) .then(body => body.execution); } if (execution.status === 'COMPLETED') { console.log(execution.result.final_text); } else { console.error(`${execution.terminal.status}: ${execution.terminal.reason}`); } ``` ## Next steps [#next-steps] [Writing prompts that succeed](/docs/guides/prompts) shows how to phrase a prompt so the run completes. [Structured results](/docs/guides/structured-results) shows how to get JSON instead of prose. # Applications and clients (https://trywool.com/docs/concepts/applications) An application is your OAuth client: what users authorize when they connect an account, and what every connection and run belongs to. A developer can own many applications, and each application's `id` is its `client_id` on the auth link. ## Choosing a client type [#choosing-a-client-type] The client type says whether your code can keep a secret. A backend can. A browser or phone app can't, because anyone can read what ships to it. | Type | Proves the code exchange with | Holds a secret | | -------------- | ------------------------------------------------------- | --------------- | | `PUBLIC` | PKCE (`code_challenge`, S256), optional but recommended | No | | `CONFIDENTIAL` | HTTP Basic `client_id:client_secret` | Yes, shown once | Pick public for browser and native apps and confidential for backends. Only a confidential application may send `external_user_id` on the auth link to tag the connection with your own id for the user. The type locks once the application has connection history or a live authorization. Changing it later means a new application, so decide before your first real user connects. ## Redirect URIs [#redirect-uris] A redirect URI is where WOOL sends the user back after they connect. They're matched exactly, so `https://app.example/callback` and `https://app.example/callback/` are different entries. * Absolute URLs, at most 10 per application. * `https:` and custom schemes are allowed. `http:` only for `localhost`, `127.0.0.1`, and `[::1]`. * No fragments and no embedded credentials. Register your development callback alongside production so one application works in both places. ## Icons [#icons] Users see your icon, name, and description on the connect page. Upload a square JPEG, PNG, or WebP of at most 512 KiB in the [console](/docs/console). ## Deleting an application [#deleting-an-application] Deleting an application from the console revokes every connection it has and removes it with its connection and execution history, icon included. If a run or a sign-in is in progress at that moment, wait for it to finish and try again. ## Next steps [#next-steps] [Identifiers and credentials](/docs/concepts/identifiers) explains the ids and tokens you'll handle. # Connections and authorization (https://trywool.com/docs/concepts/connections) A connection is one user's account on one website, authorized by that user for your application. The auth link, the URL your product sends the user to, creates it. It lives until the user or you revoke it, and every agent you run is a call against one. The auth link opens the connect page on `connect.trywool.com`. The sign-in happens there rather than in your product so that your product never holds the user's password. The user approves your application and supplies the details needed to sign in. WOOL completes the sign-in, and your product receives a token for the resulting connection. This authorization includes accepting the website's required sign-in agreements, including new or updated terms, and required consent from a sign-in provider. It does not include optional enrollment, subscriptions, purchases, or permissions unrelated to signing in. If the website asks the user to approve the sign-in on another device, the connect page shows the website's own instruction and waits for the approval. ## The authorization, step by step [#the-authorization-step-by-step] Some websites ask for more than a password, such as a choice of sign-in method or a one-time code. The connect page asks the user for the details or approvals WOOL needs to continue. Three outcomes are possible: * The sign-in completes. WOOL redirects to your `redirect_uri` with `code` and `state`. * The user declines. The redirect carries `error=access_denied` and `state`. * The sign-in fails. The redirect carries `error=server_error` and `state`. An authorization expires 15 minutes after it starts, so build the link when the user is about to open it. ## Faster sign-in discovery over time [#faster-sign-in-discovery-over-time] WOOL keeps a shared discovery cache of successful routes to a website's sign-in flow. For example, after learning that a website's account menu leads to its sign-in form, WOOL can reuse that route when another user connects through the same entry page and language. This reduces the work needed to find where sign-in begins. These routes are shared across applications. A successful authorization through one product can help users of another product reach the same website's sign-in form sooner. As more users connect websites, successful discoveries add to or update the routes available for later connections. You don't need to configure this in your integration. WOOL checks the current page before reusing a route. If the website has changed, it discovers the sign-in flow again. The time saved depends on the website and whether a matching route is available; each user still needs to authorize your application, sign in, and complete any required verification. The shared cache contains navigation guidance, not passwords, one-time codes, saved sign-ins, execution prompts, or task results. This reuse applies to sign-in discovery. Once connected, each [execution](/docs/concepts/executions) works from your prompt against that user's account. ## Connection statuses [#connection-statuses] | Status | What it means | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `ACTIVE` | Runs executions. | | `REAUTHORIZATION_REQUIRED` | The website ended the session. New executions return `409 CONNECTION_REAUTHORIZATION_REQUIRED`; the status and past runs stay readable. | | `REVOKED` | Final. Every token stopped working and the saved sign-in was deleted. | ## When the website signs the user out [#when-the-website-signs-the-user-out] When a run finds the account signed out, it fails with `session_expired` and the connection becomes `REAUTHORIZATION_REQUIRED`. New runs are refused until the user goes through the same auth link again. When they do, the connection returns to `ACTIVE` and the code exchange issues a new token. `connection_id` and `user_id` don't change, so you replace the stored token and carry on. [Re-authorization](/docs/guides/reauthorization) covers the details. ## Revoking a connection [#revoking-a-connection] Revoke from the console. Revocation ends any authorization in progress, revokes every token, and deletes the saved sign-in. Past executions remain readable. A run already in progress may still finish. To end a single token without touching the connection, use `POST /v1/oauth/revoke`. ## Next steps [#next-steps] [Executions](/docs/concepts/executions) explains what happens when you run a prompt against a connection. # Data (https://trywool.com/docs/concepts/data) ## What is stored [#what-is-stored] | Data | Where it lives | Returned by the API | | --------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------- | | Application name, description, icon, redirect URIs | With the application | Yes | | Connection website, status, user id | With the connection | Yes | | The encrypted sign-in for the connected website | With the connection | Never | | Website entry URLs and reusable routes to sign-in | Shared discovery cache across the service | No | | Execution prompt, start URL, output schema, result, error | With the execution | Yes, to the connection's token | | Operational logs for support | With the execution | No | | Billing account, purchases, and per-run usage records | With the developer | Only to the console; `usage` on an execution shows its credits | Passwords and one-time codes are never stored. Results contain whatever your prompt asked the run to read from the user's account, so treat them as user data on your side as well. The [shared discovery cache](/docs/concepts/connections#faster-sign-in-discovery-over-time) records how to reach a website's sign-in flow. It excludes credentials, saved sign-ins, execution prompts, and task results. These navigation routes can help later users connect, including users of other applications. ## What you can delete [#what-you-can-delete] * **Revoke a connection.** Tokens stop working and the saved sign-in is deleted. Past executions stay readable. * **Delete an application** from the console. Removes the application, its connections, tokens, executions, saved sign-ins, and icon. Allowed once the application has no live connections, so revoke first if needed. * **Revoke a token.** `POST /v1/oauth/revoke` ends one token and nothing else. ## Next steps [#next-steps] [Isolation and security](/docs/concepts/isolation) covers the guarantees around the saved sign-in and each run. # Executions (https://trywool.com/docs/concepts/executions) An execution is one agent run on the connected account, from your prompt, in its own isolated session in the cloud. You start it with one call, it's queued, it runs, and it ends either `COMPLETED` with a result or `FAILED` with a reason. Runs use the connection's saved sign-in, so the user doesn't need to sign in for every prompt while that session remains valid. Each run performs its task against the website's current state. The [shared discovery cache](/docs/concepts/connections#faster-sign-in-discovery-over-time) helps users reach sign-in during authorization; it doesn't replay completed tasks or reuse another user's results. ## The lifecycle [#the-lifecycle] `status` is the job state: `QUEUED`, `RUNNING`, `COMPLETED`, or `FAILED`. Poll on it until the run is over. On `COMPLETED`, `result` holds what the run produced. On `FAILED`, `terminal.status` names the reason and `terminal.reason` explains it in plain words. A completed run's `terminal.status` is `verified`, with what WOOL confirmed in `terminal.reason`. ## What success means [#what-success-means] A run is `COMPLETED` only when the agent did what your prompt asked and WOOL confirmed the result against the page. The check is independent of the agent, so an agent can't mark its own work done. If the agent finishes but WOOL can't confirm the result, the run fails with `unverified`. ## Failure reasons [#failure-reasons] | `terminal.status` | The run failed because | | ------------------ | -------------------------------------------------------------------------- | | `unverified` | The agent finished, but WOOL couldn't confirm the result against the page. | | `not_possible` | The task can't be done on this account or page. | | `needs_user` | The site asked for something only the user can give, such as a CAPTCHA. | | `session_expired` | The website signed the account out. The connection needs re-authorization. | | `site_error` | The website kept failing: server errors, blocks, or rate limits. | | `blocked_policy` | The prompt asked for something outside what the connection allows. | | `budget_exhausted` | The run used its allowance of time and actions before finishing. | | `no_progress` | The run stopped making progress and was ended. | | `timed_out` | The run hit its time limit. | | `provider_failed` | An upstream provider failed. Nothing about the prompt caused it. | | `browser_error` | The isolated session couldn't start or resume. | [Handling failures](/docs/guides/failures) says what your code should do for each. ## The result [#the-result] `result` is present on `COMPLETED` and is at most 256 KiB. The fields below are inside that object, so you read the prose answer from `result.final_text` and the evidence from `result.evidence`. | Field | What it holds | | -------------------------- | -------------------------------------------------------------------------- | | `final_text` | The answer in prose. | | `extracted` | The structured result, when you passed an `output_schema`. | | `final_url`, `final_title` | Where the run ended. | | `evidence.urls` | Every page it visited. | | `evidence.side_effects` | Every action that changed something, with what the page showed afterwards. | | `evidence.verifier` | What WOOL confirmed, and why. Also returned as `terminal.reason`. | On `FAILED`, `result` may still hold the evidence gathered before the run ended, which is often the fastest way to see what went wrong. `usage` holds what the run cost once it is over: `credits`, and `charged`, which is `false` when the run wasn't billed because it failed on our side or never reached the browser. [Billing](/docs/guides/billing) has the rate card. ## Ordering and pacing [#ordering-and-pacing] Executions on one connection run one at a time, in the order you created them. Runs on different connections don't wait for each other. Requests to one website are paced across all of your executions, so a burst is queued rather than sent at once. That's why runs take from tens of seconds to a few minutes and why you poll. ## What a run refuses [#what-a-run-refuses] A run acts only as the connected account, on the connected website. It won't sign in or create accounts, won't answer CAPTCHAs or other human checks, and won't reach other sites or accounts. Prompts that ask for those fail with `session_expired`, `needs_user`, or `blocked_policy`, so your product can tell the user what's needed instead of retrying. ## Next steps [#next-steps] [Results and evidence](/docs/concepts/results) explains what a completed run gives you. [Polling and history](/docs/guides/polling-and-history) covers reading runs from code. # Identifiers and credentials (https://trywool.com/docs/concepts/identifiers) ## Two credentials [#two-credentials] | Credential | You get it from | You use it for | | ----------------------- | ------------------------- | ---------------------------------------------------- | | Developer session | Signing in to the console | The console: applications, connections, your profile | | Connection access token | `POST /v1/oauth/token` | Executions for one connection | The session is yours as a developer and isn't part of the API you integrate. The token belongs to one connection and goes on every execution request. Tokens are long-lived and don't refresh, so a stored token works until it's revoked. Treat it like a password. Issuing a new token for the same connection revokes the previous one, and `POST /v1/oauth/revoke` revokes one on demand. ## Four ids [#four-ids] | Id | Where it appears | What it identifies | | --------------- | -------------------------------------------------------------- | -------------------------------------------- | | Application id | `client_id` on the auth link; the path of the execution routes | Your OAuth client | | `user_id` | The token response; `userId` on a connection | The person, within your application | | `connection_id` | The token response | One website account belonging to that person | | Execution id | The execution routes | One run | A person can hold several connections, one per website. Store connections keyed by `connection_id` and group them by `user_id`. ## History follows the connection [#history-follows-the-connection] `GET /v1/applications/{id}/executions` lists the runs of the connection the token belongs to. The history belongs to the connection, not the token, so a user who reconnects keeps their history. ## Ownership is checked everywhere [#ownership-is-checked-everywhere] Every route checks that the application in the path, the token, and any nested id belong together. A mismatch returns not found rather than forbidden, so an id you don't own reveals nothing. ## Next steps [#next-steps] [Connections and authorization](/docs/concepts/connections) shows how a connection is created and what happens to it over its life. # Isolation and security (https://trywool.com/docs/concepts/isolation) These are the guarantees WOOL makes, stated as promises about behavior, so you can pass them on to your users and your security review. ## Each run is isolated [#each-run-is-isolated] Every authorization and every execution runs in its own session, created for that request and discarded after it. A saved sign-in is available only to runs on its connection. A run on another connection, or an application or developer that doesn't own the connection, cannot use it. WOOL shares [sign-in discovery routes](/docs/concepts/connections#faster-sign-in-discovery-over-time) across the service to reduce repeated navigation work. That cache contains no passwords, one-time codes, saved sign-ins, execution prompts, or task results. Every user still authorizes your application and completes their own sign-in. ## Credentials never leave the connect page [#credentials-never-leave-the-connect-page] The user types their password, and any one-time code, on `connect.trywool.com`. Those values go into the isolated sign-in and nowhere else. Your product never receives them, no result contains them, and WOOL doesn't return, log, or store them. ## The saved sign-in is encrypted and yours to delete [#the-saved-sign-in-is-encrypted-and-yours-to-delete] Between runs WOOL keeps the signed-in session for the connected website, encrypted at rest and bound to the connection, so a run can start already signed in. Revoking the connection deletes it, and the API never returns it. ## What a run will not do [#what-a-run-will-not-do] A run acts only as the connected account, on the connected website. It won't sign in or create accounts, answer CAPTCHAs or other human checks, or reach other sites or accounts. If your product needs a human step, tell the user, let them complete it on the website, then retry. ## Transport and origins [#transport-and-origins] All origins are HTTPS. The OAuth and execution routes answer cross-origin requests from your own pages, so a browser app can call them directly. Console routes require the developer session and the console's origin. ## Common questions [#common-questions] * **Can WOOL see my users' data?** A run reads what your prompt asks it to read, and the result you receive is what it read. Treat results as user data on your side too. [Data](/docs/concepts/data) lists what's stored and what you can delete. * **Can a prompt make WOOL do something the user didn't authorize?** No. The connection is the boundary: one account, one website, until the user revokes it. ## Next steps [#next-steps] [Data](/docs/concepts/data) lists what WOOL stores and what you can delete. # Results and evidence (https://trywool.com/docs/concepts/results) A completed run gives you the result and the evidence WOOL used to confirm it. ## Why WOOL checks the result [#why-wool-checks-the-result] An agent can report that it submitted a form even if the website didn't accept it, or include a fact in a summary that the page never showed. WOOL checks every run against your prompt and the evidence before it marks the run `COMPLETED`. A completed run guarantees three things: * The result answers the prompt as written, including the `output_schema` when you gave one. * Every action that changed something on the website is recorded, with evidence that it took effect. * Nothing in the result is claimed without something on the website to back it. ## The evidence [#the-evidence] The execution's `result` contains an `evidence` object. The fields below are relative to `result`: | Field | What it holds | | ----------------------- | -------------------------------------------------------------------------- | | `evidence.urls` | Every page the run visited. | | `evidence.side_effects` | Every action that changed something, with what the page showed afterwards. | | `evidence.verifier` | What WOOL confirmed, and why. Also returned as `terminal.reason`. | Show it to your users when the stakes warrant it, or keep it with the result so you can answer "what did it do?" later. If a run was asked to change something on the website, read `result.evidence.side_effects` before any retry to check which actions already took effect. ## When WOOL can't confirm the result [#when-wool-cant-confirm-the-result] The run fails with `unverified`: the agent finished, but the check couldn't confirm what it claimed. Never treat an `unverified` run as done. If the failed run includes `result`, review its `final_text` and evidence. For a low-stakes task, you can decide whether that unconfirmed result is useful. Otherwise, retry with a more specific prompt or ask the user. ## Next steps [#next-steps] [Handling failures](/docs/guides/failures) turns every failure reason into an action for your code. [Structured results](/docs/guides/structured-results) shows how an `output_schema` makes the check stricter. # Billing (https://trywool.com/docs/guides/billing) WOOL bills in credits. One credit is $0.01, credits never expire, and every execution and every completed authorization debits them from your balance. You start with 500 free credits, and after that you buy credits by card in the console. ## The rate card [#the-rate-card] Each run's price is computed from what it consumed and rounded up to whole credits: | Meter | Credits | | --------------------------------------------------- | --------------------------------------------------------- | | Per execution | 5 | | Browser time | 1 per started minute | | Agent input tokens | 0.35 per 1,000, or 0.035 per 1,000 when served from cache | | Agent output tokens | 2.1 per 1,000 | | Page tokens | 0.13 per 1,000 read, 0.8 per 1,000 written | | Completed authorization, including re-authorization | 10 | A short read-only run such as summarizing three emails comes to around 30 credits. A long multi-page task comes to a few hundred. The playground shows the credits each run used, so the fastest way to learn what a prompt costs is to run it there. ## What you're charged for [#what-youre-charged-for] Every run that starts is charged, whatever its outcome, except when the failure was ours. A run whose `terminal.status` is `provider_failed` or `browser_error`, or that never reached the browser, costs nothing. A run that ends `unverified` is charged, because the agent did the work; so is one that ends `not_possible` or `needs_user`. Retrying a create request with the same `Idempotency-Key` and body returns the original execution without an additional charge. [Idempotency](/docs/guides/idempotency) explains how to reuse a key. Every execution has a `usage` object showing what that run cost. For example, a billed run could contain: ```json "usage": { "credits": 34, "charged": true } ``` `usage.credits` and `usage.charged` are `null` until the run is `COMPLETED` or `FAILED`. `usage.charged` is `false` on a run we waived. ## When the balance runs out [#when-the-balance-runs-out] Runs already in flight always finish, even if they take the balance below zero. A new run is refused with `402 INSUFFICIENT_CREDITS` while the balance is below one credit, and accepted again as soon as you top up. Authorizations are never refused, so your users can always connect. ## Buying credits and auto-reload [#buying-credits-and-auto-reload] Buy credits on the console's Billing page, $10 to $1,000 per purchase, as often as you like. The card you use is saved, and you can turn on auto-reload: choose a threshold and a target, and when a run takes the balance below the threshold we charge the card to bring it back to the target. If a charge fails, auto-reload switches off and we email you. With auto-reload off, we email you once when the balance drops below 100 credits. Credits don't expire and aren't refundable. If something went wrong with a purchase, contact support. ## Next steps [#next-steps] [Handling failures](/docs/guides/failures) lists every failure reason and what to do about each. # Confidential flow on a server (https://trywool.com/docs/guides/confidential) A confidential application keeps its `clientSecret` on your server and uses it to authenticate the code exchange. If your product has a backend, this is the normal choice: no PKCE to manage, tokens that never touch a browser, and connections tagged with your own user ids. The [server-side quickstart](/docs/quickstart-server) shows the calls. What follows is what to store and how to manage the secret. ## Tag connections with your user id [#tag-connections-with-your-user-id] Add `external_user_id`, your own id for the signed-in user, to the auth link. Use the same value when that user connects another website. WOOL uses it to associate those connections with the same user within your application. ## One user, many connections [#one-user-many-connections] A user may connect several websites, and each becomes its own connection with its own token. The code exchange returns `connection_id`, `user_id`, and `access_token`. This `user_id` is WOOL's id for the user within your application; `external_user_id` is the id from your own system. Store each token under its `connection_id`, together with the website and your user record. Group the connections by `user_id`. When you need to run a prompt, pick the connection for the right website and use its token. ## Managing the secret [#managing-the-secret] Keep the secret on your backend with your other credentials. To replace it, choose **Rotate secret** in the application's overview in the console. Copy the new secret when it appears; WOOL shows it once. The previous secret stops working immediately. Update your backend with the new one before exchanging another authorization code. Your existing connections and access tokens remain valid. ## Next steps [#next-steps] [Idempotency](/docs/guides/idempotency) keeps retries from starting a second run. [Re-authorization](/docs/guides/reauthorization) covers what to do when a website signs a user out. # Errors (https://trywool.com/docs/guides/errors) Errors from execution requests use this shape. This example shows a request rejected because its `start_url` points outside the connected website: ```json { "error": { "code": "INVALID_START_URL", "message": "start_url must remain on the connected website origin." } } ``` `error.code` is stable and meant for your logic. `error.message` is for logs and people and may change. The OAuth token and revocation routes use the OAuth error format, with a string in `error` and an explanation in `error_description`. ## Codes [#codes] | Code | Status | What it means | | ---------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `INVALID_EXECUTION` | 400 | The prompt is missing or longer than 10,000 characters. | | `INVALID_START_URL` | 400 | `start_url` isn't on the connected website's origin. | | `INVALID_OUTPUT_SCHEMA` | 400 | The schema breaks a rule in [Structured results](/docs/guides/structured-results). | | `INVALID_IDEMPOTENCY_KEY` | 400 | The key is empty or longer than 255 characters. | | `INVALID_PAGINATION` | 400 | `limit` is outside 1–50, or `after` isn't an execution id. | | `INVALID_JSON`, `REQUEST_TOO_LARGE` | 400, 413 | The body isn't JSON, or is over 64 KiB. | | `CONNECTION_NOT_FOUND`, `EXECUTION_NOT_FOUND`, `NOT_FOUND` | 404 | The token is unknown or revoked, or the resource doesn't exist or isn't yours. The response doesn't reveal whether an inaccessible resource exists. | | `INSUFFICIENT_CREDITS` | 402 | Your credit balance is below one credit. See [Billing](/docs/guides/billing). | | `CONNECTION_REAUTHORIZATION_REQUIRED` | 409 | The user needs to sign in again. See [Re-authorization](/docs/guides/reauthorization). | | `IDEMPOTENCY_KEY_REUSED` | 409 | The key was used with a different body. | | `INTERNAL_ERROR` | 500 | Something unexpected. Retry once, then report it. | ## OAuth codes [#oauth-codes] | Code | Status | What it means | | ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------- | | `invalid_client` | 401 | Unknown `client_id`, or a wrong secret. | | `invalid_grant` | 400 | The code is invalid, expired, or already used, `redirect_uri` doesn't match, or the PKCE verifier doesn't match. | | `unsupported_grant_type` | 400 | `grant_type` isn't `authorization_code`. | | `invalid_request` | 400 | The body couldn't be read. | For execution request errors, log the route, `error.code`, and `error.message`. For OAuth errors, log the route, `error`, and `error_description`. The 4xx codes are things your code or your user can fix; the 5xx codes are things to retry and then report. ## Next steps [#next-steps] A run that started and then failed reports its reason in `terminal.status` instead. [Handling failures](/docs/guides/failures) covers each one. # Handling failures (https://trywool.com/docs/guides/failures) A run that ends `FAILED` names the reason in `terminal.status` and explains it in `terminal.reason`. Branch on `terminal.status`, and use `terminal.reason` for logs and for people. Runs that end `provider_failed` or `browser_error` aren't charged; see [Billing](/docs/guides/billing). | `terminal.status` | What to do | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unverified` | Don't treat the result as done. For a low-stakes task, show `result.final_text` as unconfirmed if it is present; otherwise retry once with a more specific prompt, or ask the user. | | `not_possible` | Tell the user why, using `terminal.reason`. Don't retry the same prompt. | | `needs_user` | Ask the user to complete the step on the website themselves, then retry. | | `session_expired` | Send the user through the auth link again. Until then, creating runs returns `409 CONNECTION_REAUTHORIZATION_REQUIRED`. | | `site_error` | Retry later with backoff. The website was failing, blocking, or rate limiting. | | `blocked_policy` | Fix the prompt. It asked for another site, another account, or something the connection isn't for. | | `budget_exhausted` | Split the task into smaller prompts, or narrow it with `start_url`. | | `no_progress` | Rephrase the prompt with a clearer goal or a `start_url`, and retry once. | | `timed_out` | Rephrase the prompt with a clearer goal or a `start_url`, and retry once. | | `provider_failed` | Retry with backoff. Nothing about the prompt caused it. | | `browser_error` | Retry with backoff. If it persists on one connection, revoke and reconnect it. | ## Two buckets [#two-buckets] 1. **Needs a person.** `unverified`, `not_possible`, `needs_user`, `session_expired`, `blocked_policy`. Show the user what happened and what they can do. 2. **Try again later.** `site_error`, `budget_exhausted`, `no_progress`, `timed_out`, `provider_failed`, `browser_error`. Retry with backoff, adjusting the prompt where the table says so. ## Retrying safely [#retrying-safely] A retry with a new `Idempotency-Key` starts a new run. If the previous run changed something on the website, such as sending a message, placing an order, or deleting a record, a new run may repeat that action. Check `result.evidence.side_effects`, when present, to see what already happened before starting another run. Reuse the same key and body when retrying a create request whose response you didn't receive. That returns the original execution if it was created, including a failed one. Use a new key only when you intend a new run. ## Next steps [#next-steps] [Idempotency](/docs/guides/idempotency) has the rules for keys. [Re-authorization](/docs/guides/reauthorization) walks through the `session_expired` path. # Idempotency (https://trywool.com/docs/guides/idempotency) Idempotency lets you retry a request to create an execution without creating a duplicate run. If your request times out, retry it with the same `Idempotency-Key` and request body. If WOOL already created an execution for that key, it returns the existing execution. Otherwise, it creates one. ## How it works [#how-it-works] Send `Idempotency-Key` on `POST /v1/applications/{id}/executions`. The key is 1–255 characters and is scoped to the connection. | Situation | Result | | ------------------------ | -------------------------------------------- | | Same key, same body | The original execution, whatever its status. | | Same key, different body | `409 IDEMPOTENCY_KEY_REUSED`. | | No key | A new execution every time. | The body is compared as `prompt`, `start_url`, and `output_schema` after normalizing, so key order and whitespace don't matter. ## The pattern [#the-pattern] 1. Generate a UUID for the run you intend to start. 2. Store it with the request before you send anything. 3. Send the request with that key. 4. On any retry of that request, reuse the key. If your process crashes after sending the request, retry with the key and request body you stored in step 2. WOOL returns the existing execution if the first request created one. ## Next steps [#next-steps] [Handling failures](/docs/guides/failures) explains when a retry should reuse the key and when it should be a new run. # PKCE in a browser app (https://trywool.com/docs/guides/pkce) A public application has no secret, so PKCE (Proof Key for Code Exchange) is how WOOL knows that the app exchanging a code is the app that started the flow. You generate a random value, the verifier, keep it in the app, and send its SHA-256 hash, the challenge, on the auth link. At exchange time you send the verifier, and WOOL checks that it hashes to the challenge. PKCE is optional in WOOL and recommended for every browser or native app. ## Generate the pair [#generate-the-pair] The helper below generates a verifier and challenge for PKCE, plus a random `state` value. Your app checks `state` when the user returns to match the callback to the authorization it started. ```js title="pkce.mjs" // PKCE for a public client: a random verifier and its S256 challenge, plus a state value to check on return. // Uses only Web APIs, so it runs in a browser and in Node. const base64url = bytes => btoa(String.fromCharCode(...bytes)) .replaceAll('+', '-') .replaceAll('/', '_') .replace(/=+$/u, ''); export function createPkce() { const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); const state = base64url(crypto.getRandomValues(new Uint8Array(16))); return crypto.subtle .digest('SHA-256', new TextEncoder().encode(verifier)) .then(digest => ({ challenge: base64url(new Uint8Array(digest)), state, verifier })); } export function authLink({ applicationId, challenge, redirectUri, state, website }) { const url = new URL('https://connect.trywool.com/v1/oauth/authorize'); url.search = new URLSearchParams({ client_id: applicationId, code_challenge: challenge, code_challenge_method: 'S256', redirect_uri: redirectUri, response_type: 'code', state, website, }).toString(); return url.toString(); } ``` `createPkce()` returns the verifier, its challenge, and a `state` value. It uses only Web APIs, so the same file runs in a browser and in Node. Keep `verifier` and `state` in session storage keyed by `state`, and reject any callback whose `state` you don't recognise. ## Build the link [#build-the-link] The `authLink()` helper in `pkce.mjs` takes the challenge and `state` from `createPkce()`, along with your application id, registered redirect URI, and the website to connect. It adds `code_challenge` and `code_challenge_method=S256` to the auth link. Once a link carries a challenge, the exchange must present the matching verifier, so a stolen code is useless to anyone else. ## If you leave PKCE out [#if-you-leave-pkce-out] Without a challenge, anyone who can see the redirect, such as another app on the same custom scheme, a leaked referrer, or a shared browser log, can exchange the code themselves and receive a long-lived token to the user's account. If you skip PKCE, keep the redirect on a URL only your app can receive, exchange the code the moment it arrives, and prefer the [confidential flow](/docs/guides/confidential) on a backend where you can. ## Handle the callback [#handle-the-callback] You can handle the callback on a backend even when using a public client. This Node example imports `pkce.mjs`, sends the user to the auth link from `/start`, and exchanges the code when they return. Set `APPLICATION_ID` and `REDIRECT_URI` before running it; the [Quickstart](/docs/quickstart#3-receive-the-code) walks through local setup. ```js title="callback.mjs" // A minimal server for a public client: /start sends the user to the auth link, /callback exchanges the code. import { createServer } from 'node:http'; import { authLink, createPkce } from './pkce.mjs'; const API = process.env.WOOL_API_ORIGIN ?? 'https://api.trywool.com'; const { APPLICATION_ID, REDIRECT_URI, WEBSITE = 'mail.google.com' } = process.env; const callbackPath = new URL(REDIRECT_URI).pathname; // Verifiers waiting for their callback, keyed by state. Use your session store in a real app. const pending = new Map(); const reply = (response, status, body) => { response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); }; const server = createServer(async (request, response) => { const url = new URL(request.url, REDIRECT_URI); if (url.pathname === '/start') { const { challenge, state, verifier } = await createPkce(); pending.set(state, verifier); const location = authLink({ applicationId: APPLICATION_ID, challenge, redirectUri: REDIRECT_URI, state, website: WEBSITE, }); response.writeHead(302, { Location: location }); return response.end(); } if (url.pathname === callbackPath) { const state = url.searchParams.get('state'); const verifier = pending.get(state); pending.delete(state); if (!verifier) return reply(response, 400, { error: 'unknown state' }); if (url.searchParams.get('error')) return reply(response, 200, { declined: url.searchParams.get('error') }); const token = await fetch(`${API}/v1/oauth/token`, { body: new URLSearchParams({ client_id: APPLICATION_ID, code: url.searchParams.get('code'), code_verifier: verifier, grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, }), method: 'POST', }).then(result => result.json()); // Store token.access_token with token.connection_id and token.user_id against your user here. return reply(response, 200, { connection_id: token.connection_id, user_id: token.user_id }); } reply(response, 404, { error: 'not found' }); }); server.listen(Number(process.env.PORT ?? 3001), () => console.log(`listening on ${server.address().port}`)); ``` When the user returns: 1. Read `code` and `state` from the query. 2. If `state` is unknown, stop. Someone else started that flow. 3. If `error` is present, the user declined (`access_denied`) or the sign-in failed (`server_error`). Show a message and let them try again later. 4. Otherwise post to `POST /v1/oauth/token` with `client_id`, `code`, `code_verifier`, `grant_type=authorization_code`, and the same `redirect_uri`. The token endpoint allows cross-origin requests, so a browser app can exchange the code directly. ## Where the token should live [#where-the-token-should-live] The token is long-lived, so in a browser-only app anyone who can read the browser's storage can run executions as that user. The default choice is to exchange the code on a backend you control and keep the token there, even for a public client. Keep the browser-only path for prototypes and apps with no backend. ## Next steps [#next-steps] [Confidential flow on a server](/docs/guides/confidential) is the backend version of this flow. # Testing with the playground (https://trywool.com/docs/guides/playground) The console's playground lets you connect an account, run prompts on it, and read the execution history. It saves the connection access token when you return from authorization, or you can paste a token from your own app's callback. Test your prompts there before using them in your integration. ## Connect an account in the playground [#connect-an-account-in-the-playground] 1. In the playground, pick an application and the website to connect. The console uses its own callback as the redirect URI, which needs no registration on the application. 2. Connect an account you own on the WOOL page that opens. 3. You land back in the playground with the connection saved. Only you, signed in to the console, can complete an exchange through the console's callback, so a stranger who starts a flow with it ends up with a code they cannot use. This works the same for a server application: the console never needs your client secret. The playground stores the token in your browser. **Remove** clears the selected connection and its token from the playground. To revoke the connection and its tokens in WOOL, choose **Revoke connection**. ## Iterate on a prompt [#iterate-on-a-prompt] Write the prompt exactly as you'd send it from code, run it, and check its status. For a completed run, read `result.final_text`, then `result.evidence.urls` and `result.evidence.side_effects` to see what the run did. For a failed run, read `terminal.status` and `terminal.reason`. A prompt that names the page, the item, and the exact outcome completes more often than a broad one. ## Try the failure paths on purpose [#try-the-failure-paths-on-purpose] * Sign the account out on the website, run again, and watch the run fail with `session_expired` and the next run return `409`. * Send a prompt the connection isn't for and watch it fail with `blocked_policy` or `not_possible`. * From code, send the same create request twice with the same `Idempotency-Key`, request body, and connection token. Confirm the history shows one run; see [Idempotency](/docs/guides/idempotency). The playground doesn't set `output_schema`; do that from code. It also doesn't replace your own error handling. ## Next steps [#next-steps] Move the prompt into code with the [Quickstart](/docs/quickstart) and wire up [Handling failures](/docs/guides/failures). # Polling and history (https://trywool.com/docs/guides/polling-and-history) ## Polling a pending run [#polling-a-pending-run] `POST /v1/applications/{id}/executions` returns `202` with an `execution` object whose `status` is `QUEUED`. Use its `id` as `executionId` in `GET /v1/applications/{id}/executions/{executionId}`, with the same connection access token. Poll every 3 to 5 seconds until the returned execution's `status` is `COMPLETED` or `FAILED`, then read its `result` or `terminal.status`. A run that reaches its time limit fails with `timed_out`. There are no webhooks yet, so your product owns the polling loop. The Node script on the [Quickstart](/docs/quickstart#the-same-flow-in-node) shows one. You can still read executions with the existing token when their connection needs re-authorization. ## Listing history [#listing-history] `GET /v1/applications/{id}/executions` lists the runs of the connection the token belongs to, newest first. | Query | Meaning | | ------- | -------------------------------------------------- | | `limit` | Runs per page, 1–50. Defaults to `20`. | | `after` | The id of the last execution on the previous page. | The response is `{ "executions": [...], "has_more": true }`. Each row is the full execution object, `result` included, so one call is enough to render a history view. Keep requesting with `after` until `has_more` is `false`. This script pages through a connection's history two runs at a time. Set `APPLICATION_ID` to your application's id and `ACCESS_TOKEN` to the token from the connection's code exchange before running it: ```bash title="history.sh" #!/usr/bin/env bash # List a connection's executions two at a time until the last page. set -euo pipefail API="${WOOL_API_ORIGIN:-https://api.trywool.com}" after="" while :; do page=$(curl -sS "$API/v1/applications/$APPLICATION_ID/executions?limit=2${after:+&after=$after}" \ -H "Authorization: Bearer $ACCESS_TOKEN") echo "$page" | node -e 'const p=JSON.parse(require("fs").readFileSync(0,"utf8"));for(const e of p.executions)console.log(e.id,e.status,e.terminal?.status??"")' [ "$(echo "$page" | node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(0,"utf8")).has_more))')" = "true" ] || break after=$(echo "$page" | node -e 'const p=JSON.parse(require("fs").readFileSync(0,"utf8"));process.stdout.write(p.executions.at(-1).id)') done ``` The history belongs to the connection, not the token, so a user who reconnects sees the same runs through their new token. ## Next steps [#next-steps] [Handling failures](/docs/guides/failures) tells you what to do when a poll comes back `FAILED`. # Writing prompts that succeed (https://trywool.com/docs/guides/prompts) A prompt is the instruction for one run on a user's account. How you phrase it decides whether the run completes. ## The shape of a good prompt [#the-shape-of-a-good-prompt] Say three things: 1. **Where.** The area of the site, if it isn't obvious: "In Sent mail", "On the Orders page". 2. **What.** One task, with the specific item named: "the three newest emails", "invoice 4821". 3. **What done looks like.** The result you want back, or the exact effect you want. A vague prompt and a clear one for the same task: ```text Check my email. ``` ```text Summarize my three newest emails from the inbox: for each, give the sender, the subject, and one sentence on what it asks me to do. ``` The first leaves the agent to guess what "check" means and when it's done. The second names the place, the count, and the shape of the answer, so the run knows when it's finished and WOOL knows what to confirm. ## One task per run [#one-task-per-run] For example, in a connected email account, split "Find the newest message from Dana and reply to it" into two runs. First find the message and return its sender, subject, and timestamp. Then use those details to identify the message in a run with the exact reply to send. You can inspect the first result before sending the reply. If the reply run fails, check its evidence before deciding whether to retry it. ## Narrow with `start_url` [#narrow-with-start_url] `start_url` tells the run where to begin, as a path or URL on the connected website. For an account on a shopping site with an Orders page, a request could look like this: ```json { "prompt": "List the orders shown here with their totals.", "start_url": "/orders?status=open" } ``` Starting on the right page saves the run the work of finding it. It's the first thing to try when a run fails with `no_progress` or `budget_exhausted`. A URL off the connected origin returns `400 INVALID_START_URL`. ## Ask for data with a schema [#ask-for-data-with-a-schema] If your code needs fields, pass an `output_schema` and read `result.extracted` instead of parsing prose. The run then knows which values to find, and it fails if a required value can't be backed by the page. [Structured results](/docs/guides/structured-results) has the rules. ## Prompts with side effects [#prompts-with-side-effects] A side effect is a change the run makes on the website, such as sending a message or deleting a record. Name the exact change you want. For a connected email account, a reply prompt could be: ```text Reply to the newest email from Dana with: "Confirmed for Thursday at 10." Send it and stop. ``` Avoid conditions ("if it looks urgent, also…") and open-ended instructions ("handle my inbox"), which give the run room to do more than you meant. `result.evidence.side_effects` shows the action and what the page showed afterwards, so read it before anything else depends on the run. ## What a prompt can't do [#what-a-prompt-cant-do] * Signing in, or anything on a site the user didn't connect, fails with `session_expired` or `blocked_policy`. * CAPTCHAs and other human checks fail with `needs_user`. * Data the page doesn't show fails with `not_possible` or `unverified` rather than an invented answer. Design your product around those reasons instead of trying to prompt through them. ## Next steps [#next-steps] Try a prompt in the [playground](/docs/guides/playground) and read its evidence, then wire up [Handling failures](/docs/guides/failures). # Re-authorization (https://trywool.com/docs/guides/reauthorization) Websites end sessions on their own schedule. When a run finds the connected account signed out, it fails with `session_expired` and the connection becomes `REAUTHORIZATION_REQUIRED`. ## What changes [#what-changes] Reading the connection and its executions still works, including the run that failed and its evidence. Creating a run returns `409 CONNECTION_REAUTHORIZATION_REQUIRED`. Retrying an earlier create request with the same `Idempotency-Key` and body still returns its existing execution. This is an [idempotent replay](/docs/guides/idempotency), which retrieves the original run without starting another. ## Recovering [#recovering] Send the user through the auth link exactly as the first time. They sign in again on the connect page, the connection returns to `ACTIVE`, and the code exchange issues a new token. `connection_id` and `user_id` don't change, so replace the stored token and carry on. Check `terminal.status` on every failed run, and when you see `session_expired`, show the user what happened with the auth link ready, so reconnecting is one click. ## Next steps [#next-steps] [Connections and authorization](/docs/concepts/connections) explains the connection statuses. # Structured results (https://trywool.com/docs/guides/structured-results) By default a run answers in prose, in `result.final_text`. Pass an `output_schema` with the prompt and the run also returns `result.extracted` in that shape. The run completes only if the result matches the schema. ## Write the schema [#write-the-schema] An `output_schema` is a JSON Schema that describes the fields and value types you want in `result.extracted`. For a connected account on a shopping website, this request asks for recent orders as a list of objects: ```json { "prompt": "Find my three most recent orders.", "output_schema": { "type": "object", "properties": { "orders": { "type": "array", "items": { "type": "object", "properties": { "number": { "type": "string" }, "placed_on": { "type": "string" }, "total": { "type": "number" }, "status": { "type": "string", "enum": ["processing", "shipped", "delivered"] } }, "required": ["number", "total"] } } }, "required": ["orders"] } } ``` When this run completes, `result.extracted` has an `orders` array in which every order has at least a `number` and a `total`. `result.final_text` still carries the prose answer. ## The rules [#the-rules] The API rejects a schema outside these limits with `400 INVALID_OUTPUT_SCHEMA`: * Object root. * Objects, arrays, strings, numbers, booleans, and enums. No `$ref`, formats, or conditionals. * Depth at most 4. * At most 16 KiB. The schema describes shape only. For stricter checks, such as a date format, validate `result.extracted` on your side. ## When the page doesn't have the data [#when-the-page-doesnt-have-the-data] If a required field can't be filled from what the page shows, the run fails with `unverified` or `not_possible` rather than inventing a value. Make a field optional if it's sometimes absent on the page. ## Without a schema [#without-a-schema] Leave `output_schema` out and read `result.final_text`. `result.extracted` may still hold whatever the run chose to structure, with no guarantee about its shape, so don't build on it without a schema. ## Next steps [#next-steps] [Results and evidence](/docs/concepts/results) explains what WOOL confirms before a run completes. # Conventions (https://trywool.com/docs/reference/conventions) The OAuth, Executions, and Contract pages in the API reference are generated from the OpenAPI contract at [`https://api.trywool.com/v1/openapi.yaml`](https://api.trywool.com/v1/openapi.yaml), so they describe the API that's live. The conventions below apply to those endpoints and their request and response fields. ## What the API covers [#what-the-api-covers] The OAuth flow that connects a user's account, and executions on that connection. Managing applications, icons, and connections happens in the [console](/docs/console). ## Resources [#resources] Execution routes sit under your application: `/v1/applications/{id}/executions`. The connection access token implies its connection, so the routes carry no connection id. A mismatch between the application in the path and the token is `404`. ## Credentials [#credentials] * Execution routes take `Authorization: Bearer` with a connection access token. * The token endpoint takes form fields, with HTTP Basic for confidential clients. ## Field names and pagination [#field-names-and-pagination] Fields are `snake_case`. Timestamps are ISO 8601 in UTC, and ids are UUIDs. Lists take `limit` and `after` and return `has_more`. ## Limits [#limits] | Value | Limit | | --------------- | ------------------------------------------------- | | Prompt | 1–10,000 characters | | `start_url` | On the connected origin, at most 2,048 characters | | `output_schema` | Depth 4, 16 KiB | | Result | 256 KiB | | Request body | 64 KiB | ## Versioning [#versioning] Every route lives under `/v1`. Additive changes, such as a new field or route, arrive without notice and appear in the [changelog](/docs/changelog). A breaking change gets a new version prefix and a migration note before the old one goes away. ## Cross-origin requests [#cross-origin-requests] The OAuth routes, the execution routes, and the contract answer cross-origin requests from any page, so a browser app can call them directly. ## Next steps [#next-steps] Browse the reference by group in the sidebar, or generate a client from the contract, as [For coding agents](/docs/agents) suggests. # The auth link (https://trywool.com/docs/reference/oauth/authorize) The auth link. Send a user here to connect a website account to your application. The connect site, rather than the API, serves this page. It shows your application's name and icon and the website, asks the user to approve, and signs them in. It then redirects to `redirect_uri` with `code` and `state`, or with `error` (`access_denied` when the user declines, `server_error` when the sign-in fails) and `state`. An authorization expires 15 minutes after it starts; a code must be exchanged within 30 minutes. PKCE (`code_challenge` with S256) is optional for every client and recommended for public ones, because without a secret it is the only thing that ties the code exchange to the app that started it. See the contract at https://api.trywool.com/v1/openapi.yaml. # Exchange a code for a token (https://trywool.com/docs/reference/oauth/exchangeCode) Exchanges an authorization code for a connection access token. If your application is public, send `client_id` in the form body, plus the PKCE `code_verifier` when you sent a `code_challenge` on the auth link. If it's confidential, authenticate with HTTP Basic `client_id:client_secret`. The token is long-lived and doesn't refresh; issuing a new one for the same connection revokes the previous one. An invalid, expired, or used code returns `400 invalid_grant`. See the contract at https://api.trywool.com/v1/openapi.yaml. # Revoke a token (https://trywool.com/docs/reference/oauth/revokeToken) Revoke a connection access token. For a public application, send its `client_id` in the form body. For a confidential application, authenticate with HTTP Basic using `client_id:client_secret`. Use the application the token belongs to. The endpoint returns `200` whether or not the token was valid. The connection itself stays; revoke it from the console to delete its saved sign-in. See the contract at https://api.trywool.com/v1/openapi.yaml. # List executions (https://trywool.com/docs/reference/executions/listExecutions) Lists the connection's executions, newest first. The connection is the one your access token belongs to. A connection that needs re-authorization still lists its history, so you can show users what happened before they reconnect. See the contract at https://api.trywool.com/v1/openapi.yaml. # Create an execution (https://trywool.com/docs/reference/executions/createExecution) Starts an execution on the connection identified by your bearer access token and returns it as `QUEUED`. Send `Idempotency-Key` (1–255 characters) and reuse it with the same request body and connection on retries to retrieve the original execution. The same key with a different body returns `409 IDEMPOTENCY_KEY_REUSED`. If the connection needs the user to sign in again, you'll get `409 CONNECTION_REAUTHORIZATION_REQUIRED`; if your credit balance is empty, `402 INSUFFICIENT_CREDITS`. Use the returned `execution.id` to poll `GET /v1/applications/{applicationId}/executions/{executionId}` with the same access token until its `status` is `COMPLETED`, which means it succeeded and `result` is set, or `FAILED`, where `terminal.status` names the reason. See the contract at https://api.trywool.com/v1/openapi.yaml. # Get an execution (https://trywool.com/docs/reference/executions/getExecution) Return the execution identified by `executionId`, using the access token for its connection. See the contract at https://api.trywool.com/v1/openapi.yaml. # Get the contract (https://trywool.com/docs/reference/contract/getOpenApi) Return the OpenAPI document describing the WOOL API's routes, request fields, and responses. See the contract at https://api.trywool.com/v1/openapi.yaml.