Skip to content

Conversation

@Karrrthik7
Copy link

/claim #16797

What does this PR do?

  • Removes all Revert.dev usage for the Pipedrive integration.
  • Implements native Pipedrive OAuth2 (authorization + token exchange + refresh).
  • Adds direct Pipedrive API calls (persons/activities) using Bearer tokens.
  • Adds frontend “Connect Pipedrive” button to start OAuth flow.
  • Persists tokens securely in Cal.com credential storage (Prisma credential row).

Fixes:

Files changed (key)

  • packages/app-store/pipedrive-crm/api/auth.ts (new) — GET /api/integrations/pipedrive/auth → redirects to Pipedrive authorize URL
  • packages/app-store/pipedrive-crm/api/callback.ts (new) — GET /api/integrations/pipedrive/callback → exchanges code for tokens, persists credential
  • packages/app-store/pipedrive-crm/lib/CrmService.ts (modified) — native token refresh + Pipedrive API calls (persons/activities)
  • packages/app-store/pipedrive-crm/config.json (modified) — metadata update
  • packages/app-store/pipedrive-crm/components/EventTypeAppCardInterface.tsx (modified) — “Connect Pipedrive” button UI
  • packages/app-store/_utils/oauth/refreshOAuthTokens.ts (updated) — adapter for Pipedrive refresh
  • packages/app-store/_utils/oauth/createOAuthAppCredential.ts (used) — persists credential (no secret leaks)

Note: No Revert.dev references remain in runtime paths for the Pipedrive integration.

Short description of OAuth flow (in-code comments included)

  1. Frontend triggers GET /api/integrations/pipedrive/auth (via "Connect Pipedrive" button).
  2. Server builds redirect to:
    https://oauth.pipedrive.com/oauth/authorize?client_id=<app_key>&redirect_uri=<WEBAPP_URL>/api/integrations/pipedrive/callback&response_type=code&state=
  3. User authorizes; Pipedrive redirects to:
    GET /api/integrations/pipedrive/callback?code=...&state=...
  4. Callback exchanges code for access_token + refresh_token at https://oauth.pipedrive.com/oauth/token (POST, form-encoded) using client_id & client_secret stored in Cal.com app keys.
  5. Tokens + expiryDate persisted to Prisma credential record via createOAuthAppCredential.
  6. CrmService reads credential.key token object, checks expiryDate before API calls; if expired, refreshes using refresh_token at /oauth/token and persists updated tokens.
  7. Pipedrive API calls use Authorization: Bearer <access_token>. On refresh, CrmService retries the call.

Error handling

  • 400 returned when client_id / client_secret missing in app keys.
  • Token exchange and refresh failures return clear errors and are logged.
  • Revoked tokens require reconnect; frontend surfaces errors and user can reconnect.
  • credential.key is never exposed in API responses.

How to test locally

Environment setup

  • Configure Pipedrive app keys (client_id, client_secret) in Cal.com app-store for the pipedrive-crm slug (do not commit secrets).
  • Set:
    WEBAPP_URL_FOR_OAUTH=http://localhost:3000

Register Pipedrive OAuth app with redirect URI:

  • <WEBAPP_URL_FOR_OAUTH>/api/integrations/pipedrive/callback

Run

  • yarn dev (or yarn dx)

Test steps

  1. Open app-store UI or an event-type app card and click “Connect Pipedrive.”
  2. Complete OAuth consent in Pipedrive.
  3. Verify credential record created in DB containing access_token, refresh_token, expiryDate.
  4. Trigger a flow that creates a contact or activity — confirm Pipedrive API requests succeed.
  5. To test refresh: set expiryDate in DB to a past time, trigger an API call — verify tokens refresh automatically and DB updates.
  6. To test disconnect/reconnect: delete credential record and repeat OAuth flow.

Expected behavior

  • OAuth completes successfully.
  • Tokens are stored and refreshed automatically.
  • Pipedrive API calls succeed.
  • No Revert.dev or "pipedrive client id missing" errors occur.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code.
  • I have updated developer docs in /docs if needed (N/A).
  • I confirm automated tests are in place (manual test steps provided; add unit/integration tests on request).

Checklist

  • Title follows conventional commits: feat(pipedrive-crm): ...
  • Type check locally: run yarn type-check:ci --force on changed files
  • Biome formatting applied for changed files
  • No secrets or API keys committed
  • credential.key is not exposed in any API responses
  • Draft PR recommended

Notes / Follow-ups

  • I reused existing app-store OAuth helpers (createOAuthAppCredential, refreshOAuthTokens) to preserve credential persistence and sync endpoint behavior.
  • I can add unit tests for token exchange/refresh flows and run CI checks on request.
  • Please validate translations for any new UI strings; I can add entries to apps/web/public/static/locales/en/common.json.

@vercel
Copy link

vercel bot commented Jan 5, 2026

@Karrrthik7 is attempting to deploy a commit to the cal-staging Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added $50 community Created by Linear-GitHub Sync crm-apps area: crm apps, salesforce, hubspot, close.com, sendgrid Medium priority Created by Linear-GitHub Sync Stale ✨ feature New feature or request 💎 Bounty A bounty on Algora.io labels Jan 5, 2026
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 5 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/app-store/pipedrive-crm/api/add.ts">

<violation number="1" location="packages/app-store/pipedrive-crm/api/add.ts:37">
P1: Missing `response_type: &quot;code&quot;` parameter in OAuth authorization URL. This is a required parameter for the OAuth2 authorization code flow and without it, Pipedrive&#39;s OAuth authorization will likely fail. See `zoho-bigin/api/add.ts` for the correct pattern.</violation>
</file>

<file name="packages/app-store/pipedrive-crm/components/EventTypeAppCardInterface.tsx">

<violation number="1" location="packages/app-store/pipedrive-crm/components/EventTypeAppCardInterface.tsx:22">
P2: Missing `res.ok` check before parsing JSON. If the API returns an error status (4xx/5xx), the response may not contain `url` and the error won&#39;t be surfaced to the user.</violation>
</file>

<file name="packages/app-store/pipedrive-crm/lib/CrmService.ts">

<violation number="1" location="packages/app-store/pipedrive-crm/lib/CrmService.ts:78">
P1: Token refresh failure is silently swallowed. If `refreshAccessToken` fails, `getToken()` returns without a valid token, causing subsequent API calls to fail with expired credentials. Consider re-throwing the error after logging, or validating the token is valid after refresh attempt.</violation>

<violation number="2" location="packages/app-store/pipedrive-crm/lib/CrmService.ts:189">
P2: Unlike other API methods (`createPipedriveEvent`, `updateMeeting`), `deleteMeeting` doesn&#39;t check `res.ok` or throw on failure. Delete failures will be silently ignored.</violation>
</file>

<file name="packages/app-store/pipedrive-crm/api/callback.ts">

<violation number="1" location="packages/app-store/pipedrive-crm/api/callback.ts:24">
P1: Missing validation for absent `code` parameter. The current check `code &amp;&amp; typeof code !== &quot;string&quot;` passes when `code` is `undefined`, allowing the flow to continue and cast `undefined as string` on line 48. This will cause a confusing 500 error from Pipedrive instead of a clear 400 from your handler. Per early-return preference, validate that code exists.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +37 to 40
const params = new URLSearchParams({
client_id: String(appKeys.client_id),
redirect_uri: redirectUri,
});
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing response_type: "code" parameter in OAuth authorization URL. This is a required parameter for the OAuth2 authorization code flow and without it, Pipedrive's OAuth authorization will likely fail. See zoho-bigin/api/add.ts for the correct pattern.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/pipedrive-crm/api/add.ts, line 37:

<comment>Missing `response_type: &quot;code&quot;` parameter in OAuth authorization URL. This is a required parameter for the OAuth2 authorization code flow and without it, Pipedrive&#39;s OAuth authorization will likely fail. See `zoho-bigin/api/add.ts` for the correct pattern.</comment>

<file context>
@@ -30,8 +32,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
-    newTab: true,
+  const redirectUri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/pipedrive/callback`;
+  const state = encodeOAuthState(req);
+  const params = new URLSearchParams({
+    client_id: String(appKeys.client_id),
+    redirect_uri: redirectUri,
</file context>
Suggested change
const params = new URLSearchParams({
client_id: String(appKeys.client_id),
redirect_uri: redirectUri,
});
const params = new URLSearchParams({
client_id: String(appKeys.client_id),
redirect_uri: redirectUri,
response_type: "code",
});
Fix with Cubic

setLoading(true);
const teamId = eventType.team?.id;
const q = teamId ? `?teamId=${teamId}` : "";
const res = await fetch(`/api/integrations/pipedrive/add${q}`);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing res.ok check before parsing JSON. If the API returns an error status (4xx/5xx), the response may not contain url and the error won't be surfaced to the user.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/pipedrive-crm/components/EventTypeAppCardInterface.tsx, line 22:

<comment>Missing `res.ok` check before parsing JSON. If the API returns an error status (4xx/5xx), the response may not contain `url` and the error won&#39;t be surfaced to the user.</comment>

<file context>
@@ -4,24 +4,55 @@ import AppCard from &quot;@calcom/app-store/_components/AppCard&quot;;
+      setLoading(true);
+      const teamId = eventType.team?.id;
+      const q = teamId ? `?teamId=${teamId}` : &quot;&quot;;
+      const res = await fetch(`/api/integrations/pipedrive/add${q}`);
+      const json = await res.json();
+      if (json?.url) {
</file context>
Suggested change
const res = await fetch(`/api/integrations/pipedrive/add${q}`);
const res = await fetch(`/api/integrations/pipedrive/add${q}`);
if (!res.ok) {
throw new Error(`Failed to initiate Pipedrive connection: ${res.status}`);
}
Fix with Cubic

Comment on lines +189 to +192
await fetch(`https://api.pipedrive.com/v1/activities/${uid}`, {
method: "DELETE",
headers: headers,
};

return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
headers: { Authorization: `Bearer ${currentToken.access_token}` },
});
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Unlike other API methods (createPipedriveEvent, updateMeeting), deleteMeeting doesn't check res.ok or throw on failure. Delete failures will be silently ignored.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/pipedrive-crm/lib/CrmService.ts, line 189:

<comment>Unlike other API methods (`createPipedriveEvent`, `updateMeeting`), `deleteMeeting` doesn&#39;t check `res.ok` or throw on failure. Delete failures will be silently ignored.</comment>

<file context>
@@ -10,180 +10,195 @@ import type { CredentialPayload } from &quot;@calcom/types/Credential&quot;;
-    const requestOptions = {
+    await (await this.auth).getToken();
+    const currentToken = this.credential.key as unknown as PipedriveToken;
+    await fetch(`https://api.pipedrive.com/v1/activities/${uid}`, {
       method: &quot;DELETE&quot;,
-      headers: headers,
</file context>
Suggested change
await fetch(`https://api.pipedrive.com/v1/activities/${uid}`, {
method: "DELETE",
headers: headers,
};
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
headers: { Authorization: `Bearer ${currentToken.access_token}` },
});
const res = await fetch(`https://api.pipedrive.com/v1/activities/${uid}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${currentToken.access_token}` },
});
if (!res.ok) throw new Error("Failed to delete activity in Pipedrive");
Fix with Cubic

Comment on lines +78 to +79
} catch (e: unknown) {
this.log.error(e);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Token refresh failure is silently swallowed. If refreshAccessToken fails, getToken() returns without a valid token, causing subsequent API calls to fail with expired credentials. Consider re-throwing the error after logging, or validating the token is valid after refresh attempt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/pipedrive-crm/lib/CrmService.ts, line 78:

<comment>Token refresh failure is silently swallowed. If `refreshAccessToken` fails, `getToken()` returns without a valid token, causing subsequent API calls to fail with expired credentials. Consider re-throwing the error after logging, or validating the token is valid after refresh attempt.</comment>

<file context>
@@ -10,180 +10,195 @@ import type { CredentialPayload } from &quot;@calcom/types/Credential&quot;;
+        // persist updated token in this instance for subsequent API calls
+        this.credential.key = pipedriveRefresh as any;
+        currentToken = { ...currentToken, ...pipedriveRefresh };
+      } catch (e: unknown) {
+        this.log.error(e);
       }
</file context>
Suggested change
} catch (e: unknown) {
this.log.error(e);
} catch (e: unknown) {
this.log.error(e);
throw e;
Fix with Cubic

const { code } = req.query;
const state = decodeOAuthState(req);

if (code && typeof code !== "string") {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing validation for absent code parameter. The current check code && typeof code !== "string" passes when code is undefined, allowing the flow to continue and cast undefined as string on line 48. This will cause a confusing 500 error from Pipedrive instead of a clear 400 from your handler. Per early-return preference, validate that code exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/pipedrive-crm/api/callback.ts, line 24:

<comment>Missing validation for absent `code` parameter. The current check `code &amp;&amp; typeof code !== &quot;string&quot;` passes when `code` is `undefined`, allowing the flow to continue and cast `undefined as string` on line 48. This will cause a confusing 500 error from Pipedrive instead of a clear 400 from your handler. Per early-return preference, validate that code exists.</comment>

<file context>
@@ -1,19 +1,73 @@
+  const { code } = req.query;
+  const state = decodeOAuthState(req);
+
+  if (code &amp;&amp; typeof code !== &quot;string&quot;) {
+    res.status(400).json({ message: &quot;`code` must be a string&quot; });
+    return;
</file context>
Suggested change
if (code && typeof code !== "string") {
if (!code || typeof code !== "string") {
Fix with Cubic

@github-actions github-actions bot removed the Stale label Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙋 Bounty claim 💎 Bounty A bounty on Algora.io community Created by Linear-GitHub Sync crm-apps area: crm apps, salesforce, hubspot, close.com, sendgrid ✨ feature New feature or request Medium priority Created by Linear-GitHub Sync size/L $50

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native Pipedrive integration

2 participants