Skip to main content
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 and supports PKCE (RFC 7636). 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

1
Generate PKCE Values
2
PKCE binds the authorization request to the token exchange, preventing code interception attacks. Generate a fresh pair for every authorization attempt.
3
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'
// }
4
Also generate a random state value to protect against CSRF:
5
const state = crypto.randomBytes(16).toString('hex');
// Store state in your session alongside the codeVerifier
6
Redirect to the Authorization Endpoint
7
Redirect the user’s browser to https://api.bkmark.it/oauth/authorize with the following query parameters:
8
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
9
Bkmark displays a consent screen that lists exactly which permissions your app is requesting. The user can approve or deny.
10
Authorization endpoint parameters:
11
Must be code.
12
Your application’s client ID, obtained from Settings → Developer → OAuth2 Clients.
13
The URI Bkmark redirects to after the user decides. Must exactly match one of the redirect URIs you registered.
14
Space-separated list of permission scopes. See the Scopes reference for all available values.
15
A random, unguessable string you generate per-request. Bkmark echoes it back in the callback. Verify it matches before continuing.
16
The SHA-256 hash of your code_verifier, encoded as base64url. Required for public clients; strongly recommended for all clients.
17
Must be S256 when providing a code_challenge.
18
Handle the Callback
19
On approval, Bkmark redirects to your redirect_uri with an authorization code:
20
GET https://yourapp.com/oauth/callback
  ?code=SplxlOBeZQQYbYS6WxSbIA
  &state=a8f3bc19d02e47c1
21
On denial, Bkmark redirects with an error:
22
GET https://yourapp.com/oauth/callback
  ?error=access_denied
  &state=a8f3bc19d02e47c1
23
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.
24
// 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');
});
25
Exchange the Code for Tokens
26
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:
27
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"
28
Token endpoint parameters:
29
Must be authorization_code.
30
The authorization code from the callback query string.
31
Must match the redirect_uri used in Step 2 exactly.
32
Your application’s client ID.
33
Your application’s client secret. Never expose this in client-side code.
34
The original random verifier you generated in Step 1. Required if you sent a code_challenge in the authorization request.
35
Successful response 200 OK:
36
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "scope": "bookmarks:read bookmarks:write tags:read"
}
37
Bearer token to include in Authorization headers. Expires in 1 hour (expires_in: 3600).
38
Long-lived token used to obtain new access tokens. Expires in 90 days.
39
Lifetime of the access token in seconds (currently 3600).
40
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.
41
// 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();
}
42
Make Authenticated API Calls
43
Include the access token as a Bearer token in the Authorization header on every API request:
44
curl https://api.bkmark.it/api/v1/bookmarks \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
45
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();
}
46
Refresh the Access Token
47
Access tokens expire after 1 hour. Use the refresh token to obtain a new pair without requiring the user to re-authorize.
48
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"
49
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.
50
The response has the same shape as the initial token exchange response. Store both the new access_token and the new refresh_token.
51
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;
}

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:
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"
token
string
required
The access token or refresh token to revoke.
token_type_hint
string
access_token or refresh_token. Optional but helps Bkmark look up the token faster.
client_id
string
required
Your application’s client ID.
client_secret
string
required
Your application’s client secret.
This endpoint always returns 200 OK per RFC 7009, 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:
curl https://api.bkmark.it/oauth/token-info \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Active token response:
{
  "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:
{
  "active": false
}
active
boolean
true if the token is valid and has not expired or been revoked.
scope
string
Space-separated scopes granted to this token.
client_id
string
The client ID that was issued this token.
user_id
string
The Bkmark user ID this token acts on behalf of.
expires_at
string (ISO 8601)
UTC timestamp when the access token expires.

Security Checklist

Review this checklist before shipping your integration to production.
  • 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 for guidance on minimal scope sets.