Documentation
Wire your agent to Dunbar, see how the tools compose, and look up inputs/outputs when your workflow needs them.
Five minute setup
- Make an API key at /v3/app/account. You see the secret once — copy it.
- Drop the right snippet into your client. Replace
YOUR_KEY. - Connect LinkedIn from /v3/app/account.
- In your client, ask the agent: “list the dunbar tools and call account_status”
- Hand it a workflow and let it run.
Connect a client
Pick the snippet for your client. Replace YOUR_KEY with an API key from /v3/app/account.
# Run once — Claude Code stores it in ~/.claude/settings.json
claude mcp add dunbar --transport http https://www.getdunbar.ai/api/mcp \
--header "Authorization: Bearer YOUR_KEY"// ~/.cursor/mcp.json (or per-project .cursor/mcp.json)
{
"mcpServers": {
"dunbar": {
"url": "https://www.getdunbar.ai/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}// Claude Desktop doesn't pass custom headers to remote servers natively, so
// we use the mcp-remote stdio shim — it runs a tiny local proxy that adds
// the Authorization header on every request.
{
"mcpServers": {
"dunbar": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://www.getdunbar.ai/api/mcp",
"--header", "Authorization:Bearer YOUR_KEY"
]
}
}
}// OAuth (recommended for Claude Desktop / ChatGPT)
// First connection opens a browser, you sign in to Dunbar, you click
// "approve" on the consent screen, and tokens are negotiated automatically.
// No bearer key to copy/paste.
{
"mcpServers": {
"dunbar": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://www.getdunbar.ai/api/mcp"]
}
}
}MCP URL (canonical): https://www.getdunbar.ai/api/mcp
You own the rules. Your agent follows them.
A workflow is your prose playbook — instructions for the agent on how YOU want it to do outreach. Plain text, not code. The agent reads it and follows your rules: who to target, what voice to use, when to ask permission, who to skip, what to do when there's no reply.
Workflows are saved as Claude Skill files — the standard format any agentic harness loads as context. Once you've written one, drop it into your client of choice (Claude Code, Cursor, Claude Desktop, ChatGPT). It works the same in all of them.
We don't lock you into a specific outreach playbook. Write your own from scratch — or fork a starter and edit anything you don't like. Both happen inside our in-app builder at /v3/app — a chat pane on the left for describing your workflow, an editable markdown pane on the right for the file itself.
What the builder gives you
AI co-author
Describe how you do outreach in chat. The builder drafts a workflow you can edit live.
Tool autocomplete
Type / to insert any of the 14 tools. Hover any tool name in the editor to see what it does and what it costs.
Read-only preamble
We auto-prepend a small section that tells the downstream agent what Dunbar is and how the safety rules work. You never have to maintain it.
One-click install
Save and copy. Paste into your harness. Or share a public URL — fetch_skill pulls it on demand.
Common flows
Most tools chain — one returns the input the next one needs.
Cold outreach to people at a specific company
- 1.organization_search→ id (e.g. "Stripe" → 5d0a...)
- 2.people_searchwith organizationIds + titles → linkedinUrl per person
- 3.get_profile→ provider_id (the LinkedIn URN)
- 4.send_connection_requestwith personalized note ≤200 chars
Warm intro through a mutual connection
- 1.get_profileof the target → provider_id
- 2.find_mutuals→ list of mutuals you can reach out to
- 3.send_messageto the mutual asking for the intro
Cold outreach by title (no specific company)
- 1.people_searchtitles + locations → linkedinUrls
- 2.get_profile→ provider_id
- 3.send_connection_requestwith personalized note
Find mutuals across a target list (batch)
- 1.account_statuscheck batches.inflight first — only one batch per user at a time
- 2.find_mutuals_batchwith up to 20 profileUrls → returns jobId
- 3.get_batchpoll every 30–60s until status === 'succeeded'
- 4.send_messagekick off intros to the mutuals output.results returned
Watch for replies + follow up
- 1.list_inbox_eventspoll every minute
- 2.get_conversationfor each new reply
- 3.send_messagerespond
Stay above water on credits
- 1.account_statuscredits.balance + lowBalance flag
- 2.topup_creditsif low — returns a Stripe URL the user clicks
Tool reference
1 credit = 1¢. Volume bonuses kick in at $25/$100/$500.
account_status
Free · <100msReturns LinkedIn connection status, sends-today vs daily cap (50), credit balance + per-tool pricing, and any in-flight batch jobs. Call freely.
▸Schema
// no parameters{
linkedin: {
connected: boolean
status?: string
connectionSyncStatus?: "RUNNING" | "IDLE" | "FAILED"
syncedConnectionCount?: number
}
budget: { sendsToday: number; dailyCap: 50 }
credits: {
balance: number // in credits (1 credit = 1¢)
lowBalance: boolean
lowBalanceThreshold: number
pricing: Record<string, number>
}
batches: {
inflight: { jobId: number; status: string; progress: object } | null
}
}list_connections
Free · <100msPaginated list of the user's first-degree LinkedIn connections from the local DB. No external call.
▸Schema
{
afterId?: number
limit?: number // 1-200, default 50
}{
connections: Array<{
id: number
name: string
title: string | null
company: string | null
profileUrl: string
urn: string
}>
nextAfterId: number | null
}connection_status
Free · 1-5sCheck the user's connection state with a target profile. Returns 'connected' | 'pending' | 'none' with the LinkedIn URN.
▸Schema
{ profileUrl: string /* URL */ }{
status: "connected" | "pending" | "none"
profileUrl: string
urn: string | null
}list_inbox_events
Free · <100msRecent LinkedIn events (replies, accepts, declines). Cheap polling source for the agent.
▸Schema
{
since?: string // ISO timestamp
afterId?: number
limit?: number // 1-100
}{
events: Array<{
id: number
eventType: "reply" | "accept" | "decline" | "message"
profileUrl: string
summary: string | null
occurredAt: string
}>
}fetch_skill
Free · <100msFetch a Dunbar workflow file by its getdunbar.ai/s/<user>/<slug> URL.
▸Schema
{ url: string /* workflow URL or path */ }{
title: string
slug: string
visibility: "public" | "unlisted" | "private"
contentMd: string
}get_profile
1 credit · 1-5sFull LinkedIn profile data for any URL. Returns provider_id (URN) needed by send_connection_request and send_message.
▸Schema
{ profileUrl: string /* URL */ }{
provider_id: string // LinkedIn URN — feeds send_*
public_identifier: string // the /in/<slug> slug
first_name: string
last_name: string
headline: string | null
summary: string | null // "about" section
profile_picture_url: string | null
network_distance: "DISTANCE_1" | "DISTANCE_2" | "OUT_OF_NETWORK"
pending_invitation: boolean // true if YOU sent a pending request
is_premium: boolean
is_open_profile: boolean
follower_count: number | null
connections_count: number | null
location: {
country: string | null
city: string | null
}
current_positions: Array<{
company_name: string
company_id?: string
title: string
description: string | null
start_year: number | null
end_year: number | null // null = current
}>
past_positions: Array<{
company_name: string
title: string
start_year: number | null
end_year: number | null
}>
education: Array<{
school_name: string
degree: string | null
field_of_study: string | null
start_year: number | null
end_year: number | null
}>
skills: string[]
languages: Array<{ name: string; proficiency: string | null }>
}get_conversation
1 credit · 1-5sMessage history for a Unipile chat_id.
▸Schema
{
chatId: string
limit?: number // 1-100
cursor?: string
}{
items: Array<{
sender: string
text: string
timestamp: string
}>
cursor: string | null
}find_mutuals
5 credits · 5-120sMutual connections between user and target profile. The warm-intro primitive. Don't call in tight loops — use find_mutuals_batch for >1 profile.
▸Schema
{
targetProfileUrl: string // URL
targetUrn?: string // skips one lookup if already known
}{
mutuals: Array<{
urn: string
name: string | null
title: string | null
company: string | null
profileUrl: string | null
strength: number
}>
count: number
rawUrnCount: number
targetUrn: string
}organization_search
5 credits · 1-5sLook up companies by name (filter results by domain). The `id` is what people_search accepts — free-text names don't filter reliably.
▸Schema
{
name?: string
domain?: string // client-side filter on results
perPage?: number // 1-25
page?: number // 1+
}{
results: Array<{
id: string // → people_search.organizationIds
name: string | null
domain: string | null
linkedinUrl: string | null
industry: string | null
employees: number | null
description: string | null
}>
page: number
perPage: number
totalAvailable: number | null
hasMore: boolean
nextPage: number | null
searchCapacity: "ok" | "limited" | "depleted"
}people_search
5 credits · 1-5s200M+ person prospect database. Feed each `linkedinUrl` into get_profile to obtain the URN. At least one filter is required.
▸Schema
{
titles?: string[]
organizationIds?: string[] // from organization_search
locations?: string[]
keywords?: string
perPage?: number // 1-25
page?: number // 1+
}{
results: Array<{
name: string | null
title: string | null
company: string | null
location: string | null
linkedinUrl: string // → get_profile
}>
page: number
perPage: number
totalAvailable: number | null
hasMore: boolean
nextPage: number | null
searchCapacity: "ok" | "limited" | "depleted"
}send_connection_request
1 credit · 1-5s · 50/day capLinkedIn connection request with optional ≤200-char note. Server-enforced 50/day rate limit, 7-day dedup.
▸Schema
{
profileUrl: string // URL
providerId: string // LinkedIn URN from get_profile
note?: string // ≤200 chars
}{
ok: boolean
sentAt?: string
reason?: "rate_limit" | "dedup" | "no_linkedin_account" | string
}send_message
1 credit · 1-5s · 50/day capLinkedIn DM. Pass either `chatId` (existing thread) or `providerId` (new chat). Same safety as send_connection_request.
▸Schema
{
profileUrl: string // URL
text: string // ≤8000 chars
chatId?: string // for existing threads
providerId?: string // for new chats — from get_profile
}{
ok: boolean
chatId?: string
reason?: "rate_limit" | "dedup" | "no_linkedin_account" | string
}cancel_connection
1 credit · 1-5sWithdraw a pending invitation. No safety check.
▸Schema
{
profileUrl: string // URL
invitationId: string // Unipile invitation id
}{
ok: boolean
cancelledAt?: string
}find_mutuals_batch
5 credits · 5-120sQueue find_mutuals against up to 20 LinkedIn profiles at once. Reserves 5 credits per profile up front, runs serially in the background with 5s pacing. Returns a jobId to poll. One batch in flight per user — refuses with existingJobId if you already have one running.
▸Schema
{
profileUrls: string[] // 1-20 LinkedIn URLs
}{
jobId: number
status: "queued"
total: number
creditsReserved: number // total × 5
estimatedDurationSec: number
instructions: string // "save jobId, poll get_batch, ..."
}get_batch
Free · <100msPoll status + results for a batch job. Free — call every 30–60 seconds while running. Per-profile errors live inside `output.results[url].error` — the batch can succeed even if individual profiles failed.
▸Schema
{ jobId: number }{
jobId: number
status: "queued" | "running" | "succeeded" | "failed" | "canceled"
progress: {
processed: number
total: number
currentUrl: string | null
}
output?: { // populated when status === succeeded
results: Record<string,
| { mutuals: Array<{ urn; name; profileUrl }>; count: number }
| { error: string }
>
}
errorMessage?: string
completedAt?: string
}cancel_batch
Free · <100msStop an in-progress batch and refund credits for unstarted profiles. The currently-running profile is not refunded. Already-completed profiles stay in `output`.
▸Schema
{ jobId: number }{
canceled: boolean
processedAtCancel: number
total: number
refundedCredits: number
}topup_credits
Free · 1-5sReturns a Stripe Checkout URL the user opens. The agent cannot complete the payment. Volume bonuses: $25 +5%, $100 +10%, $500 +20%.
▸Schema
{
amountUsd: number // whole dollars, $5-$5,000
}{
checkoutUrl: string
amountUsd: number
creditsAwarded: number
bonusCredits: number
bonusPct: number
instructions: string // "show URL to user, they must click it"
}Pay as you go
Credits never expire. No seat fees, no tiers.
See your live balance and top up at /v3/app/billing.