# Sign in with Wire

Sign in with Wire lets a person sign in to your own app with their Wire account. Your app learns who they are to your agent, and your server can call your agent's tools as that person, with no container URL and no API key in the browser.

It is for agents whose manifest declares `access`. The sign-in is finished on your server, with a client secret. If your manifest does not declare `access`, nothing on this page applies and [`connectInBrowser()`](/build/sdk/connect-flows/#from-a-browser-app) works as before.

You need `@usewire/sdk` 0.17.0 or later. The helpers here are on the `@usewire/sdk/agent` entry and run on Node 18+, Cloudflare Workers, Bun, and Deno.

## What your app gets

A completed sign-in gives your server three things.

| You get | What it is | Where it lives |
|---|---|---|
| An identity | The person's id for your agent (`au_…`), and their email, name, and picture if your manifest asks for them | Your user record |
| An access token | What your server presents to your agent's endpoint to call your tools as that person | Your server only |
| A refresh token | A way to get a new access token later, when you ask for `offline_access` | Your server only |

The id is the same `agentUserId` your [install webhooks and agent API](/build/sdk/installs-webhooks/) use. It is stable for that person and your agent, another agent gets a different id for the same person, and it never reveals their Wire account.

## Declare what you ask for

The `access` block of your manifest says how far your app may reach into a person's container, and which identity fields it wants.

```typescript
export const manifest = defineManifest({
  manifest: 1,
  app: {
    id: 'someday',
    name: 'Someday',
    version: '2.0.0',
    privacy_policy_url: 'https://someday.example/privacy',
  },
  access: {
    level: 'write',
    read: 'Shows the places you saved.',
    write: 'Saves places you add.',
    identity: ['email'],
  },
  // objects, tools, ...
});
```

| `level` | What your app may do in the person's container |
|---|---|
| `none` | Nothing. Your app learns who the person is and no more. |
| `read` | Call your tools that do not change a record. |
| `write` | Call all of your tools. |

The rest of the block:

- **`read` and `write`** are one line each, in your words, saying what that access is for. Plain text, at most 200 characters, no links or markup. Give `read` with level `read` or `write`, and `write` only with level `write`.
- **`identity`** lists `email`, `profile` (name and picture), or both. Leave it out and your app gets the id alone.
- **`app.privacy_policy_url`** is required as soon as `identity` asks for anything. It must be an `https` page on your own domain.

Two rules follow from what your tools do:

- A manifest whose tools send container records to your own server may not declare `none`. A tool does that when an `after` action uses the tool's output.
- At any level, your app never reads the container's activity log or its uploaded files.

`defineManifest` checks all of this locally, the same way Wire does when you register the manifest.

## What the person sees

The first time someone signs in to your app, Wire shows a sign-in screen under your agent's name. It states, in Wire's words, what your app may do in their container at the level you declared, followed by your own `read` and `write` lines, marked as coming from you. It lists what your app is given: the id, and each identity field you asked for. It links to your privacy policy, showing the address's host as the link text.

The person approves once. On later sign-ins from the same app they are sent straight back to you.

If a new version of your manifest raises the level or adds an identity field, the person is shown the screen again before it applies. Lowering the level, dropping a field, or rewording a line does not ask them again.

Signing in to your app needs a Wire account. Someone trying your agent without an account cannot sign in this way.

## Set up your agent

Three things on your agent's page in Wire, all before you ship.

1. **Turn on your agent's endpoint.** Under **Connect**, turn the endpoint on. A sign-in connects the person to it, and it is what your server calls afterwards. While it is off, Wire refuses the sign-in and says so.
2. **Create a client secret.** Under **Sign-in**, create a client secret. It is shown once. Put it in your server's secret store. Creating a new one stops the old one at once.
3. **Add your callback address.** Add your server's callback URL to the agent's redirect addresses. Wire only sends people back to an address on that list, matched exactly.

The client secret must never reach anything a person can read: not a web page, a worker in the browser, or a mobile or desktop app.

## Sign people in on your server

The flow is an authorization code flow with PKCE. `WireSignIn` builds the request, exchanges the code, and verifies the result.

<Tabs syncKey="lang">
  <TabItem label="TypeScript">
    ```typescript
    const wire = new WireSignIn({
      agentId: 'someday',
      clientSecret: process.env.WIRE_CLIENT_SECRET!,
      redirectUri: 'https://someday.example/auth/wire/callback',
    });

    // 1. Start: send the browser to Wire.
    app.get('/auth/wire/start', async (req, res) => {
      const request = await wire.createAuthorizeRequest({ scope: ['email', 'offline_access'] });
      req.session.wire = { state: request.state, codeVerifier: request.codeVerifier, nonce: request.nonce };
      res.redirect(request.url);
    });

    // 2. Callback: Wire sends the browser back with ?code=...&state=...
    app.get('/auth/wire/callback', async (req, res) => {
      const kept = req.session.wire;
      if (!kept || req.query.state !== kept.state) return res.status(400).send('This sign-in did not start here.');
      delete req.session.wire;
      if (typeof req.query.code !== 'string') return res.redirect('/?signin=cancelled');

      let tokens;
      try {
        tokens = await wire.exchangeCode({ code: req.query.code, codeVerifier: kept.codeVerifier, nonce: kept.nonce });
      } catch {
        return res.redirect('/?signin=failed'); // a code works once: start again
      }

      const user = await upsertUser({ wireId: tokens.identity.agentUserId, email: tokens.identity.email });
      await saveTokens(user.id, tokens);

      req.session.regenerate(() => {
        req.session.userId = user.id;
        res.redirect('/');
      });
    });
    ```
  </TabItem>
</Tabs>

What each step does:

1. **`createAuthorizeRequest()`** returns the URL to send the browser to, plus a `state`, a `codeVerifier`, and a `nonce`. Keep those three in the person's server-side session.
2. **Check `state`** when the browser comes back. It must be the one you kept.
3. **`exchangeCode()`** trades the code for tokens, authenticated with your client secret. It verifies the ID token before it returns: signed by Wire, issued for your agent, not expired, tied to this sign-in by the `nonce`, and naming a per-agent id. If any check fails it throws and nobody is signed in.
4. **Make a new session** for the signed-in person. Do not promote the session they arrived with.

`scope` controls what you ask for on this sign-in. `openid` is always included. Add `email` and `profile` only if your manifest's `identity` lists them: Wire grants no more than the manifest declares. Add `offline_access` for a refresh token.

The request never names a container or a resource. Wire refuses one from an agent's own app.

### Preview

To run against Wire's preview environment, pass `baseUrl: 'https://preview.app.usewire.io'` to `WireSignIn`.

## Call your tools as the person

Your agent has one endpoint, on its own hostname. Besides `/mcp`, which AI clients use, it serves two REST paths for your server.

| Request | What it does |
|---|---|
| `GET /tools` | Lists the tools this sign-in may call |
| `POST /tools/{name}` | Calls one, with the tool's input as the JSON body |

Nothing else is served there. Your server never needs a container id: Wire finds the person's container from the access token.

<Tabs syncKey="lang">
  <TabItem label="TypeScript">
    ```typescript
    const endpoint = new WireAgentEndpoint({ agentId: 'someday' });

    const tools = await endpoint.listTools(accessToken);
    // [{ name: 'find_places', description, inputSchema, annotations }, ...]

    try {
      const { data } = await endpoint.callTool(accessToken, 'find_places', { q: 'ramen' });
    } catch (err) {
      if (err instanceof WireEndpointError && err.code === 'UNAUTHORIZED') {
        // the access token is no longer good: refresh it, then try again
      }
      throw err;
    }
    ```
  </TabItem>
  <TabItem label="HTTP">
    ```bash
    curl https://someday.agent.usewire.io/tools \
      -H "Authorization: Bearer $ACCESS_TOKEN"

    curl -X POST https://someday.agent.usewire.io/tools/find_places \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H 'content-type: application/json' \
      -d '{ "q": "ramen" }'
    ```
  </TabItem>
</Tabs>

What comes back depends on your manifest's level:

| Level | `GET /tools` | `POST /tools/{name}` |
|---|---|---|
| `none` | An empty list | Refused |
| `read` | Your tools that do not change a record | Those tools run. A tool that writes is refused, with the reason. |
| `write` | All of your tools | All of them run |

A refused call throws `WireEndpointError` with `code: 'TOOL_REFUSED'` and the tool's own message. A tool call is billed like the same call made by an AI client.

If your agent uses a custom hostname, pass it: `new WireAgentEndpoint({ agentId: 'someday', endpoint: 'https://mcp.someday.example' })`. Set it from your own configuration, never from a request, because the person's access token is sent there.

Make these calls from your server. Do not send an access token to a browser to call the endpoint from there.

## Refresh tokens

A refresh token works once. When Wire answers a refresh with new tokens, the one you sent is spent, and the answer carries a new one to store in its place.

Sending a spent refresh token later signs the person out of your agent: Wire treats it as stolen and ends every token your agent holds for them. Three rules keep that from happening by accident.

1. **Refresh one at a time per person.** Take a lock around reading the stored token, refreshing, and writing the new one. Two refreshes at once with the same token is a spent token sent twice.
2. **Store the new tokens first.** That includes when the call throws: if the error has `tokens`, Wire had already answered and the old token is spent.
3. **Retry only what is safe to retry.**

```typescript
await withLock(`wire-refresh:${user.id}`, async () => {
  const stored = await loadTokens(user.id);
  try {
    const next = await wire.refresh(stored.refreshToken);
    await saveTokens(user.id, next);
  } catch (err) {
    if (!(err instanceof WireSignInError)) throw err;
    if (err.tokens) return saveTokens(user.id, err.tokens); // Wire had answered: store these
    if (err.code === 'INVALID_GRANT') return signOut(user.id); // they sign in again
    throw err; // UNAVAILABLE or NETWORK_ERROR: see the table
  }
});
```

| The call ended with | Is your refresh token spent? | What to do |
|---|---|---|
| New tokens | Yes | Store the new ones. |
| An error with `tokens` | Yes | Store `err.tokens`. Do not retry. |
| `UNAVAILABLE` | Almost always no | Retry later with the same token. Wire decides everything before it replaces a token. The exception is a gateway error on the way back after Wire had answered: the retry then answers `INVALID_GRANT`, and the person signs in again. |
| `UNEXPECTED_RESPONSE` or `OAUTH_ERROR` without `tokens` | Unknown | Try once more, not in a loop. If that answers `INVALID_GRANT`, the person signs in again. |
| `NETWORK_ERROR` | Unknown | Retry once, right away, with the same token. If that answers `INVALID_GRANT`, the person signs in again. |
| `INVALID_GRANT` | It was already | The person signs in again. |

After a refresh, `identity` may be `null`: Wire can leave the ID token out of a refresh answer. The person is who they were, so keep the identity you stored at sign-in.

`INVALID_GRANT` also means the person disconnected your agent in Wire. Treat it as signed out, not as an error to retry.

## Demo accounts

A demo account is a login you create for someone who has to try your app and has no Wire account, such as an app store reviewer. Create one under **Demo accounts** on your agent's page. Wire shows its email and password once, so copy both then.

When your agent has a demo account, Wire's sign-in screen for your agent adds a line: "Have a demo account? Sign in with it." The reviewer enters the email and password there and carries on through the same screens as anyone else, so what they review is your real sign-in.

| What | For a demo account |
|---|---|
| How many | 3 per agent |
| Where the password works | Only while signing in to your agent |
| How long a sign-in lasts | One day. After that the reviewer signs in again with the same password. |
| What it can keep | Up to 3 containers |
| Who pays | Your organization pays for what it uses with your agent |

Wrong passwords are limited by where they come from, never by account, so nobody can lock your reviewer out by guessing.

Delete a demo account on the same page when the review is over. It can no longer sign in from that moment, and what it saved is removed, usually within a day.

## Move from in-browser connect

If your app calls `connectInBrowser()` today, the move has one rule: the manifest's `access` block and your server-side sign-in ship together. Once a manifest with `access` is registered, your agent's sign-in needs its client secret, and `connectInBrowser()`, which finishes in the browser without one, stops working for it. It throws `WireSdkError` with `code: 'ACCESS_AGENT_NEEDS_SERVER'`.

Do it in this order:

1. **Build the server side first, and register nothing new.** Add the start and callback routes, token storage keyed by `agentUserId`, and your calls to `WireAgentEndpoint`. Deploy it without linking to it.
2. **Turn on the endpoint and create the client secret** on your agent's page, and add your server's callback to the redirect addresses.
3. **In one release, register the manifest with `access`** and change your app's connect button from `connectInBrowser()` to a link to your start route.

People who connected before keep their container and everything in it. The first time each of them signs in, Wire shows what your manifest asks for and they approve it once, on the container they already use.

What changes in your code:

| Before, with `connectInBrowser()` | After, with Sign in with Wire |
|---|---|
| The browser finishes the connect and holds a `Connection`: an API key and a container's URL. | Your server finishes the sign-in and holds an access token. The browser holds your own session and nothing of Wire's. |
| You call the container's URL with the API key. | You call your agent's endpoint, `GET /tools` and `POST /tools/{name}`, with the access token. |
| `connection.agentUserId` identifies the person. | `tokens.identity.agentUserId` does. It is the same id for the same person, so your existing user records match. |
| Every tool of yours, plus the base tools your manifest lists. | Your tools, held to `access.level`. |

## Errors

Both clients throw typed errors with a `code` and a `retryable` flag.

| Error | `code` | Meaning |
|---|---|---|
| `WireSignInError` | `INVALID_GRANT` | The code, verifier, or refresh token was refused. The person signs in again. |
| | `INVALID_CLIENT` | The client secret was refused, or the agent has none. |
| | `INVALID_ID_TOKEN` | The ID token did not verify. Nobody is signed in. |
| | `UNAVAILABLE`, `NETWORK_ERROR` | Wire could not answer, or the request did not arrive. `retryable` is true unless the error carries `tokens`. |
| `WireEndpointError` | `UNAUTHORIZED` | The access token is no longer good. Refresh it. |
| | `TOOL_REFUSED` | The tool said no: not allowed at your level, unknown, or bad input. |
| | `INSUFFICIENT_CREDITS` | The call could not be paid for. |
| | `UNAVAILABLE`, `NETWORK_ERROR` | Try again. `retryAfterSec` is set when Wire said how long to wait. |

## Related

- [Connect flows](/build/sdk/connect-flows/): the device and in-browser flows, for agents that do not declare `access`.
- [Installs and webhooks](/build/sdk/installs-webhooks/): the same `agentUserId`, read from your server, and events when an install changes.
- [Custom tools](/guides/custom-tools/): the tools your app calls through the endpoint.