The real problem
Why this is harder than it looks
Pipedrive's API is REST, well-documented, and easy to prototype against. Most developers estimate a day or two to wire up a working integration. The problems arrive when you try to run it for multiple real users over time.
The first surprise is Pipedrive's token rotation behavior. When you refresh an access token, Pipedrive issues a new refresh token alongside the new access token — and the old refresh token is invalidated immediately. If your backend doesn't capture and store the new refresh token on every refresh cycle, the next refresh will fail with an invalid_grant error. This is silent in development (tokens last long enough that you may never hit it) but catastrophic in production, where background agents refresh tokens constantly across many users. Developers routinely discover this only after users start getting logged out inexplicably.
Then there's the company-domain issue. Every Pipedrive API call must go to {COMPANYDOMAIN}.pipedrive.com — the base URL is specific to each user's Pipedrive company account. You need to capture the api_domain from the initial token exchange response and store it per-user. If you miss this and hardcode a base URL, every call silently routes to the wrong endpoint for any user whose domain differs.
Pipedrive also requires you to register your own OAuth app in Developer Hub — there is no managed app. Scopes are declared at app registration and requested at authorization time; a mismatch between the two blocks installation with an error your users cannot resolve themselves. And refresh tokens expire after 60 days of inactivity, which means users on low-frequency workflows need re-authorization — and password changes immediately invalidate all their refresh tokens.
Scalekit handles token rotation, per-user domain storage, scope configuration, and revocation detection. Your agent names a tool and passes parameters. Everything else is invisible.
Capabilities
What your agent can do with Pipedrive
Once connected, your agent has 68 pre-built tools covering the full Pipedrive CRM model:
- Manage deals end-to-end: create, update, move between stages, search by keyword or filter, and list with pagination — including product and participant sub-resources
- Work with persons and organizations: create and update contacts, search by name or email, list deals per person or org for complete account context
- Convert and track leads: create unqualified leads, update and archive them, search across titles and notes, and convert to deals once qualified
- Schedule and query activities: create calls, meetings, emails, and tasks linked to any CRM object; filter by date range, type, or completion status
- Attach notes and files: create pinned notes on deals, persons, orgs, or leads; upload files and link them to any object
- Manage pipelines, stages, products, and goals: introspect and update the full sales workflow structure; create and track quota goals per user or team
Setup context
What we're building
This guide connects a sales assistant agent to Pipedrive — helping reps manage their pipeline, log activities, and update CRM records without leaving your product.
🤖
Example agent
Sales assistant creating deals, scheduling follow-up activities, and updating pipeline stages on behalf of each rep
🔐
Auth model
B2B SaaS — each rep connects their own Pipedrive account. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Pipedrive secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const actions = scalekit.actions;
Already have credentials?
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each rep gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="pipedrive",
identifier="user_pd_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
const response = await actions.getOrCreateConnectedAccount({
connectionName: "pipedrive",
identifier: "user_pd_456" // your internal user ID
});
const connectedAccount = response.connectedAccount;
console.log(`Status: ${connectedAccount.status}`);
// Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The rep authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, redirect handling, and state pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="pipedrive",
identifier="user_pd_456"
)
# Redirect user → Pipedrive consent screen
# Scalekit captures the token on callback
return redirect(link.link)
if (connectedAccount.status !== "ACTIVE") {
const { link } = await actions.getAuthorizationLink({
connectionName: "pipedrive",
identifier: "user_pd_456"
});
// Redirect user → Pipedrive consent screen
// Scalekit captures the token on callback
return redirect(link);
}
Token management is automatic
After the user approves, Scalekit stores encrypted tokens — including the per-user company domain — and the connected account moves to ACTIVE. Access tokens are refreshed proactively before expiry, and the rotated refresh token is captured on every cycle. If a rep revokes access from Pipedrive or changes their password, the account moves to REVOKED — no silent invalid_grant failures. Check account.status before critical operations.
BYOC is required for Pipedrive
Pipedrive requires you to register your own app in Developer Hub and supply your own Client ID and Client Secret. Create your app at developers.pipedrive.com, paste the Scalekit redirect URI as the callback URL, configure your scopes, then enter credentials in the Scalekit dashboard. Token management stays fully handled.
Calling Pipedrive
4 Calling Pipedrive: What your agent writes
With the connected account active, your agent calls Pipedrive actions using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval, request construction, and response parsing.
Create a deal and schedule a follow-up call
The most common first action for a sales agent — create a deal linked to a person and org, then schedule the discovery call in one workflow.
deal = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_deal_create",
tool_input={
"title": "Acme Corp — Enterprise Plan",
"value": 48000,
"currency": "USD",
"person_id": 1042,
"org_id": 305,
"stage_id": 2,
"expected_close_date": "2026-06-30"
}
)
deal_id = deal["data"]["id"]
activity = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_activity_create",
tool_input={
"subject": "Discovery call — Acme Corp",
"type": "call",
"deal_id": deal_id,
"due_date": "2026-04-07",
"due_time": "10:00",
"duration": "00:30"
}
)
# Returns: { "id": 88, "subject": "Discovery call — Acme Corp", ... }
const deal = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_deal_create",
toolInput: {
title: "Acme Corp — Enterprise Plan",
value: 48000,
currency: "USD",
person_id: 1042,
org_id: 305,
stage_id: 2,
expected_close_date: "2026-06-30"
}
});
const dealId = deal.data.id;
const activity = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_activity_create",
toolInput: {
subject: "Discovery call — Acme Corp",
type: "call",
deal_id: dealId,
due_date: "2026-04-07",
due_time: "10:00",
duration: "00:30"
}
});
// Returns: { id: 88, subject: "Discovery call — Acme Corp", ... }
Search for a person and attach a note
Look up a contact by name, then pin a contextual note to their record. Useful for post-call logging workflows.
results = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_persons_search",
tool_input={"term": "Jane Smith", "exact_match": False, "limit": 5}
)
persons = results["data"]["items"]
if persons:
person_id = persons[0]["item"]["id"]
actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_note_create",
tool_input={
"content": "Spoke with Jane re: renewal. Budget confirmed for Q2.",
"person_id": person_id,
"pinned_to_person_flag": True
}
)
const results = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_persons_search",
toolInput: { term: "Jane Smith", exact_match: false, limit: 5 }
});
const persons = results.data.items;
if (persons.length > 0) {
const personId = persons[0].item.id;
await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_note_create",
toolInput: {
content: "Spoke with Jane re: renewal. Budget confirmed for Q2.",
person_id: personId,
pinned_to_person_flag: true
}
});
}
Move a deal to the next pipeline stage
Fetch the deal's current stage, resolve the pipeline stages in order, then advance the deal. Always fetch stages at runtime — stage IDs differ per workspace.
deal = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_deal_get",
tool_input={"deal_id": 9871}
)
current_stage = deal["data"]["stage_id"]
stages = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_stages_list",
tool_input={"pipeline_id": deal["data"]["pipeline_id"]}
)
stage_ids = sorted(s["id"] for s in stages["data"])
next_stage = stage_ids[stage_ids.index(current_stage) + 1]
actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_deal_update",
tool_input={"deal_id": 9871, "stage_id": next_stage}
)
const deal = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_deal_get",
toolInput: { deal_id: 9871 }
});
const currentStage = deal.data.stage_id;
const stages = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_stages_list",
toolInput: { pipeline_id: deal.data.pipeline_id }
});
const stageIds = stages.data.map(s => s.id).sort((a, b) => a - b);
const nextStage = stageIds[stageIds.indexOf(currentStage) + 1];
await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_deal_update",
toolInput: { deal_id: 9871, stage_id: nextStage }
});
List deals with a reusable filter
Create a saved filter for high-value open deals, then pass its ID to pipedrive_deals_list for fast, repeatable queries without rebuilding filter logic on every call.
import json
filter_result = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_filter_create",
tool_input={
"name": "High-value open deals",
"type": "deals",
"conditions": json.dumps({
"glue": "and",
"conditions": [{
"glue": "and",
"conditions": [
{"object": "deal", "field_id": "status", "operator": "=", "value": "open", "extra_value": None},
{"object": "deal", "field_id": "value", "operator": ">", "value": "50000", "extra_value": None}
]
}]
})
}
)
filter_id = filter_result["data"]["id"]
deals = actions.execute_tool(
identifier="user_pd_456",
tool_name="pipedrive_deals_list",
tool_input={"filter_id": filter_id, "limit": 50}
)
# Returns: list of deals matching the saved filter
const filterResult = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_filter_create",
toolInput: {
name: "High-value open deals",
type: "deals",
conditions: JSON.stringify({
glue: "and",
conditions: [{
glue: "and",
conditions: [
{ object: "deal", field_id: "status", operator: "=", value: "open", extra_value: null },
{ object: "deal", field_id: "value", operator: ">", value: "50000", extra_value: null }
]
}]
})
}
});
const filterId = filterResult.data.id;
const deals = await actions.executeTool({
identifier: "user_pd_456",
toolName: "pipedrive_deals_list",
toolInput: { filter_id: filterId, limit: 50 }
});
// Returns: list of deals matching the saved filter
Framework wiring
5 Wiring into your agent framework
Scalekit's tools load directly into LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
pd_tools = get_tools(
connection_name="pipedrive",
identifier="user_pd_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a sales assistant. Use the available tools to help manage Pipedrive deals, contacts, and activities."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), pd_tools, prompt)
result = AgentExecutor(agent=agent, tools=pd_tools).invoke({
"input": "Find all open deals over $50k and schedule a follow-up call for each one due this week"
})
import { ChatAnthropic } from "@langchain/anthropic";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { getTools } from "@scalekit-sdk/langchain";
const pdTools = getTools({
connectionName: "pipedrive",
identifier: "user_pd_456"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a sales assistant. Use the available tools to help manage Pipedrive deals, contacts, and activities."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: pdTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: pdTools
}).invoke({
input: "Find all open deals over $50k and schedule a follow-up call for each one due this week"
});
Other frameworks supported
Tool reference
All 68 Pipedrive tools
Grouped by object and capability. Your agent calls tools by name — no API wrappers to write.
Create a deal with title (required), value, currency, stage, linked person and org, close date, and probability
Retrieve full deal details by ID, including linked person, org, stage, and custom fields
Update any deal field — stage, value, close date, status, owner. Only supplied fields are changed
Permanently delete a deal by ID
List deals with filtering by status, stage, pipeline, owner, or saved filter ID. Supports pagination and sort
Search deals by keyword across title, notes, and custom fields with optional person or org scope
pipedrive_deal_activities_list
List all activities linked to a deal, filtered by completion status
pipedrive_deal_products_list
List all products attached to a deal with unit price, quantity, and discount per line
pipedrive_deal_participants_list
List all person participants linked to a deal beyond the primary contact
Create a contact with name (required), email, phone, org association, and marketing consent status
Retrieve a person by ID including linked org, deals, and custom fields
Update any person field — name, email, phone, org, marketing status
Permanently delete a person by ID
List persons with optional filter ID, first-character filter, pagination, and sort
Search persons by keyword across name, email, phone, and custom fields
pipedrive_organization_create
Create an org with name (required), owner, address, and visibility
pipedrive_organization_get
Retrieve an org by ID including linked persons and deals
pipedrive_organization_update
Update any org field — name, owner, address, visibility
pipedrive_organization_delete
Permanently delete an org by ID
pipedrive_organizations_list
List orgs with filter ID, first-character filter, pagination, and sort
pipedrive_organizations_search
Search orgs by keyword across name, address, and notes
pipedrive_organization_deals_list
List all deals linked to an org, filtered by status — useful for account-level pipeline views
pipedrive_person_deals_list
List all deals linked to a specific person, filtered by status
Create a lead with title (required), linked person or org, value, and expected close date
Update any lead field or archive/unarchive it
Permanently delete a lead by UUID
List leads with optional archived-status filter and pagination
Search leads by keyword across title, notes, and linked person or org
pipedrive_activity_create
Create a call, meeting, email, task, or custom activity linked to any CRM object
Retrieve an activity by ID
pipedrive_activity_update
Update any activity field including marking it done
pipedrive_activity_delete
Permanently delete an activity by ID
pipedrive_activities_list
List activities with filtering by type, user, date range, or completion status
pipedrive_activity_types_list
List all activity types configured in the account — use to validate type values before creating activities
Create a note (HTML allowed) and optionally pin it to a deal, person, org, or lead
Retrieve a note and its metadata by ID
Update note content or pin status
Permanently delete a note by ID
List notes filtered by linked object (deal, person, org, or lead), date range, or pin status
Upload a file (base64 or public URL) and attach it to any CRM object
Retrieve file metadata and download URL by ID
Permanently delete a file by ID
List all account files with pagination, sort, and optional inclusion of soft-deleted files
pipedrive_pipeline_create
Create a new sales pipeline with name and optional probability tracking
Retrieve pipeline details and its stages by ID
pipedrive_pipeline_update
Update pipeline name, probability tracking, display order, or active state
pipedrive_pipeline_delete
Permanently delete a pipeline and its stages (deals are not deleted)
List all pipelines in the account — no parameters required
Create a stage within a pipeline with default probability and optional rotting rules
Update stage name, probability, rotting rules, display order, or active state
List all stages, optionally filtered to a specific pipeline — use to resolve stage IDs before creating deals
Create a product catalog entry with name, SKU, price, tax, and category
Retrieve product details by ID
Update any product field — name, code, price, tax, category
Permanently delete a product by ID
List all catalog products with optional filter ID, pagination, and sort
Create a quota goal for a user or company with type, target, tracking metric, and reporting interval
Retrieve goal details and current progress by UUID
Update goal target, assignee, duration, or reporting interval
List goals filtered by type name, assignee, or active status
Retrieve a user by ID including role, email, and active status
List all users in the account — no parameters required
Register a webhook for real-time notifications on any object type and action (added, updated, deleted, or all)
Delete a webhook subscription by ID — stops notifications immediately
List all active webhook subscriptions for the account
Create a reusable saved filter for deals, leads, persons, orgs, products, or activities
Retrieve a saved filter and its conditions by ID
List all saved filters, optionally scoped to a specific object type
Connector notes
Pipedrive-specific behavior
Refresh token rotation — capture the new token on every refresh
Pipedrive rotates refresh tokens on every access token refresh. The old refresh token is invalidated the moment a new one is issued. If your system doesn't capture and store the new refresh token on each cycle, the next refresh will fail with invalid_grant. Scalekit handles this automatically — the rotated token is captured and stored on every refresh without any code changes on your end.
Stage and pipeline IDs differ per workspace
Pipedrive stage and pipeline IDs are integers assigned per account — they are not consistent across customer workspaces. Always call pipedrive_stages_list or pipedrive_pipelines_list at runtime to resolve IDs before creating or updating deals. An agent that hardcodes stage IDs will silently write to the wrong stage for other customers.
Leads use UUIDs; most other objects use integer IDs
Deals, persons, organizations, activities, notes, and most other objects use integer IDs. Leads and goals use UUIDs. Pass the correct type for each tool's ID parameter — mixing them returns a 404, not a type error.
Infrastructure decision
Why not build this yourself
The Pipedrive OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Refresh token rotation — Pipedrive invalidates the old refresh token the moment you use it, so missing the new token in your storage layer locks users out with no warning
PROBLEM 02
Per-user company domain — every API call must go to that user's specific Pipedrive subdomain, which must be captured at token exchange time and stored alongside the token
PROBLEM 03
Revocation on password change — all refresh tokens are immediately invalidated when a user changes their password, requiring graceful re-authorization prompts in your product
PROBLEM 04
Per-user token isolation across a multi-tenant system — one rep's Pipedrive credentials must never be accessible to another, even within the same organization
That's one connector. Your agent product will eventually need Salesforce, HubSpot, Gmail, Linear, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.