> ## Documentation Index
> Fetch the complete documentation index at: https://www.1password.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a user

> Run the OAuth 2.0 authorization code flow with PKCE so a person can connect their 1Password account to your product and you can receive the integration key.

export const StatusBadge = ({children}) => <span className="op-status-badge not-prose">{children}</span>;

<StatusBadge>Partner preview</StatusBadge>

Each person connects their 1Password account to your product once. The flow follows the OAuth 2.0 authorization code flow with PKCE, with one addition: the first time a person connects, 1Password also returns an **integration key** in the URL fragment of your callback. You need both the tokens and the integration key to request or use that person's logins.

A connection lets you create access requests for that person. It doesn't let you read anything until they approve a request.

## The connection flow

```mermaid theme={null}
sequenceDiagram
    actor User
    participant FE as Your frontend
    participant BE as Your backend
    participant Web as 1Password web
    participant API as 1Password API

    User->>FE: Select "Connect 1Password"
    FE->>BE: Begin the OAuth flow
    BE->>BE: Create state and PKCE values
    BE-->>User: Redirect to the authorize endpoint
    User->>Web: Sign in to 1Password
    Web-->>User: Show the consent screen
    User->>Web: Approve
    opt First consent, or first consent after revocation
        Web->>Web: Create the integration key in the browser
        Note over Web,API: The integration key never reaches 1Password servers
    end
    Web-->>User: Redirect with code and state, and the integration key in the fragment
    User->>FE: Load your callback page
    FE->>FE: Read and store the integration key, then clear the fragment
    FE->>BE: Send the code and state
    BE->>BE: Validate state
    BE->>API: Exchange the code at the token endpoint
    API-->>BE: Access token and refresh token
```

## Base URLs and endpoints

1Password has two hosts. The person's browser goes to the web origin to sign in and consent. Your backend calls the API origin for tokens and revocation.

| Origin | URL |
| - | - |
| Web origin | `https://my.1password.com` |
| API origin | `https://api.1password.com` |

Test against these production origins. The development environment, `b5dev.eu`, can't show approval prompts with current 1Password app builds, and its tokens and integration keys don't work on production.

| Endpoint | Called from | URL |
| - | - | - |
| Authorize | The person's browser | `GET {web origin}/oauth/authorize` |
| Token | Your backend | `POST {api origin}/v1/oauth/token` |
| Revoke | Your backend | `POST {api origin}/v1alpha1/oauth-integrations/self:revoke` |

<Warning>
  The authorize path has no version prefix. `/v1/oauth/authorize` sends the person to the 1Password home page instead of the consent screen. The token path does have one: `/oauth/token` without `/v1` returns 404.
</Warning>

Everyone authorizes at the environment's web origin, regardless of their account address. A person whose account is at `acme.1password.com` still authorizes at `https://my.1password.com`. Don't ask the person for their account address or build an origin from it.

## Register your redirect URI

Register your callback on your OAuth client, then send it character for character in every authorize and token request. The console doesn't show the registered value again, so record it when you create the client.

A redirect URI must use `https` and a hostname, with no user info and no fragment. Custom URL schemes and `http://localhost` aren't accepted. A native mobile app needs an `https` callback, such as a universal link or app link, or a web page that hands the result back to the app.

## Start authorization

<Tip>
  To send one link that also starts a sign-up for people who don't have 1Password yet, and connects inside 1Password for iOS, see [Build a connect link](/agentic-autofill/partners/connect-links). It's in development.
</Tip>

Redirect the person's browser to the authorize endpoint:

```text theme={null}
GET {web origin}/oauth/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=https%3A%2F%2Fpartner.example.com%2Fcallback
  &scope=brokered-credentials%3Arequest-access%20brokered-credentials%3Aread
  &state=<opaque value you generate>
  &code_challenge=<base64url SHA-256 of the code verifier>
  &code_challenge_method=S256
```

<ParamField query="response_type" type="string" required>
  Always `code`.
</ParamField>

<ParamField query="client_id" type="string" required>
  Your OAuth client ID. It isn't a secret.
</ParamField>

<ParamField query="redirect_uri" type="string" required>
  Exactly one of the redirect URIs registered on your client, URL-encoded.
</ParamField>

<ParamField query="scope" type="string" required>
  The two scopes, space-delimited: `brokered-credentials:request-access brokered-credentials:read`. Encode the separating space as `%20`.
</ParamField>

<ParamField query="state" type="string" required>
  A unique, unpredictable value for this attempt. Bind it to the person's pending connection and check it on the callback.
</ParamField>

<ParamField query="code_challenge" type="string" required>
  `BASE64URL(SHA256(code_verifier))`, where `code_verifier` is a high-entropy value you generate and keep on your server for this attempt.
</ParamField>

<ParamField query="code_challenge_method" type="string" required>
  Always `S256`. `plain` isn't supported.
</ParamField>

Assembled, an authorization URL looks like this:

```text theme={null}
https://my.1password.com/oauth/authorize?response_type=code&client_id=A1B2C3D4E5F6G7H8J9K0LMNPQR&redirect_uri=https%3A%2F%2Fpartner.example.com%2Fcallback&scope=brokered-credentials%3Arequest-access%20brokered-credentials%3Aread&state=7f3c1e9a2b5d4f80&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
```

## The person signs in and consents

1Password signs the person in and shows the consent screen, which lists what your integration can and can't do. The person accepts or declines.

The consent screen appears for Individual and Family accounts. Other account types, including Business accounts, land on the 1Password home page, so don't use your company's Business account as a test user.

The consent screen shows the name and icon from your OAuth client registration.

## Handle the callback

When 1Password creates a new integration key, it redirects to your callback with the code and `state` in the query string and the `integration_key` in the fragment:

```text theme={null}
https://partner.example.com/callback?code=abc&state=opaque#integration_key=ops_eyJ2ZXIiOjEsInR5cCI6...
```

The query string can also carry `iss`, the issuer, and `base_url`, the API origin for the person's environment.

When the person connects again and their integration key is still active, the fragment is absent:

```text theme={null}
https://partner.example.com/callback?code=abc&state=opaque
```

If the person declines:

```text theme={null}
https://partner.example.com/callback?error=access_denied&state=<echoed state>
```

Browsers never send the fragment to a server, so your callback has to be a page that runs in the browser. It reads the integration key, stores it, and only then clears it from the URL:

```typescript callback.ts theme={null}
const integrationKey = new URLSearchParams(
  window.location.hash.replace(/^#/, ""),
).get("integration_key") ?? undefined;

if (integrationKey) {
  // Store it first. Clear the URL only once it is stored.
  await storeIntegrationKeyToken(integrationKey);
  history.replaceState(
    null,
    "",
    window.location.pathname + window.location.search,
  );
}

// An absent key means this connection already has an active one.
// Keep the key you stored earlier.
```

Then send the code and `state` to your backend. Validate `state` on the backend before you accept the result: reject a missing, mismatched, or replayed value. The code is single use and expires quickly, and it's bound to the client, the person, their account, the scope, the redirect URI, and the PKCE challenge.

A missing `integration_key` is a normal result, not an error. See [When you receive a key](/agentic-autofill/partners/keys-and-tokens#when-you-receive-a-key).

## Exchange the code

Your backend exchanges the code directly with the token endpoint:

```http theme={null}
POST {api origin}/v1/oauth/token
Authorization: Basic base64(<client_id>:<client_secret>)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<redirect uri>&code_verifier=<code verifier>
```

| Form parameter | Value |
| - | - |
| `grant_type` | `authorization_code` |
| `code` | The code from the callback |
| `redirect_uri` | The exact redirect URI you sent to the authorize endpoint |
| `code_verifier` | The code verifier for this attempt |

Authenticate with HTTP Basic only. Don't also send `client_id` in the body: the token endpoint rejects a request that uses both.

```json Response theme={null}
{
  "access_token": "op_o_u_<user-id>_<secret>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "op_o_r_<user-id>_<secret>",
  "scope": "brokered-credentials:request-access brokered-credentials:read"
}
```

The response can also include `user_id`. Store the access token and refresh token in your backend, and never expose the client secret or the code verifier to browser code. Treat the token strings as opaque.

## Refresh the access token

Access tokens expire after 15 minutes. Refresh from your backend:

```http theme={null}
POST {api origin}/v1/oauth/token
Authorization: Basic base64(<client_id>:<client_secret>)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<refresh token>&scope=brokered-credentials%3Arequest-access+brokered-credentials%3Aread
```

1Password returns a new access token and a new refresh token. The old refresh token stops working, so replace it atomically and serialize refresh requests per person to prevent competing token rotations. An `invalid_grant` response to a refresh means the connection has ended: ask the person to connect again.

## Errors

| Error | Where | Meaning |
| - | - | - |
| `access_denied` | Callback | The person declined the consent screen. |
| `invalid_request` | Token endpoint | A parameter is missing or malformed, or you sent client credentials twice (HTTP Basic and `client_id` in the body). |
| `invalid_client` | Token endpoint | The client ID or secret is wrong, or the client belongs to another environment. |
| `invalid_grant` | Token endpoint | The code was used already or expired, the code verifier or redirect URI doesn't match, or the refresh token was rotated, revoked, or expired. |
| `invalid_scope` | Authorize or token endpoint | The requested scopes don't match the two listed above. |
| `unsupported_response_type` | Authorize endpoint | `response_type` isn't `code`. |

## Checklist

* A **Connect 1Password** action in your product.
* A backend endpoint that creates `state`, the code verifier, and the S256 code challenge, then starts the redirect.
* An `https` callback, registered exactly on your OAuth client.
* A callback page that reads `integration_key` from the fragment, stores it before anything else that can fail, then clears the fragment. It keeps the key out of analytics and error reporting.
* Backend handling that validates `state` and rejects missing, mismatched, or replayed values.
* A backend code exchange, with the client secret and code verifier never exposed to browser code.
* Encrypted backend storage for tokens, with logs and error messages that redact secrets.
* Refresh-token rotation, and a reconnect path for connections that expire or are revoked.
* A **Disconnect 1Password** action. See [Disconnect a user](/agentic-autofill/partners/disconnect).


## Related topics

- [user](/cli/reference/management-commands/user.md)
- [Manage Connect servers](/connect/manage-connect.md)
- [1Password Connect Server API reference](/connect/api-reference.md)
