Skip to main content

Authentication

📋 Overview

Pipedrive integration uses OAuth 2.0 for authentication with automatic token refresh on every API request. Each account receives a unique API domain that must be used for all subsequent requests.

Key Features:

  • OAuth 2.0 Authorization Code Flow
  • JWT state parameter (10-minute expiry)
  • Pre-authentication check (skips OAuth if already connected)
  • Automatic token refresh on every request (proactive approach)
  • Dynamic API domain per account
  • Basic Auth for token refresh requests
  • Token invalidation detection
  • Sub-account support

🔄 OAuth Flow Diagram

1. Frontend (App)

├─→ GET /auth/login?forward_url=...
│ (with JWT Bearer token)

2. API Server

├─→ Check existing token
│ └─→ If exists: redirect to forward_url?status=success

├─→ Generate JWT state parameter
│ (account_id, owner, forward_url, 10min expiry)

├─→ Redirect to Pipedrive OAuth
│ https://oauth.pipedrive.com/oauth/authorize

3. Pipedrive OAuth

├─→ User grants permissions

├─→ Redirect to callback
│ /auth/callback?code=...&state=...

4. API Server (Callback)

├─→ Verify JWT state

├─→ Exchange code for tokens
│ POST https://oauth.pipedrive.com/oauth/token
│ (with Basic Auth)

├─→ Store token + api_domain in MongoDB
│ integrations.pipedrive.key

├─→ Redirect to forward_url
│ ?status=success&integration=pipedrive

5. Subsequent API Requests

├─→ Middleware: getToken()
│ │
│ ├─→ Retrieve token from MongoDB
│ │
│ ├─→ Always refresh token
│ │ POST https://oauth.pipedrive.com/oauth/token
│ │ (grant_type=refresh_token, Basic Auth)
│ │
│ ├─→ Update token in MongoDB
│ │
│ └─→ Attach access_token + api_domain to request

└─→ Make API call to {api_domain}/v1/...

🔐 Authentication Endpoints

1. Initiate OAuth Flow

Endpoint: GET /v1/e/pipedrive/auth/login

Purpose: Start OAuth 2.0 authorization flow or skip if already authenticated

Headers:

Authorization: Bearer {jwt_token}

Query Parameters:

ParameterTypeRequiredDescription
forward_urlstringYesURL to redirect after OAuth completion

Pre-authentication Check:

Before redirecting to Pipedrive, the system checks if a valid token already exists:

// Check for existing token
const snapshot = await PipeDriveCollection.getData(account_id, uid);

if (snapshot && snapshot._doc && snapshot._doc.token) {
// Already authenticated - skip OAuth
return res.redirect(`${forward_url}?status=success&integration=pipedrive`);
}

OAuth Redirect (if no token exists):

// Generate JWT state parameter
const state = jwt.sign(
{
account_id: req.auth.account_id,
owner: req.auth.uid,
forward_url: forward_url,
},
process.env.APP_SECRET,
{ expiresIn: 600 }, // 10 minutes
);

// Redirect to Pipedrive
const authUrl =
`https://oauth.pipedrive.com/oauth/authorize?` +
`client_id=${process.env.PIPE_DRIVE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(process.env.PIPE_DRIVE_REDIRECT_URL)}&` +
`state=${state}&` +
`response_type=code`;

res.redirect(authUrl);

Response: HTTP 302 redirect to Pipedrive OAuth consent screen

Example Request:

curl -X GET \
'https://api.dashclicks.com/v1/e/pipedrive/auth/login?forward_url=https://app.dashclicks.com/integrations' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

Success Scenarios:

  1. Already authenticated: Redirects to forward_url?status=success&integration=pipedrive
  2. Not authenticated: Redirects to Pipedrive OAuth consent screen

Error Scenarios:

  1. Missing forward_url: Returns 400 error
  2. Invalid JWT token: Returns 401 error
  3. Account validation fails: Returns 403 error

2. OAuth Callback

Endpoint: GET /v1/e/pipedrive/auth/callback

Purpose: Handle OAuth callback, exchange code for tokens, store in database

Query Parameters:

ParameterTypeRequiredDescription
codestringYesAuthorization code from Pipedrive
statestringYesJWT state parameter from login

Processing Steps:

Step 1: Verify State Parameter

// Decode and verify JWT state
const decoded = jwt.verify(req.query.state, process.env.APP_SECRET);

const { account_id, owner, forward_url } = decoded;

If verification fails: Redirect to forward_url?status=error&integration=pipedrive&reason=invalid_state

Step 2: Exchange Authorization Code

const tokenUrl = 'https://oauth.pipedrive.com/oauth/token';

// Create Basic Auth credentials
const base64Credentials = Buffer.from(
`${process.env.PIPE_DRIVE_CLIENT_ID}:${process.env.PIPE_DRIVE_CLIENT_SECRET}`,
).toString('base64');

const tokenData = qs.stringify({
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: process.env.PIPE_DRIVE_REDIRECT_URL,
});

const response = await axios.post(tokenUrl, tokenData, {
headers: {
Authorization: `Basic ${base64Credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});

Token Response Structure:

{
"access_token": "7507356:11465942:72cdfd552a1c4c2659fd8395aaf0da3e14934874",
"refresh_token": "7507356:11465942:cf3d769527455ee0beb3dd3fcf68276a45039570",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "base,deals:full,activities:full,contacts:full,products:full,users:read,recents:read,search:read",
"api_domain": "https://dashclicksllc.pipedrive.com"
}

Step 3: Store Token in MongoDB

await PipeDriveCollection.saveData({
account_id: account_id,
owner: owner,
token: {
generated_at: moment().unix(),
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
token_type: response.data.token_type,
scope: response.data.scope,
api_domain: response.data.api_domain, // Critical: Store dynamic domain
},
});

Step 4: Redirect to Frontend

// Success redirect
res.redirect(`${forward_url}?status=success&integration=pipedrive`);

// Error redirect (if token exchange fails)
res.redirect(`${forward_url}?status=error&integration=pipedrive&reason=${error.message}`);

Example Callback URL:

https://api.dashclicks.com/v1/e/pipedrive/auth/callback?
code=abcd1234efgh5678&
state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Success Response: Redirect to forward_url?status=success&integration=pipedrive

Error Responses:

ErrorRedirect URL
Invalid stateforward_url?status=error&integration=pipedrive&reason=invalid_state
Token exchange failedforward_url?status=error&integration=pipedrive&reason=token_exchange_failed
Database save failedforward_url?status=error&integration=pipedrive&reason=database_error

3. Disconnect Integration

Endpoint: DELETE /v1/e/pipedrive/auth

Purpose: Remove Pipedrive integration (delete stored tokens)

Headers:

Authorization: Bearer {jwt_token}

Implementation:

const account_id = req.auth.account_id;
const uid = req.auth.uid;

// Delete token document
await PipeDriveCollection.deleteData(account_id, uid);

res.status(200).json({
success: true,
message: 'Pipedrive integration disconnected successfully',
});

Example Request:

curl -X DELETE \
'https://api.dashclicks.com/v1/e/pipedrive/auth' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

Success Response:

{
"success": true,
"message": "Pipedrive integration disconnected successfully"
}

Error Response (token not found):

{
"success": false,
"message": "Token not found",
"error": "NOT_FOUND"
}

🔄 Automatic Token Refresh

Middleware: getToken

All export endpoints use the getToken middleware to automatically refresh the access token on every request.

Key Characteristics:

  • Proactive approach: Always refreshes, doesn't check expiration
  • Basic Auth: Uses base64-encoded client credentials
  • Updates token: Saves new token to MongoDB
  • Attaches to request: Adds access_token and api_domain to req object

Middleware Implementation:

const getToken = async (req, res, next) => {
try {
const account_id = req.auth.account_id;
const uid = req.auth.uid;

// Step 1: Retrieve stored token
const snapshot = await PipeDriveCollection.getData(account_id, uid);

if (!snapshot || !snapshot._doc) {
return res.status(401).json({
success: false,
message: 'User oauth token not found. Please redirect to login',
error: 'TOKEN_NOT_FOUND',
});
}

const doc = snapshot._doc;

// Check if token is invalidated
if (doc.token_invalidated) {
return res.status(401).json({
success: false,
message: 'Token has been invalidated. Please re-authenticate.',
error: 'TOKEN_INVALIDATED',
});
}

// Step 2: Always refresh token (proactive approach)
const tokenUrl = 'https://oauth.pipedrive.com/oauth/token';

// Prepare Basic Auth header
const base64Credentials = Buffer.from(
`${process.env.PIPE_DRIVE_CLIENT_ID}:${process.env.PIPE_DRIVE_CLIENT_SECRET}`,
).toString('base64');

const requestData = qs.stringify({
grant_type: 'refresh_token',
refresh_token: doc.token.refresh_token,
});

// Step 3: Make refresh request
const response = await axios.post(tokenUrl, requestData, {
headers: {
Authorization: `Basic ${base64Credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});

// Step 4: Update token in MongoDB
await PipeDriveCollection.updateData(doc._id, {
token: {
generated_at: moment().unix(),
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
token_type: response.data.token_type,
scope: response.data.scope,
api_domain: response.data.api_domain,
},
});

// Step 5: Attach to request for controllers
req.access_token = response.data.access_token;
req.api_domain = response.data.api_domain; // Critical for API calls

next();
} catch (error) {
// Handle token refresh errors
if (error.response?.status === 401 || error.response?.status === 400) {
return res.status(401).json({
success: false,
message: 'Token refresh failed. Please re-authenticate.',
error: 'TOKEN_REFRESH_FAILED',
});
}

next(error);
}
};

Why Refresh on Every Request?

  1. Always fresh tokens: Eliminates timing issues with expiration
  2. Correct API domain: Ensures api_domain is always current
  3. Simplicity: No expiration calculation logic needed
  4. Reliability: Pipedrive handles rate limiting for token refresh

Token Refresh Request:

POST https://oauth.pipedrive.com/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=7507356:11465942:cf3d769...

Token Refresh Response:

{
"access_token": "7507356:11465942:NEW_ACCESS_TOKEN",
"refresh_token": "7507356:11465942:NEW_REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "base,deals:full,...",
"api_domain": "https://dashclicksllc.pipedrive.com"
}

🌐 API Domain Handling

Dynamic API Domain

Each Pipedrive account has a unique API domain returned in the OAuth response:

Sandbox Accounts:

https://{company}-sandbox.pipedrive.com

Production Accounts:

https://{company}.pipedrive.com

Storage:

The api_domain is stored in the token object:

{
account_id: "507f191e810c19729de860ea",
owner: "user_Lwh9EzeD8",
token: {
access_token: "...",
refresh_token: "...",
api_domain: "https://dashclicksllc.pipedrive.com" // Stored here
}
}

Usage in API Calls:

// Controller receives api_domain from middleware
const contactInformation = async (api_domain, access_token, type, start = 0) => {
const url = `${api_domain}/v1/${type}`;

const response = await axios.get(url, {
headers: { Authorization: `Bearer ${access_token}` },
params: { limit: 100, start },
});

return response.data;
};

Critical: Always use the api_domain from the middleware - never hard-code API endpoints.


🔒 OAuth Scopes

Pipedrive integration requests comprehensive access:

ScopeDescription
baseBasic account information
deals:fullFull read/write access to deals
activities:fullFull read/write access to activities
contacts:fullFull read/write access to persons and organizations
products:fullFull read/write access to products
users:readRead user information
recents:readRead recent items
search:readSearch functionality

Scope String (URL-encoded in OAuth request):

base,deals:full,activities:full,contacts:full,products:full,users:read,recents:read,search:read

⚠️ Token Invalidation Detection

When Tokens Become Invalid

Tokens can be invalidated when:

  1. User revokes access in Pipedrive account settings
  2. User changes password (invalidates all OAuth tokens)
  3. Admin removes app from account
  4. Account suspended or deactivated

Detection Mechanism

The error handler in index.js detects token invalidation:

// Error handling middleware
if (error.response && error.response.data) {
const errorData = error.response.data;

// Detect Pipedrive 401 errors
if (errorData.errorCode === '401') {
const accountId = error.config?.accountId;

if (accountId) {
// Mark all tokens for this account as invalidated
await pipedriveKeys.updateMany(
{ account_id: accountId.toString() },
{ $set: { token_invalidated: true } },
);
}

error.message = 'TOKEN_INVALIDATED';
}
}

Pipedrive Error Response (401):

{
"success": false,
"error": "Unauthorized",
"errorCode": "401"
}

DashClicks API Response:

{
"success": false,
"message": "TOKEN_INVALIDATED",
"error": "TOKEN_INVALIDATED"
}

Frontend Handling:

When receiving TOKEN_INVALIDATED error, redirect user to re-authenticate:

if (error.response?.data?.message === 'TOKEN_INVALIDATED') {
// Redirect to OAuth login
window.location.href =
'/v1/e/pipedrive/auth/login?forward_url=' + encodeURIComponent(window.location.href);
}

🎯 Use Cases

1. First-time OAuth Connection

// User clicks "Connect Pipedrive" button in frontend
async function connectPipedrive(jwtToken) {
const forwardUrl = 'https://app.dashclicks.com/integrations';

// Initiate OAuth flow
window.location.href = `/v1/e/pipedrive/auth/login?forward_url=${encodeURIComponent(forwardUrl)}`;

// After OAuth completes, user is redirected back to:
// https://app.dashclicks.com/integrations?status=success&integration=pipedrive
}

2. Check OAuth Connection Status

// Check if Pipedrive is already connected
async function checkPipedriveConnection(jwtToken) {
try {
// Try to fetch data (will fail if not connected)
const response = await axios.get('/v1/e/pipedrive/export/deals?limit=1', {
headers: { Authorization: `Bearer ${jwtToken}` },
});

return { connected: true };
} catch (error) {
if (error.response?.data?.error === 'TOKEN_NOT_FOUND') {
return { connected: false };
}
throw error;
}
}

3. Handle Token Invalidation

// Wrapper function with automatic re-authentication
async function fetchPipedriveData(jwtToken, type) {
try {
const response = await axios.get(`/v1/e/pipedrive/export/${type}`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});

return response.data;
} catch (error) {
if (error.response?.data?.message === 'TOKEN_INVALIDATED') {
// Redirect to re-authenticate
const forwardUrl = encodeURIComponent(window.location.href);
window.location.href = `/v1/e/pipedrive/auth/login?forward_url=${forwardUrl}`;
return;
}

throw error;
}
}

4. Disconnect Integration

// Remove Pipedrive connection
async function disconnectPipedrive(jwtToken) {
try {
await axios.delete('/v1/e/pipedrive/auth', {
headers: { Authorization: `Bearer ${jwtToken}` },
});

console.log('Pipedrive disconnected successfully');
} catch (error) {
console.error('Failed to disconnect:', error);
}
}

🛡️ Security Best Practices

1. State Parameter Security

  • JWT-signed state: Prevents CSRF attacks
  • 10-minute expiry: Limits window for replay attacks
  • Includes account context: Validates ownership
// State includes critical context
const state = jwt.sign(
{
account_id: req.auth.account_id,
owner: req.auth.uid,
forward_url: forward_url,
},
process.env.APP_SECRET,
{ expiresIn: 600 },
);

2. Token Storage

  • MongoDB encryption at rest: Protect tokens in database
  • Never expose refresh_token: Only return access_token to frontend (if needed)
  • Separate by account: Each user has isolated token document

3. Basic Auth for Token Refresh

  • Client credentials: Never expose client_secret in frontend
  • Base64 encoding: Standard OAuth 2.0 practice
  • Server-side only: Token refresh happens in backend

4. API Domain Validation

  • Use stored domain: Always use api_domain from token
  • No hard-coding: Prevents attacks using incorrect domains
  • Updated on refresh: Domain may change (company renamed, etc.)

5. Error Handling

  • Generic error messages: Don't expose internal details
  • Log security events: Track OAuth failures, token invalidations
  • Rate limiting: Protect OAuth endpoints from abuse

📊 Error Scenarios

1. Missing JWT Token

Request:

GET /v1/e/pipedrive/auth/login?forward_url=...
# Missing Authorization header

Response: 401 Unauthorized

{
"success": false,
"message": "Authentication required",
"error": "UNAUTHORIZED"
}

2. Invalid State Parameter

Scenario: State JWT is expired or tampered

Response: Redirect to forward_url?status=error&integration=pipedrive&reason=invalid_state

3. Token Exchange Failure

Scenario: Pipedrive rejects authorization code

Response: Redirect to forward_url?status=error&integration=pipedrive&reason=token_exchange_failed

4. Token Not Found

Request: Export data without OAuth connection

Response: 401 Unauthorized

{
"success": false,
"message": "User oauth token not found. Please redirect to login",
"error": "TOKEN_NOT_FOUND"
}

5. Token Refresh Failed

Scenario: Refresh token is invalid or expired

Response: 401 Unauthorized

{
"success": false,
"message": "Token refresh failed. Please re-authenticate.",
"error": "TOKEN_REFRESH_FAILED"
}

6. Token Invalidated

Scenario: User revoked access in Pipedrive

Response: 401 Unauthorized

{
"success": false,
"message": "TOKEN_INVALIDATED",
"error": "TOKEN_INVALIDATED"
}

MongoDB Update: token_invalidated flag set to true for all account tokens


💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:30 AM