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 reportIt 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
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/link-building/serp-outreach | Start a campaign |
| GET | /v1/link-building/serp-outreach | List campaigns |
| GET | /v1/link-building/serp-outreach/{id} | Get one (with summary) |
| GET | /v1/link-building/serp-outreach/{id}/drafts | List drafted outreach emails |
| POST | /v1/link-building/serp-outreach/{id}/approve | Bulk approve drafts |
| POST | /v1/link-building/serp-outreach/{id}/reject | Bulk 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
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
seed.keyword | string | yes | — | The SERP query |
seed.country / language / device | enum | no | workspace default | — |
filters.topN | number | no | 50 | SERP positions to consider |
filters.positionRange | [number, number] | no | [1, 50] | Refine to a positional band |
filters.excludeRootPages | boolean | no | true | Skip homepage results — usually too generic |
filters.excludeSubdomains | string[] | no | global defaults | Domains to always skip |
filters.minDomainRating / maxDomainRating | number | no | 30 / 85 | Ahrefs DR range; skips both crap and giants |
filters.requireContactDiscovery | boolean | no | true | Skip domains where Apollo finds no contacts |
filters.excludeAlreadyContactedWithinDays | number | no | 180 | Don't re-pester recent contacts |
filters.excludeStatuses | enum[] | no | ["NOT_INTERESTED", "DO_NOT_CONTACT"] | LinkBuildingAccount.systemContactStatus values to skip |
outreach.ourContentUrl | string | yes | — | The URL you want to pitch as the link/asset |
outreach.targetingAngle | enum | no | "link_insertion" | link_insertion, guest_post, resource_page, broken_link_replacement, link_swap |
outreach.emailTemplate | string | no | workspace default | Template slug; LLM personalizes per recipient |
outreach.personalize | boolean | no | true | LLM personalization (each draft cites specifics from the target page) |
outreach.approval.required | boolean | no | workspace policy | If true, drafts park at WAITING_APPROVAL |
tags | string[] | 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.
GET /v1/link-building/serp-outreach/{id}/drafts
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"
}'| Field | Notes |
|---|---|
draftIds | null or "all" to approve every draft in AWAITING_APPROVAL |
scheduleAt | ISO timestamp; omit to send immediately |
editsPerDraft | Map 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.
PATCH /v1/link-building/serp-outreach/{id}
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.
DELETE /v1/link-building/serp-outreach/{id}
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
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing seed.keyword or outreach.ourContentUrl |
| 402 | insufficient_credits | — |
| 403 | outreach_platform_not_configured | No Instantly (or equivalent) connection in this workspace |
| 422 | no_drafts_produced | All SERP results were filtered out (try loosening filters) |
| 503 | external_unavailable | DataForSEO, Ahrefs, or Apollo unreachable |
Cost
| Action | Credits |
|---|---|
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\" }
}"B. The link_hunter skill runs it on a schedule
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.Related
- Concept: Link Building
- Concept: Approval Workflows
- API: Link Building · competitor link outreach (sibling — backlinks of competitors as the seed)
- API: Link Building · campaign management
- API: Link Building · CRM
- API: Research · serp
- API: Agent · invoke
- Playbook: Turn SERP into outreach campaigns
IndexNow
REST API for submitting URLs via the IndexNow protocol to Bing, Yandex, Naver, and other partner search engines. Faster than waiting for crawlers, with key.txt publication handled automatically.
Competitor outreach
Seed a campaign with a competitor domain. The agent pulls their backlinks via Ahrefs, filters for quality and relevance, discovers contacts, and drafts personalized outreach pitching your content as a parallel option.