The real problem
Why this is harder than it looks
Affinity's REST API is clean and the documentation covers both V1 and V2. A working prototype against a single workspace takes an afternoon. The complexity arrives when you try to support real users in a multi-tenant product — and like Zendesk, Affinity does not use OAuth 2.0.
Affinity authenticates via a static Bearer token — a credential generated per user in Affinity Settings under Manage Apps. That token inherits the permissions of the user who created it. In a B2B product where each of your customers operates their own Affinity workspace, you're managing a credential vault: one API key per user, each scoped to a different workspace. There's no OAuth redirect flow to standardize collection — you have to build the UI to prompt users for their key, store it encrypted, and route every API call with the right token. Enterprise accounts can also restrict which roles can generate API keys at all, meaning a user who appears fully set up may silently fail when their token lacks admin-level access. Additionally, if a user is deactivated in Affinity, their API key is automatically revoked — with no webhook or notification — and calls start returning 401s with no indication of why.
Beyond credential management, the Affinity API splits functionality across V1 and V2 — and V2 doesn't yet cover everything V1 does. An agent that needs to mix list operations (V1) with field-level updates (V2) has to handle two base URLs, two pagination styles, and two response schemas in parallel. Getting this wrong means silent data gaps or misrouted writes.
Scalekit handles credential storage, per-user token injection, and account status tracking. Your agent names a tool and passes parameters. The auth plumbing is not your problem.
Capabilities
What your agent can do with Affinity
Once connected, your agent has 13 pre-built tools covering deal pipelines, relationship intelligence, and CRM records:
- Search and retrieve people and organizations: search Affinity's network by name, email, or domain; fetch full profiles with interaction dates and relationship scores
- Manage deal pipelines end-to-end: create, retrieve, update, and list opportunities with stage, owner, and associated entity support
- Log notes on any entity: attach meeting summaries, due diligence findings, or context notes to people, organizations, or opportunities simultaneously
- Query relationship strength: retrieve interaction-based scores between team members and external contacts to surface the best warm introduction path
- Manage list membership: add people or organizations to any Affinity list; list all available pipelines and workspace lists
Setup context
What we're building
This guide connects a deal intelligence agent to Affinity — helping investors and sales teams track pipeline, log context on deals, and surface relationship paths without leaving your product.
🤖
Example agent
Deal intelligence assistant querying pipeline opportunities, logging meeting notes, and surfacing warm intro paths on behalf of each user
🔐
Auth model
Bearer Token — each user supplies their Affinity API key. 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 Affinity secrets, no user tokens, nothing belonging to your users.
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
Affinity uses Bearer Token authentication — there is no OAuth redirect flow. Each user's connected account is provisioned directly with their Affinity API key. The identifier is any unique string from your system.
In the Scalekit dashboard, go to Agent Auth → Connected Accounts for your Affinity connection and click Add account. Supply the user's identifier and their Affinity API key. Scalekit stores the credential encrypted and injects it into every API call automatically.
response = actions.get_or_create_connected_account(
connection_name="affinity",
identifier="user_aff_123" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: ACTIVE — credentials were supplied at account creation
const response = await actions.getOrCreateConnectedAccount({
connectionName: "affinity",
identifier: "user_aff_123" // your internal user ID
});
const connectedAccount = response.connectedAccount;
console.log(`Status: ${connectedAccount.status}`);
// Status: ACTIVE — credentials were supplied at account creation
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Credential handling
3 Credential management
Because Affinity uses API keys rather than OAuth, there is no user-facing authorization step. Credentials are entered once in the Scalekit dashboard (or via API) per connected account. Scalekit stores them encrypted and uses them on every tool call.
Credential storage is automatic
Once a connected account is provisioned with an Affinity API key, Scalekit stores it in its encrypted vault
and the account is immediately ACTIVE. Every tool call is injected with the correct Bearer token — your
agent code never touches it. If the credential becomes invalid (e.g. the user is deactivated in Affinity
or rotates their key), the account moves to REVOKED. Check account.status before critical operations
and surface a re-credentialing prompt.
Generating an Affinity API key
In Affinity, go to Settings → Manage Apps and click Generate an API key. The key is shown only once — copy
it immediately. The key inherits the permissions of the user who generated it; use an account with the
appropriate role for the operations your agent will perform. Enterprise accounts may restrict key generation
to Admins only.
Calling Affinity
4 Calling Affinity: What your agent writes
With the connected account active, your agent calls Affinity actions using execute_tool. Name the tool, pass parameters. Scalekit handles credential injection and response parsing.
Search for a person
Search Affinity's network by name or email. Use with_interaction_dates to include recency signals — useful for surfacing warm vs. cold contacts.
result = actions.execute_tool(
identifier="user_aff_123",
tool_name="affinity_search_persons",
tool_input={
"term": "Jane Smith",
"with_interaction_dates": True,
"page_size": 10
}
)
# Returns: list of person records with id, name, emails, organization memberships, interaction dates
const result = await actions.executeTool({
identifier: "user_aff_123",
toolName: "affinity_search_persons",
toolInput: {
"term": "Jane Smith",
"with_interaction_dates": true,
"page_size": 10
}
});
// Returns: list of person records with id, name, emails, organization memberships, interaction dates
List pipeline opportunities
Retrieve deals from a specific pipeline list. Use list_id to scope to a particular pipeline — get list IDs first with affinity_list_lists.
result = actions.execute_tool(
identifier="user_aff_123",
tool_name="affinity_list_opportunities",
tool_input={
"list_id": 12058,
"page_size": 25
}
)
# Returns: paginated deal records with stage, value, associated persons and organizations
const result = await actions.executeTool({
identifier: "user_aff_123",
toolName: "affinity_list_opportunities",
toolInput: {
"list_id": 12058,
"page_size": 25
}
});
// Returns: paginated deal records with stage, value, associated persons and organizations
Create a note on a deal
Log a note directly on an opportunity — and optionally attach it to the associated person and organization in the same call. All entity ID fields are optional; at least one is required.
result = actions.execute_tool(
identifier="user_aff_123",
tool_name="affinity_create_note",
tool_input={
"content": "Initial call with founder. Strong product-market fit signal. Team has prior exit. Follow up in 2 weeks.",
"opportunity_ids": [98431],
"person_ids": [44320],
"organization_ids": [7133202]
}
)
# Returns: created note ID and confirmation
const result = await actions.executeTool({
identifier: "user_aff_123",
toolName: "affinity_create_note",
toolInput: {
"content": "Initial call with founder. Strong product-market fit signal. Team has prior exit. Follow up in 2 weeks.",
"opportunity_ids": [98431],
"person_ids": [44320],
"organization_ids": [7133202]
}
});
// Returns: created note ID and confirmation
Get relationship strength
Retrieve interaction-based scores between your team and an external contact. Omit internal_id to get scores across the whole team — useful for finding the best intro path to a founder or LP.
result = actions.execute_tool(
identifier="user_aff_123",
tool_name="affinity_get_relationship_strength",
tool_input={
"external_id": 44320
# omit internal_id to get scores for all team members
}
)
# Returns: strength scores per team member, based on email and meeting frequency and recency
const result = await actions.executeTool({
identifier: "user_aff_123",
toolName: "affinity_get_relationship_strength",
toolInput: {
"external_id": 44320
// omit internal_id to get scores for all team members
}
});
// Returns: strength scores per team member, based on email and meeting frequency and recency
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to look up or act on; Scalekit handles credential injection on every invocation. No credential 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
aff_tools = get_tools(
connection_name="affinity",
identifier="user_aff_123"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a deal intelligence assistant. Use the available tools to help manage Affinity pipeline, log meeting notes, and surface relationship context."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), aff_tools, prompt)
result = AgentExecutor(agent=agent, tools=aff_tools).invoke({
"input": "Find all open opportunities in our Series A pipeline and log a summary note on each deal reviewed today"
})
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 affTools = getTools({
connectionName: "affinity",
identifier: "user_aff_123"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a deal intelligence assistant. Use the available tools to help manage Affinity pipeline, log meeting notes, and surface relationship context."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: affTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: affTools
}).invoke({
input: "Find all open opportunities in our Series A pipeline and log a summary note on each deal reviewed today"
});
Other frameworks supported
Tool reference
All 13 Affinity tools
Grouped by object and capability. Your agent calls tools by name — no API wrappers to write.
affinity_create_opportunity
Create a new deal or opportunity in an Affinity pipeline list. Supports associating persons and organizations, setting the name, and assigning an owner
Retrieve full deal details including stage, owner, associated persons and organizations, custom field values, and list membership
affinity_update_opportunity
Update an existing opportunity — rename, or add/remove associated persons and organizations by ID
affinity_list_opportunities
List pipeline opportunities with optional filters by list ID. Returns paginated deal records with stage, value, and associated entities
Search for people by name or email address. Returns contact info, organization memberships, and optional interaction dates
Retrieve a person's full profile including emails, phone numbers, organization memberships, and relationship score. Supports optional interaction dates
affinity_search_organizations
Search companies by name or domain. Returns org records with team connections, domain info, and optional interaction metadata
affinity_get_organization
Retrieve a company's full profile including domain, team connections, associated people, deal history, and interaction metadata
Create a plain-text note and attach it to any combination of people, organizations, or opportunities simultaneously
Retrieve notes for a person, organization, or opportunity. Returns paginated records with content, creator, and timestamp
Retrieve all lists in the workspace — people lists, org lists, and deal pipelines. Returns list IDs, names, types, and owner info
Add a person or organization to an Affinity list by entity ID and list ID
affinity_get_relationship_strength
Retrieve relationship strength scores between team members and an external contact, based on email and meeting frequency and recency. Identify the best warm intro path
Connector notes
Affinity-specific behavior
API keys are revoked when a user is deactivated
If a user is deactivated in Affinity, their API key is automatically and immediately revoked — with no
webhook or notification. Calls using that key will start returning 401s. The connected account will move
to REVOKED in Scalekit. Surface a re-credentialing flow rather than returning a generic error, and use
a key from an active account with appropriate permissions.
API key permissions are role-dependent
Affinity Enterprise accounts can restrict API key generation to Admin roles only. A non-admin user may
appear fully set up but fail silently if their role cannot access certain endpoints. Use a key from an
account with the right permissions for the operations your agent performs.
list_id is required context for most write operations
Creating an opportunity or adding an entity to a list requires a valid list_id. Use affinity_list_lists
first to discover list IDs in the workspace — they are not predictable and differ across Affinity instances.
Store list IDs alongside user credentials rather than fetching them on every agent call.
Infrastructure decision
Why not build this yourself
The Affinity API is documented. Credential storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Per-user Bearer token storage and injection — one encrypted API key per user, each scoped to a different Affinity workspace, with no OAuth flow to standardize collection
PROBLEM 02
Silent revocation when users are deactivated — no webhook, no notification, just 401s — requires per-account status tracking and re-credentialing flows without OAuth lifecycle hooks
PROBLEM 03
Per-user credential isolation in a multi-tenant system — one customer's Affinity token must never be used for another customer's workspace
PROBLEM 04
Role-based key generation restrictions, V1/V2 API surface split, and IP allowlist enforcement are all failure modes that only surface with real users in production
That's one connector. Your agent product will eventually need Salesforce, Gmail, HubSpot, Linear, and whatever else your customers ask for. Each has its own auth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.