DOCS

Documentation

Wire your agent to Dunbar, see how the tools compose, and look up inputs/outputs when your workflow needs them.

QUICKSTART

Five minute setup

  1. Make an API key at /v3/app/account. You see the secret once — copy it.
  2. Drop the right snippet into your client. Replace YOUR_KEY.
  3. Connect LinkedIn from /v3/app/account.
  4. In your client, ask the agent: “list the dunbar tools and call account_status”
  5. Hand it a workflow and let it run.
SETUP

Connect a client

Pick the snippet for your client. Replace YOUR_KEY with an API key from /v3/app/account.

CLAUDE CODE (CLI)
Easiest — one command. Restart Claude Code afterward.
# 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
Drop the JSON into ~/.cursor/mcp.json (create the file if it doesn't exist).
// ~/.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 · API KEY + SHIM
Claude Desktop can't pass headers natively, so we use mcp-remote. Restart Claude Desktop after editing.
// 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"
      ]
    }
  }
}
CLAUDE DESKTOP · OAUTH (NO KEY COPY/PASTE)
Recommended path for Claude Desktop. Opens a browser for sign-in + consent on first connect.
// 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

WRITING WORKFLOWS

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.

PATTERNS

Common flows

Most tools chain — one returns the input the next one needs.

Cold outreach to people at a specific company

  1. 1.organization_search→ id (e.g. "Stripe" → 5d0a...)
  2. 2.people_searchwith organizationIds + titles → linkedinUrl per person
  3. 3.get_profile→ provider_id (the LinkedIn URN)
  4. 4.send_connection_requestwith personalized note ≤200 chars

Warm intro through a mutual connection

  1. 1.get_profileof the target → provider_id
  2. 2.find_mutuals→ list of mutuals you can reach out to
  3. 3.send_messageto the mutual asking for the intro

Cold outreach by title (no specific company)

  1. 1.people_searchtitles + locations → linkedinUrls
  2. 2.get_profile→ provider_id
  3. 3.send_connection_requestwith personalized note

Find mutuals across a target list (batch)

  1. 1.account_statuscheck batches.inflight first — only one batch per user at a time
  2. 2.find_mutuals_batchwith up to 20 profileUrls → returns jobId
  3. 3.get_batchpoll every 30–60s until status === 'succeeded'
  4. 4.send_messagekick off intros to the mutuals output.results returned

Watch for replies + follow up

  1. 1.list_inbox_eventspoll every minute
  2. 2.get_conversationfor each new reply
  3. 3.send_messagerespond

Stay above water on credits

  1. 1.account_statuscredits.balance + lowBalance flag
  2. 2.topup_creditsif low — returns a Stripe URL the user clicks
REFERENCE

Tool reference

1 credit = 1¢. Volume bonuses kick in at $25/$100/$500.

READS

account_status

Free · <100ms

Returns LinkedIn connection status, sends-today vs daily cap (50), credit balance + per-tool pricing, and any in-flight batch jobs. Call freely.

Schema
PARAMETERS
// no parameters
RETURNS
{
  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 · <100ms

Paginated list of the user's first-degree LinkedIn connections from the local DB. No external call.

Schema
PARAMETERS
{
  afterId?: number
  limit?: number               // 1-200, default 50
}
RETURNS
{
  connections: Array<{
    id: number
    name: string
    title: string | null
    company: string | null
    profileUrl: string
    urn: string
  }>
  nextAfterId: number | null
}

connection_status

Free · 1-5s

Check the user's connection state with a target profile. Returns 'connected' | 'pending' | 'none' with the LinkedIn URN.

Schema
PARAMETERS
{ profileUrl: string /* URL */ }
RETURNS
{
  status: "connected" | "pending" | "none"
  profileUrl: string
  urn: string | null
}

list_inbox_events

Free · <100ms

Recent LinkedIn events (replies, accepts, declines). Cheap polling source for the agent.

Schema
PARAMETERS
{
  since?: string               // ISO timestamp
  afterId?: number
  limit?: number               // 1-100
}
RETURNS
{
  events: Array<{
    id: number
    eventType: "reply" | "accept" | "decline" | "message"
    profileUrl: string
    summary: string | null
    occurredAt: string
  }>
}

fetch_skill

Free · <100ms

Fetch a Dunbar workflow file by its getdunbar.ai/s/<user>/<slug> URL.

Schema
PARAMETERS
{ url: string /* workflow URL or path */ }
RETURNS
{
  title: string
  slug: string
  visibility: "public" | "unlisted" | "private"
  contentMd: string
}

get_profile

1 credit · 1-5s

Full LinkedIn profile data for any URL. Returns provider_id (URN) needed by send_connection_request and send_message.

Schema
PARAMETERS
{ profileUrl: string /* URL */ }
RETURNS
{
  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-5s

Message history for a Unipile chat_id.

Schema
PARAMETERS
{
  chatId: string
  limit?: number               // 1-100
  cursor?: string
}
RETURNS
{
  items: Array<{
    sender: string
    text: string
    timestamp: string
  }>
  cursor: string | null
}

find_mutuals

5 credits · 5-120s

Mutual connections between user and target profile. The warm-intro primitive. Don't call in tight loops — use find_mutuals_batch for >1 profile.

Schema
PARAMETERS
{
  targetProfileUrl: string     // URL
  targetUrn?: string           // skips one lookup if already known
}
RETURNS
{
  mutuals: Array<{
    urn: string
    name: string | null
    title: string | null
    company: string | null
    profileUrl: string | null
    strength: number
  }>
  count: number
  rawUrnCount: number
  targetUrn: string
}
PROSPECTING

organization_search

5 credits · 1-5s

Look up companies by name (filter results by domain). The `id` is what people_search accepts — free-text names don't filter reliably.

Schema
PARAMETERS
{
  name?: string
  domain?: string              // client-side filter on results
  perPage?: number             // 1-25
  page?: number                // 1+
}
RETURNS
{
  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-5s

200M+ person prospect database. Feed each `linkedinUrl` into get_profile to obtain the URN. At least one filter is required.

Schema
PARAMETERS
{
  titles?: string[]
  organizationIds?: string[]   // from organization_search
  locations?: string[]
  keywords?: string
  perPage?: number             // 1-25
  page?: number                // 1+
}
RETURNS
{
  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"
}
WRITES

send_connection_request

1 credit · 1-5s · 50/day cap

LinkedIn connection request with optional ≤200-char note. Server-enforced 50/day rate limit, 7-day dedup.

Schema
PARAMETERS
{
  profileUrl: string           // URL
  providerId: string           // LinkedIn URN from get_profile
  note?: string                // ≤200 chars
}
RETURNS
{
  ok: boolean
  sentAt?: string
  reason?: "rate_limit" | "dedup" | "no_linkedin_account" | string
}

send_message

1 credit · 1-5s · 50/day cap

LinkedIn DM. Pass either `chatId` (existing thread) or `providerId` (new chat). Same safety as send_connection_request.

Schema
PARAMETERS
{
  profileUrl: string           // URL
  text: string                 // ≤8000 chars
  chatId?: string              // for existing threads
  providerId?: string          // for new chats — from get_profile
}
RETURNS
{
  ok: boolean
  chatId?: string
  reason?: "rate_limit" | "dedup" | "no_linkedin_account" | string
}

cancel_connection

1 credit · 1-5s

Withdraw a pending invitation. No safety check.

Schema
PARAMETERS
{
  profileUrl: string           // URL
  invitationId: string         // Unipile invitation id
}
RETURNS
{
  ok: boolean
  cancelledAt?: string
}
BATCHES

find_mutuals_batch

5 credits · 5-120s

Queue 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
PARAMETERS
{
  profileUrls: string[]        // 1-20 LinkedIn URLs
}
RETURNS
{
  jobId: number
  status: "queued"
  total: number
  creditsReserved: number      // total × 5
  estimatedDurationSec: number
  instructions: string         // "save jobId, poll get_batch, ..."
}

get_batch

Free · <100ms

Poll 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
PARAMETERS
{ jobId: number }
RETURNS
{
  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 · <100ms

Stop an in-progress batch and refund credits for unstarted profiles. The currently-running profile is not refunded. Already-completed profiles stay in `output`.

Schema
PARAMETERS
{ jobId: number }
RETURNS
{
  canceled: boolean
  processedAtCancel: number
  total: number
  refundedCredits: number
}
BILLING

topup_credits

Free · 1-5s

Returns a Stripe Checkout URL the user opens. The agent cannot complete the payment. Volume bonuses: $25 +5%, $100 +10%, $500 +20%.

Schema
PARAMETERS
{
  amountUsd: number            // whole dollars, $5-$5,000
}
RETURNS
{
  checkoutUrl: string
  amountUsd: number
  creditsAwarded: number
  bonusCredits: number
  bonusPct: number
  instructions: string         // "show URL to user, they must click it"
}
PRICING

Pay as you go

Credits never expire. No seat fees, no tiers.

Free
account_status, list_connections, list_inbox_events, fetch_skill, connection_status, topup_credits, get_batch, cancel_batch
0 credits
Actions
send_message, send_connection_request, get_profile, get_conversation, cancel_connection
1 credit
Search
find_mutuals, people_search, organization_search, find_mutuals_batch (per profile)
5 credits

See your live balance and top up at /v3/app/billing.

TROUBLESHOOTING

Common errors

401 Unauthorized
Missing or wrong Authorization header. The whole header value should be Bearer dnb_… — the word Bearer, a space, then your key.
insufficient_credits
Your balance is below the tool's cost. Tell the user to top up at /v3/app/billing or call topup_credits to send them a Stripe URL.
account_status returns linkedin.connected: false
LinkedIn isn't connected yet. Visit /v3/app/account → 'Connect LinkedIn'. Until you do, all LinkedIn-touching tools return a 'no_linkedin_account' error.
list_connections returns []
LinkedIn is connected but the initial sync hasn't finished. Check linkedin.connectionSyncStatus in account_status — RUNNING during sync, IDLE when complete.
send_connection_request returns ok: false, reason: 'rate_limit'
You hit the 50/day cap. Resets on a rolling 24h window — the reason field includes the exact retryAfter timestamp.
send_connection_request returns ok: false, reason: 'dedup'
You already sent a request to that profile in the last 7 days. Skip the lead — agents should treat dedup as 'try again later'.
find_mutuals takes 30+ seconds
Expected — it makes a LinkedIn Voyager call and runs a small LLM parse. We allow up to 120 seconds. Don't call it in a tight loop.