> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bkmark.it/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth2 Authorization Code Flow — Bkmark Integration Guide

> Implement OAuth2 Authorization Code with PKCE to act on behalf of Bkmark users. Covers authorization, token exchange, refresh, and revocation.

Use the OAuth2 Authorization Code flow when you are building an application that acts on behalf of Bkmark users — a browser extension, a third-party integration, a mobile app, or a Zapier-style automation. The user grants your application specific permissions, and Bkmark issues your app a short-lived access token and a longer-lived refresh token. Your app never sees the user's credentials.

This flow follows [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) and supports [PKCE (RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636). PKCE is recommended for all clients and **required** for public clients such as single-page applications and mobile apps.

## Getting OAuth2 Credentials

Before you can start the flow, register your application to receive a `client_id` and `client_secret`.

Go to **Settings → Developer → OAuth2 Clients** in your Bkmark account and create a new client. You will need to provide:

* A display name for your application
* One or more allowed redirect URIs (exact match required)
* The scopes your application plans to request

***

## The Authorization Flow

<Steps>
  ### Generate PKCE Values

  PKCE binds the authorization request to the token exchange, preventing code interception attacks. Generate a fresh pair for every authorization attempt.

  ```javascript theme={null}
  import crypto from 'node:crypto';

  // Step 1: Generate a cryptographically random verifier (32 bytes → 43-char base64url string)
  const codeVerifier = crypto.randomBytes(32).toString('base64url');

  // Step 2: Hash the verifier with SHA-256 to produce the challenge
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  // Store codeVerifier in your server-side session — you'll need it in Step 4
  // Send codeChallenge (not the verifier) in the authorization request
  console.log({ codeVerifier, codeChallenge });
  // {
  //   codeVerifier:  'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
  //   codeChallenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'
  // }
  ```

  Also generate a random `state` value to protect against CSRF:

  ```javascript theme={null}
  const state = crypto.randomBytes(16).toString('hex');
  // Store state in your session alongside the codeVerifier
  ```

  ### Redirect to the Authorization Endpoint

  Redirect the user's browser to `https://api.bkmark.it/oauth/authorize` with the following query parameters:

  ```text theme={null}
  GET https://api.bkmark.it/oauth/authorize
    ?response_type=code
    &client_id=bk_your_client_id
    &redirect_uri=https://yourapp.com/oauth/callback
    &scope=bookmarks:read%20bookmarks:write%20tags:read
    &state=a8f3bc19d02e47c1
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256
  ```

  Bkmark displays a consent screen that lists exactly which permissions your app is requesting. The user can approve or deny.

  **Authorization endpoint parameters:**

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

  <ParamField query="client_id" type="string" required>
    Your application's client ID, obtained from Settings → Developer → OAuth2 Clients.
  </ParamField>

  <ParamField query="redirect_uri" type="string" required>
    The URI Bkmark redirects to after the user decides. Must exactly match one of the redirect URIs you registered.
  </ParamField>

  <ParamField query="scope" type="string" required>
    Space-separated list of permission scopes. See the [Scopes reference](/api/oauth/scopes) for all available values.
  </ParamField>

  <ParamField query="state" type="string" required>
    A random, unguessable string you generate per-request. Bkmark echoes it back in the callback. Verify it matches before continuing.
  </ParamField>

  <ParamField query="code_challenge" type="string">
    The SHA-256 hash of your `code_verifier`, encoded as base64url. Required for public clients; strongly recommended for all clients.
  </ParamField>

  <ParamField query="code_challenge_method" type="string">
    Must be `S256` when providing a `code_challenge`.
  </ParamField>

  ### Handle the Callback

  **On approval**, Bkmark redirects to your `redirect_uri` with an authorization code:

  ```text theme={null}
  GET https://yourapp.com/oauth/callback
    ?code=SplxlOBeZQQYbYS6WxSbIA
    &state=a8f3bc19d02e47c1
  ```

  **On denial**, Bkmark redirects with an error:

  ```text theme={null}
  GET https://yourapp.com/oauth/callback
    ?error=access_denied
    &state=a8f3bc19d02e47c1
  ```

  <Warning>
    **Always verify the `state` parameter.** Compare the value in the callback against the value you stored in the session before Step 2. If they don't match, abort the flow — this is a CSRF indicator. Never proceed with a mismatched or missing state.
  </Warning>

  ```javascript theme={null}
  // Example Express.js callback handler
  app.get('/oauth/callback', async (req, res) => {
    const { code, state, error } = req.query;

    // 1. Verify state to prevent CSRF
    if (state !== req.session.oauthState) {
      return res.status(403).send('State mismatch — possible CSRF attack');
    }

    // 2. Handle user denial
    if (error === 'access_denied') {
      return res.redirect('/connect?denied=1');
    }

    // 3. Exchange code for tokens (see next step)
    const tokens = await exchangeCodeForTokens(code, req.session.codeVerifier);
    req.session.accessToken = tokens.access_token;
    req.session.refreshToken = tokens.refresh_token;

    res.redirect('/dashboard');
  });
  ```

  ### Exchange the Code for Tokens

  The authorization code is single-use and expires quickly. Exchange it for an access token and refresh token by making a server-side `POST` request:

  ```bash theme={null}
  curl -X POST https://api.bkmark.it/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "code=SplxlOBeZQQYbYS6WxSbIA" \
    -d "redirect_uri=https://yourapp.com/oauth/callback" \
    -d "client_id=bk_your_client_id" \
    -d "client_secret=your_client_secret" \
    -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  ```

  **Token endpoint parameters:**

  <ParamField body="grant_type" type="string" required>
    Must be `authorization_code`.
  </ParamField>

  <ParamField body="code" type="string" required>
    The authorization code from the callback query string.
  </ParamField>

  <ParamField body="redirect_uri" type="string" required>
    Must match the `redirect_uri` used in Step 2 exactly.
  </ParamField>

  <ParamField body="client_id" type="string" required>
    Your application's client ID.
  </ParamField>

  <ParamField body="client_secret" type="string" required>
    Your application's client secret. Never expose this in client-side code.
  </ParamField>

  <ParamField body="code_verifier" type="string">
    The original random verifier you generated in Step 1. Required if you sent a `code_challenge` in the authorization request.
  </ParamField>

  **Successful response `200 OK`:**

  ```json theme={null}
  {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
    "scope": "bookmarks:read bookmarks:write tags:read"
  }
  ```

  <ResponseField name="access_token" type="string">
    Bearer token to include in `Authorization` headers. Expires in 1 hour (`expires_in: 3600`).
  </ResponseField>

  <ResponseField name="refresh_token" type="string">
    Long-lived token used to obtain new access tokens. Expires in 90 days.
  </ResponseField>

  <ResponseField name="expires_in" type="integer">
    Lifetime of the access token in seconds (currently `3600`).
  </ResponseField>

  <ResponseField name="scope" type="string">
    The scopes actually granted. May be a subset of what you requested if the user partially approved. Always check this field before making API calls.
  </ResponseField>

  ```javascript theme={null}
  // Full token exchange helper
  async function exchangeCodeForTokens(code, codeVerifier) {
    const params = new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://yourapp.com/oauth/callback',
      client_id: process.env.BKMARK_CLIENT_ID,
      client_secret: process.env.BKMARK_CLIENT_SECRET,
      code_verifier: codeVerifier,
    });

    const response = await fetch('https://api.bkmark.it/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString(),
    });

    if (!response.ok) {
      const err = await response.json();
      throw new Error(`Token exchange failed: ${err.error} — ${err.error_description}`);
    }

    return response.json();
  }
  ```

  ### Make Authenticated API Calls

  Include the access token as a `Bearer` token in the `Authorization` header on every API request:

  ```bash theme={null}
  curl https://api.bkmark.it/api/v1/bookmarks \
    -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
  ```

  ```javascript theme={null}
  async function getBookmarks(accessToken) {
    const response = await fetch('https://api.bkmark.it/api/v1/bookmarks', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    if (response.status === 401) {
      // Token expired — trigger the refresh flow
      throw new Error('TOKEN_EXPIRED');
    }

    return response.json();
  }
  ```

  ### Refresh the Access Token

  Access tokens expire after 1 hour. Use the refresh token to obtain a new pair without requiring the user to re-authorize.

  ```bash theme={null}
  curl -X POST https://api.bkmark.it/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=refresh_token" \
    -d "refresh_token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \
    -d "client_id=bk_your_client_id" \
    -d "client_secret=your_client_secret"
  ```

  <Warning>
    The refresh response contains a **new refresh token** that replaces the old one. The old refresh token is immediately revoked. Always persist the new refresh token to storage — if you reuse the old one, subsequent refreshes will fail.
  </Warning>

  The response has the same shape as the initial token exchange response. Store both the new `access_token` and the new `refresh_token`.

  ```javascript theme={null}
  async function refreshTokens(refreshToken) {
    const params = new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.BKMARK_CLIENT_ID,
      client_secret: process.env.BKMARK_CLIENT_SECRET,
    });

    const response = await fetch('https://api.bkmark.it/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString(),
    });

    if (!response.ok) {
      // Refresh token expired or revoked — user must re-authorize
      const err = await response.json();
      throw new Error(`Refresh failed: ${err.error}`);
    }

    const tokens = await response.json();
    // Persist tokens.access_token AND tokens.refresh_token
    return tokens;
  }
  ```
</Steps>

***

## Revoking Tokens

When a user disconnects your application, revoke their refresh token so Bkmark can clean up the session and the token can no longer be used:

```bash theme={null}
curl -X POST https://api.bkmark.it/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \
  -d "token_type_hint=refresh_token" \
  -d "client_id=bk_your_client_id" \
  -d "client_secret=your_client_secret"
```

<ParamField body="token" type="string" required>
  The access token or refresh token to revoke.
</ParamField>

<ParamField body="token_type_hint" type="string">
  `access_token` or `refresh_token`. Optional but helps Bkmark look up the token faster.
</ParamField>

<ParamField body="client_id" type="string" required>
  Your application's client ID.
</ParamField>

<ParamField body="client_secret" type="string" required>
  Your application's client secret.
</ParamField>

This endpoint always returns `200 OK` per [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009), even if the token was already expired or revoked.

***

## Token Introspection

To check whether a token is still active — for example, before making a batch of API calls — query the introspection endpoint:

```bash theme={null}
curl https://api.bkmark.it/oauth/token-info \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
```

**Active token response:**

```json theme={null}
{
  "active": true,
  "scope": "bookmarks:read bookmarks:write",
  "client_id": "bk_your_client_id",
  "user_id": "usr_01hq8p2z3kxv7m4n9rbt5c6d",
  "token_type": "Bearer",
  "expires_at": "2025-06-01T15:00:00.000Z",
  "created_at": "2025-06-01T14:00:00.000Z"
}
```

**Expired or revoked token response:**

```json theme={null}
{
  "active": false
}
```

<ResponseField name="active" type="boolean">
  `true` if the token is valid and has not expired or been revoked.
</ResponseField>

<ResponseField name="scope" type="string">
  Space-separated scopes granted to this token.
</ResponseField>

<ResponseField name="client_id" type="string">
  The client ID that was issued this token.
</ResponseField>

<ResponseField name="user_id" type="string">
  The Bkmark user ID this token acts on behalf of.
</ResponseField>

<ResponseField name="expires_at" type="string (ISO 8601)">
  UTC timestamp when the access token expires.
</ResponseField>

***

## Security Checklist

<Tip>
  Review this checklist before shipping your integration to production.
</Tip>

* **Never expose your `client_secret`** in client-side JavaScript, mobile app binaries, or source control.
* **Always validate `state`** in the callback before processing the authorization code.
* **Use PKCE** for SPAs and mobile apps, which cannot keep a `client_secret` confidential.
* **Store tokens securely** — use encrypted storage or a secrets manager, never cookies without `HttpOnly`/`Secure` flags or `localStorage`.
* **Implement automatic token refresh** so users aren't interrupted when access tokens expire.
* **Revoke tokens on disconnect** — always call `POST /oauth/revoke` when a user unlinks your app.
* **Request only the scopes you need** — see the [Scopes reference](/api/oauth/scopes) for guidance on minimal scope sets.
