CitationBenchTalk to Sales
API referenceLink Building

SERP outreach API — turn a keyword into a scoped outreach campaign

Turn a single keyword into a fully scoped outreach campaign. Fetches the top SERP, filters junk, discovers contacts, drafts personalized emails, and parks the campaign at WAITING_APPROVAL for review.

Turn a single keyword into a fully scoped outreach campaign in minutes. CitationBench fetches the top SERP results, filters out junk (own domains, ineligible page types, irrelevant rankers), discovers contacts at each remaining domain, drafts personalized outreach emails, and parks the whole campaign at WAITING_APPROVAL for your review.

Conceptual overview

A SERP outreach campaign is a parent–child job graph:

serp_outreach campaign
├── research.serp (fetch top N results)
├── filter pipeline (rules + domain dedup)
├── per-domain children:
│   ├── link_building.crm.contact.discover (Apollo)
│   ├── link_building.campaign.draft_email (LLM, personalized)
│   └── (paused at WAITING_APPROVAL)
└── summary report

It creates one durable SearchCampaign record + N LinkBuildingRelationship records (one per surviving domain) + N drafted-but-unsent LinkBuildingEvents. You then approve drafts individually or in bulk; sending is gated by your workspace's approval policy.

→ Concept: Link Building for the CRM data model. → Concept: Approval Workflows for how pausing works.

Endpoints

MethodPathPurpose
POST/v1/link-building/serp-outreachStart a campaign
GET/v1/link-building/serp-outreachList campaigns
GET/v1/link-building/serp-outreach/{id}Get one (with summary)
GET/v1/link-building/serp-outreach/{id}/draftsList drafted outreach emails
POST/v1/link-building/serp-outreach/{id}/approveBulk approve drafts
POST/v1/link-building/serp-outreach/{id}/rejectBulk reject drafts
PATCH/v1/link-building/serp-outreach/{id}Update filters / pause
DELETE/v1/link-building/serp-outreach/{id}Cancel campaign

POST /v1/link-building/serp-outreach

Request

POST /v1/link-building/serp-outreach HTTP/1.1
Host: api.citationbench.com
Authorization: Bearer sk_live_***
X-Workspace-Id: ws_acme
Content-Type: application/json
Idempotency-Key: 7f3a1b18-7d8b-4f3e-9c4b-2c1a3e0a9b8f

{
  "seed": {
    "keyword": "best project management software for engineering teams",
    "country": "us",
    "language": "en",
    "device": "desktop"
  },
  "filters": {
    "topN": 50,
    "positionRange": [1, 50],
    "excludeRootPages": true,
    "excludeSubdomains": ["amazon.com", "youtube.com", "reddit.com"],
    "minDomainRating": 30,
    "maxDomainRating": 85,
    "requireContactDiscovery": true,
    "excludeAlreadyContactedWithinDays": 180,
    "excludeStatuses": ["NOT_INTERESTED", "DO_NOT_CONTACT"]
  },
  "outreach": {
    "ourContentUrl": "https://acme.com/blog/engineering-team-capacity-tracking",
    "targetingAngle": "guest_post",
    "emailTemplate": "tpl_friendly-introduction",
    "personalize": true,
    "approval": { "required": true }
  },
  "tags": ["q2-2026", "engineering-icp"]
}

Parameters

FieldTypeRequiredDefaultNotes
seed.keywordstringyesThe SERP query
seed.country / language / deviceenumnoworkspace default
filters.topNnumberno50SERP positions to consider
filters.positionRange[number, number]no[1, 50]Refine to a positional band
filters.excludeRootPagesbooleannotrueSkip homepage results — usually too generic
filters.excludeSubdomainsstring[]noglobal defaultsDomains to always skip
filters.minDomainRating / maxDomainRatingnumberno30 / 85Ahrefs DR range; skips both crap and giants
filters.requireContactDiscoverybooleannotrueSkip domains where Apollo finds no contacts
filters.excludeAlreadyContactedWithinDaysnumberno180Don't re-pester recent contacts
filters.excludeStatusesenum[]no["NOT_INTERESTED", "DO_NOT_CONTACT"]LinkBuildingAccount.systemContactStatus values to skip
outreach.ourContentUrlstringyesThe URL you want to pitch as the link/asset
outreach.targetingAngleenumno"link_insertion"link_insertion, guest_post, resource_page, broken_link_replacement, link_swap
outreach.emailTemplatestringnoworkspace defaultTemplate slug; LLM personalizes per recipient
outreach.personalizebooleannotrueLLM personalization (each draft cites specifics from the target page)
outreach.approval.requiredbooleannoworkspace policyIf true, drafts park at WAITING_APPROVAL
tagsstring[]no[]

Response

HTTP/1.1 202 Accepted

{
  "campaignId":   "scmp_01HVZ...",
  "invocationId": "inv_01HVZ...",
  "agentId":      "agt_01HVZ...",
  "skill":        "link_building.serp_outreach",
  "status":       "RUNNING",
  "estimatedCost": { "credits": 38, "durationSeconds": 480 },
  "estimatedFinalSize": { "draftsLikely": 12, "afterFilters": "8-18 (depends on Apollo coverage)" },
  "links": {
    "self":    "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_01HVZ...",
    "events":  "https://api.citationbench.com/v1/agent/invocations/inv_01HVZ.../events",
    "drafts":  "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_01HVZ.../drafts"
  }
}

Final summary (when complete)

{
  "campaignId": "scmp_01HVZ...",
  "invocationId": "inv_01HVZ...",
  "agentId": "agt_01HVZ...",
  "skill": "link_building.serp_outreach",
  "skillsUsed": [
    "link_building.serp_outreach",
    "research.serp",
    "link_building.crm.contact.discover"
  ],
  "status": "AWAITING_APPROVAL",
  "creditsUsed": 36,
  "summary": {
    "serpResultsFetched": 50,
    "afterRootPageFilter": 38,
    "afterDomainExclusion": 31,
    "afterDomainRatingFilter": 24,
    "afterAlreadyContactedFilter": 19,
    "afterContactDiscovery": 14,
    "finalDrafts": 14
  },
  "topDomains": [
    {
      "domain": "engineering-blog.com",
      "drafted": true,
      "rank": 4,
      "domainRating": 62
    },
    {
      "domain": "devleadweekly.com",
      "drafted": true,
      "rank": 9,
      "domainRating": 48
    },
    {
      "domain": "amazon.com",
      "drafted": false,
      "rank": 1,
      "skippedReason": "excluded_subdomain"
    }
  ],
  "raw": "Pulled the top 50 SERP results for \"best project management software for engineering teams\" via DataForSEO. Stripped the 12 root-domain pages and 7 always-excluded subdomains. The remaining 31 went through DR filtering — kept 24. After deduping against contacts we'd reached in the last 180 days, 19 remained. Apollo found contacts for 14 of those; for the other 5 I marked the relationship NO_CONTACT_FOUND ...",
  "files": [
    "agent-workspace/serp-raw.json",
    "agent-workspace/filter-decisions.csv",
    "agent-workspace/drafts.md"
  ]
}

See Agent · invoke § Universal response envelope for what raw and files contain.


Inspect what got drafted. Useful before bulk-approving.

curl -G "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_***/drafts" \
  -H "Authorization: Bearer sk_live_***" \
  --data-urlencode "status=AWAITING_APPROVAL" \
  --data-urlencode "limit=20"

Response

{
  "data": [
    {
      "draftId": "draft_***",
      "relationshipId": "rel_***",
      "accountId": "acct_***",
      "accountDomain": "engineering-blog.com",
      "contact": {
        "id": "ct_***",
        "firstName": "Marina",
        "lastName": "Olafsson",
        "email": "marina@engineering-blog.com",
        "position": "Senior Editor"
      },
      "serpContext": {
        "rank": 4,
        "title": "12 PM tools for engineering teams in 2026",
        "url": "https://engineering-blog.com/best-pm-tools"
      },
      "subject": "Section 7 of your PM tools roundup",
      "body": "Hi Marina,\n\nThe section in your '12 PM tools for engineering teams in 2026' on capacity tracking lines up with a piece we just published on the same problem space — happy to share what we found ...",
      "draftedBy": "agent_link_hunter@v3",
      "draftedAt": "2026-05-24T08:14:27Z",
      "status": "AWAITING_APPROVAL",
      "approvalUrl": "https://api.citationbench.com/v1/agent/approvals/appr_***"
    },
    {
      "draftId": "draft_***",
      "...": "..."
    }
  ],
  "nextCursor": null,
  "total": 14
}

POST /v1/link-building/serp-outreach/{id}/approve

Bulk approve. Sends each approved draft via the workspace's configured outreach platform (Instantly.ai by default).

curl -X POST "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_***/approve" \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "draftIds": ["draft_***", "draft_***"],
    "scheduleAt": "2026-05-25T09:00:00-04:00"
  }'
FieldNotes
draftIdsnull or "all" to approve every draft in AWAITING_APPROVAL
scheduleAtISO timestamp; omit to send immediately
editsPerDraftMap of draftId{subject?, body?} for last-mile edits before send

Response:

{
  "approved": 13,
  "scheduled": 13,
  "rejected": 0,
  "events": [
    {
      "draftId": "draft_***",
      "eventId": "evt_***",
      "eventType": "EMAIL_SENT",
      "sentAt": "2026-05-25T09:00:00-04:00",
      "instantlyMessageId": "im_***"
    }
  ]
}

POST /v1/link-building/serp-outreach/{id}/reject

curl -X POST "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_***/reject" \
  -d '{
    "draftIds": ["draft_***"],
    "reason": "Wrong audience",
    "markRelationship": "NOT_INTERESTED"
  }'

Sets the relationship's status accordingly so the same account is skipped in future campaigns.


Adjust filters mid-run or pause.

curl -X PATCH "https://api.citationbench.com/v1/link-building/serp-outreach/scmp_***" \
  -d '{
    "filters": { "minDomainRating": 50 },
    "paused": true
  }'

Setting paused: true halts new drafts but doesn't cancel existing ones. Resume by paused: false.


Cancels the campaign. In-flight contact discovery and drafting are aborted. Already-drafted-but-unsent emails are marked CANCELLED. Already-sent emails are untouched.


MCP

> Turn the SERP for "best project management software for engineering teams" into an outreach campaign.
  Use the friendly intro template. Target top 30 results, exclude amazon and reddit.

Claude calls link_building.serp_outreach.create with the right filters.

> Show me the drafts for that campaign — let me approve them one by one.

Claude calls link_building.serp_outreach.drafts.list, then walks through each one in chat, calling link_building.serp_outreach.approve per draft after you say yes.


Errors

StatusCodeCause
400validation_errorMissing seed.keyword or outreach.ourContentUrl
402insufficient_credits
403outreach_platform_not_configuredNo Instantly (or equivalent) connection in this workspace
422no_drafts_producedAll SERP results were filtered out (try loosening filters)
503external_unavailableDataForSEO, Ahrefs, or Apollo unreachable

Cost

ActionCredits
POST /v1/link-building/serp-outreach (per 50-result campaign)~20
+ per-domain contact discovery (Apollo)+0.4 each
+ per-draft LLM personalization+0.5 each
POST /.../approve (per email actually sent)2

A typical campaign that produces ~15 sent emails costs ~50 credits end-to-end.


Use cases (string things together)

A. From keyword research → outreach campaign in one chain

# 1. Find a high-intent keyword you want to win
KW_ID=$(curl -sf -X POST .../v1/keywords/search -d '{
  "intent": ["ALTERNATIVE"],
  "relevance": ["INCUMBENT"],
  "limit": 1
}' | jq -r '.data[0].id')
KEYWORD=$(curl -sf .../v1/keywords/$KW_ID | jq -r '.keyword')

# 2. Fire the campaign
curl -X POST .../v1/link-building/serp-outreach -d "{
  \"seed\": { \"keyword\": \"$KEYWORD\" },
  \"outreach\": { \"ourContentUrl\": \"https://acme.com/blog/our-comparison-post\" }
}"
curl -X POST .../v1/agent/invoke -d '{
  "skill": "link_hunter",
  "input": {
    "seedKeywords": ["best project management software for engineering teams"],
    "ourContentUrls": ["https://acme.com/blog/engineering-team-capacity-tracking"],
    "cadence": "weekly",
    "maxOutreachPerCycle": 25
  },
  "approval": { "required": true }
}'

The skill runs link_building.serp_outreach weekly, drafts emails, pauses for your approval, sends approved ones, and tracks responses.

C. Run the same campaign across a portfolio of clients

curl -X POST .../v1/workspaces/bulk-action \
  -H "Authorization: Bearer sk_live_agency_***" \
  -d '{
    "action": "link_building.serp_outreach.create",
    "workspaces": "all",
    "config": {
      "seed": { "keyword": "{{ workspace.primaryKeyword }}" },
      "outreach": { "ourContentUrl": "{{ workspace.featuredContentUrl }}" }
    }
  }'

Workspace-scoped variables ({{ workspace.X }}) resolve per child workspace. One call fans out to N campaigns.

D. Drafts auto-routed to a client portal for approval

# Register the webhook once
curl -X POST .../v1/webhooks -d '{
  "url": "https://hooks.our-portal.com/lb-drafts",
  "events": ["link_building.draft.awaiting_approval"]
}'

# The portal renders each draft and POSTs back to
# .../v1/link-building/serp-outreach/{id}/approve when the client clicks.

On this page