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