Skip to content

Continue with Configure

Continue with Configure button

Use Continue with Configure when your app already has sign-in or account linking. Configure authenticates the user on accounts.configure.dev, redirects to your callback with an authorization code, and lets your backend read and write the approved profile through the SDK.

The authorization page is hosted for every developer agent and supports multiple Gmail accounts by default. No hosted-SSO or Gmail rollout flag is required. Your integration chooses this redirect flow by using the Continue with Configure button; message agents send their sign_in_url instead.

For chat products, pair SSO with Inline UI Components. SSO connects the user's identity; inline Configure gives them ongoing controls for permissions, connected tools, and personalization. Use Link-only when your product has no sign-in flow or no place for an SSO button.

Create a client

Create an OAuth SSO client for one of your developer-owned agents:

http
POST /v1/developer/oauth/clients
Authorization: Bearer <developer_token>
Content-Type: application/json

{
  "agent": "your-agent",
  "client_name": "Your App",
  "redirect_uris": ["https://app.example.com/auth/configure/callback"],
  "scope": "profile.read profile.search profile.remember profile.commit"
}

The response includes a client_id and, for confidential web clients, a one-time client_secret. Store the secret server-side. Do not put it in browser JavaScript. This guide shows the confidential web-client flow, where your server holds the client_secret and runs the token exchange.

Use a developer dashboard bearer token for this request, not an sk_ API key. Today that token is issued when you sign in to the developer dashboard at https://configure.dev/login; until the OAuth client UI ships, advanced integrations can copy the configure_developer_token value from the dashboard session and send it as Authorization: Bearer <developer_token>.

json
{
  "client_id": "oc_...",
  "client_secret": "ocs_...",
  "client_name": "Your App",
  "agent": "your-agent",
  "redirect_uris": ["https://app.example.com/auth/configure/callback"],
  "scope": "profile.read profile.search profile.remember profile.commit",
  "resource": "https://api.configure.dev"
}

Add the button

Add the hosted script and button beside your existing SSO options:

html
<script src="https://configure.dev/js/configure.js"></script>

<configure-sso-button
  client-id="oc_..."
  redirect-uri="https://app.example.com/auth/configure/callback"
  scopes="profile.read profile.search profile.remember profile.commit"
  width="100%">
</configure-sso-button>

The button opens https://accounts.configure.dev/oauth/authorize in a popup by default, generates a PKCE challenge, and uses the grey Configure SVG brandmark on a white button with black text.

Default dimensions are 240px wide by 40px high, keeping the full "Continue with Configure" label visible while staying inside common OAuth button bounds. Google caps custom button width at 400px and recommends third-party sign-in buttons be approximately the same size and visual weight; Apple allows web button heights from 30px to 64px and widths from 130px to 375px or 100%.

Reference baselines: Google Identity button reference, Apple web button guidance, and Microsoft sign-in branding.

Choose the Configure button size by matching the OAuth provider buttons already in your product. Measure the rendered Google, Apple, Microsoft, or other sign-in rows and use the Configure size that keeps every button in the same group the same visual height:

Existing provider rowConfigure size
32px compact rowssize="compact"
40px standard rowsomit size or use size="standard"
48px large rowssize="large"

Use width="100%" only inside a fixed-width host row where every provider button shares the same visual width and height. Good host rows are usually 240px to 400px wide. The default brandmark renders at 18px square and compact mode renders it at 16px square.

Use launch-mode="iframe" when you want the Continue with Configure flow to stay in a closeable in-page modal:

html
<configure-sso-button
  client-id="oc_..."
  redirect-uri="https://app.example.com/auth/configure/callback"
  scopes="profile.read profile.search profile.remember profile.commit"
  launch-mode="iframe"
  width="100%">
</configure-sso-button>

The iframe mode still uses the OAuth authorization endpoint and PKCE. Configure allows the authorization page to be framed only by the registered redirect origin for that client. Your callback page must also be frameable by your own app and call Configure.completeSso() after your backend exchanges the code.

Use launch-mode="redirect" when your environment blocks popups and iframes.

Programmatic equivalent:

js
Configure.ssoButton({
  el: "#configure-sso",
  clientId: "oc_...",
  redirectUri: "https://app.example.com/auth/configure/callback",
  scopes: ["profile.read", "profile.search", "profile.remember", "profile.commit"],
  launchMode: "iframe",
  width: "100%",
});

For programmatic mounts, use a plain container such as <div id="configure-sso"></div>. Do not put data-configure-sso on that container. The data-configure-sso attribute is reserved for declarative auto-mounting and expects client-id or data-client-id on the same element:

html
<div
  data-configure-sso
  data-client-id="oc_..."
  data-redirect-uri="https://app.example.com/auth/configure/callback"
  data-scopes="profile.read profile.search profile.remember profile.commit">
</div>

If your backend already generated state, code_challenge, and the full authorization URL, pass authorizationUrl to the helper instead. For iframe launches, include frame_origin=<your app origin> in the authorization URL. Keep the matching code_verifier in your server session.

Handle the callback

The callback page receives code and state. Exchange the code on your backend, store Configure tokens server-side, then notify the opener and close the popup.

The drop-in helper stores the PKCE verifier under your app origin in both localStorage and sessionStorage with a 30-minute TTL, so redirect mode can survive a full cross-origin round trip. Configure.getSsoPkce(state) checks both stores. Configure.completeSso({ state, ... }) clears the verifier after a successful exchange; call Configure.clearSsoPkce(state) yourself if you complete the flow without completeSso().

html
<script src="https://configure.dev/js/configure.js"></script>
<script>
  async function finishConfigureSso() {
    const params = new URLSearchParams(window.location.search);
    const code = params.get("code");
    const state = params.get("state");
    const error = params.get("error");

    if (error) {
      Configure.completeSso({ error, payload: { message: error } });
      return;
    }

    const pkce = Configure.getSsoPkce(state);
    if (!pkce || !pkce.codeVerifier) {
      Configure.completeSso({
        error: "pkce_missing",
        payload: { message: "Configure sign-in expired. Please try again." },
      });
      return;
    }

    const res = await fetch("/api/configure/oauth/callback", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        code,
        state,
        codeVerifier: pkce.codeVerifier,
        // Must equal a registered redirect_uri byte-for-byte; send the
        // registered constant, not a value rebuilt from window.location.
        redirectUri: "https://app.example.com/auth/configure/callback",
        resource: "https://api.configure.dev",
      }),
    });

    if (!res.ok) {
      Configure.completeSso({
        error: "callback_failed",
        payload: { message: "Configure sign-in failed." },
      });
      return;
    }

    const result = await res.json();
    Configure.completeSso({ state, payload: result });
  }

  finishConfigureSso();
</script>

Configure.completeSso() notifies either the popup opener or the parent iframe, then closes the popup when one exists. In iframe mode the modal closes from the parent page after it receives the callback message.

Server exchange example:

ts
app.post("/api/configure/oauth/callback", async (req, res) => {
  const { code, codeVerifier, redirectUri, resource } = req.body;

  const tokenRes = await fetch("https://api.configure.dev/oauth/token", {
    method: "POST",
    headers: {
      "Authorization": "Basic " + Buffer.from(
        process.env.CONFIGURE_OAUTH_CLIENT_ID + ":" +
        process.env.CONFIGURE_OAUTH_CLIENT_SECRET
      ).toString("base64"),
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: redirectUri,
      client_id: process.env.CONFIGURE_OAUTH_CLIENT_ID!,
      code_verifier: codeVerifier,
      resource: resource || "https://api.configure.dev",
    }),
  });

  const tokens = await tokenRes.json();
  if (!tokenRes.ok) {
    return res.status(400).json({ error: "configure_oauth_failed" });
  }

  req.session.configureAccessToken = tokens.access_token;
  req.session.configureRefreshToken = tokens.refresh_token;
  res.json({ connected: true });
});

Validate state in your app before exchanging the code. The example uses browser-generated PKCE for the drop-in path. Apps that already have a session-backed OAuth helper can generate their own state and codeVerifier, persist the verifier in durable app storage, and pass both into Configure.ssoButton({ state, codeVerifier, ... }).

The redirect_uri you send to /oauth/token must exactly match the value used at /oauth/authorize. On a mismatch the token endpoint returns 400 invalid_grant, "redirect_uri does not match authorization code." The authorize endpoint checks registration first and rejects an unregistered value with invalid_request, "redirect_uri must exactly match client registration." Reconstructing the URI from window.location only works when the callback page is served at exactly the registered path, with no trailing-slash or query difference; send the registered constant instead, as the callback snippet does.

A successful exchange returns:

json
{
  "access_token": "cfg_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "cfg_rt_...",
  "scope": "profile.read profile.search profile.remember profile.commit",
  "resource": "https://api.configure.dev"
}

Token prefixes follow the audience: cfg_ for https://api.configure.dev, mcp_ for https://mcp.configure.dev.

When the access token expires, exchange the refresh token for a new one (same endpoint and Basic auth, grant_type=refresh_token):

ts
body: new URLSearchParams({
  grant_type: "refresh_token",
  refresh_token: req.session.configureRefreshToken,
  client_id: process.env.CONFIGURE_OAUTH_CLIENT_ID!,
  resource: "https://api.configure.dev",
});

Keep resource in the refresh body. When omitted, the audience defaults to https://mcp.configure.dev and the refreshed pair comes back with mcp_ prefixes. Sibling cross-accept keeps those tokens working at the API, but the audience and prefix silently change.

Common exchange failures

ResponseCause
401The client_id:client_secret Basic-auth pair is wrong (bad or lost one-time secret).
400 invalid_grantThe code_verifier does not match the code_challenge, the redirect_uri does not match the one used at /oauth/authorize, or the code is expired (5-minute TTL) or already used.
400 invalid_requestA required body param (code, redirect_uri, client_id, code_verifier) is missing or blank.

An expired PKCE verifier (older than its 30-minute storage TTL) never reaches the token endpoint. Configure.getSsoPkce(state) returns null and the callback snippet takes its pkce_missing path instead.

Use profile context and tools

Three credentials are in play, and they do different jobs. Keep them straight:

CredentialWhere it comes fromWhat it does
Developer token (configure_developer_token)Copied from the developer dashboard sessionAuthorizes creating OAuth clients (Create a client)
API key (sk_…) + agentnpx configure setupFor my users (writes CONFIGURE_API_KEY, CONFIGURE_AGENT)Resolves the acting agent server-side in the SDK
OAuth access_token / refresh_tokenThe token exchange aboveResolves the signed-in user, per request

resource in the exchange is the token's audience (RFC 8707); use https://api.configure.dev. The issued token is also valid at the sibling audience https://mcp.configure.dev, which is why the same token can drive both the SDK and a direct MCP connection.

If your agent already speaks MCP, point it at https://mcp.configure.dev with the access token; the tools below surface automatically. Use the SDK path that follows when you own the model loop in-process.

After the token exchange, use the Configure access token from your server session with the SDK (the apiKey here is the sk_… key from For my users setup, not the developer token):

ts
import { Configure, toOpenAIFunctions } from "configure";

const configure = new Configure({
  apiKey: process.env.CONFIGURE_API_KEY,
  agent: process.env.CONFIGURE_AGENT,
});

async function toolsForUser(req) {
  const profile = configure.profile({
    token: req.session.configureAccessToken,
  });
  const tools = profile.tools({
    connectors: ["gmail", "calendar"],
  });

  return {
    profile,
    tools: toOpenAIFunctions(tools),
  };
}

profile.tools() returns the default profile set: read, search, remember, and forget. Your tool router should expect configure_profile_forget calls; profile.executeTool() routes them to profile.forget(id, { date, reason }).

Action tools, such as sending email or creating calendar events, change external state. Add the actions your app supports to profile.tools() when your product has a confirmation or hosted approval path, then let profile.executeTool() enforce linked state, connector state, permissions, scopes, and approval state.

Route only Configure-prefixed tool calls back to Configure:

ts
const result = call.function.name.startsWith("configure_")
  ? await profile.executeTool({
      name: call.function.name,
      arguments: JSON.parse(call.function.arguments || "{}"),
    })
  : await executeYourTool(call);

The OAuth access token resolves the Configure user. Your sk_ key and configured agent resolve the acting agent server-side. Do not send user IDs, CFS paths, app display names, or browser-supplied agent names to choose the profile namespace.

Add inline Configure after SSO

After Continue with Configure succeeds, add Configure inside the chat UI. The user should not need to authenticate again. Inline Configure becomes the place to adjust permissions, connect Gmail/Calendar/Drive/Notion/Sheets, and toggle personalization for the current agent.

html
<script src="https://configure.dev/js/configure.js"></script>
<div id="configure-entry"></div>

<script>
  Configure.personalizationButton({
    el: "#configure-entry",
    publishableKey: "pk_...",
    agent: "your-agent",
    agentName: "Your Agent",
    personalizationLabel: "Manage Configure",
    variant: "integration",
    onEvent(event) {
      if (event.type === "configure:linked") {
        fetch("/api/configure/session", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            token: event.payload.token,
            userId: event.payload.userId,
          }),
        });
      }
    },
  });
</script>

Keep OAuth access and refresh tokens on the backend. Hosted Configure UI can reuse the Configure-origin session created during SSO. Pass a browser-safe Configure handoff token or userId only when your backend intentionally returns one for hosted UI; never pass OAuth access or refresh tokens to browser JavaScript.

If the user has not completed SSO, the same inline UI can fall back to Configure Link and emit configure:linked.

Scopes

Web SSO supports exactly four profile.* scopes:

ScopeAllows
profile.readGET /v1/profile and configure_profile_read
profile.searchGET /v1/profile/search and configure_profile_search
profile.rememberPOST /v1/profile/remember and configure_profile_remember, plus POST /v1/profile/forget and configure_profile_forget
profile.commitPOST /v1/profile/commit and configure_profile_commit

Forget carries the same own-namespace write authority as remember; there is no dedicated profile.forget scope. When you omit scope at client creation or scopes on the button, the default is profile.read profile.search. Request profile.remember and profile.commit explicitly, both when registering the client and in the button configuration.

Connector and action tools still require the user to connect the underlying account and your server to opt into those tools for the turn. Keep refresh tokens, access tokens, and connector credentials on the backend.

Connected state

For returning users, render the button with a connected state after your backend verifies the server-side Configure session:

html
<configure-sso-button
  client-id="oc_..."
  redirect-uri="https://app.example.com/auth/configure/callback"
  connected="true"
  connected-label="Configure connected"
  account-popover="inline">
</configure-sso-button>

Clicking a connected button opens a small account popover and can be closed without restarting auth. The popover does not expose tokens.

Personalization infrastructure for agents