Get started

Quickstart

Connect a user's account and run your first execution from a public client.

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 shows how to authenticate the code exchange with a client secret and associate connections with your own user ids.

1. Create an application

Sign in to the console 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.

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 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.

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

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. 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.

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

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.

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:

{
  "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

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:

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.

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:

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 says what to do for each.

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.

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

Writing prompts that succeed shows how to phrase a prompt so the run completes. Structured results shows how to get JSON instead of prose.