Outreach API Reference

The Outreach API exposes the same engine that powers the in-app Cold Outreach surface — create leads, list campaigns, fetch replies, and track delivery, from any HTTP client. All endpoints are versioned under /api/v1/outreach/* and authenticated via a Bearer token on the Authorization header.

Quick start — generate an API key in Outreach → Integrations → API keys (Pro plan or higher). Then:
curl https://www.planmysaas.com/api/v1/outreach/me \
  -H "Authorization: Bearer pms_live_<your-key>"

Returns the workspace + the scopes attached to the key. If you see 401 Unauthorized, the key is invalid or revoked. If you see 403 Forbidden with a scope error, the key is valid but missing the scope the endpoint needs.

Authentication

Every /api/v1/outreach/* request must carry anAuthorization: Bearer <key> header. Keys are workspace-scoped — all data returned belongs to the workspace that owns the key.

  • Where to get a key — the API keys panel inside Outreach → Integrations. Plain-text key is shown once on creation and never again.
  • Storage — server stores a SHA-256 hash; the key itself never appears in any list or get endpoint.
  • Rotation — revoke the old key and generate a new one. We keep the revoked-key row so you can audit any later request that arrives with the leaked secret (it gets a clear “Key revoked” error instead of a generic 401).

Scopes

Each key carries a list of scopes. Endpoints assert the relevant scope; missing scope → 403. Pick the smallest set the integration needs — least-privilege limits blast radius if a key leaks.

ScopeGrants
leads:read List + get leads, fetch sends/replies attached to a lead
leads:write Create + update leads, queue them into a campaign
campaigns:read List campaigns and their step counts + status
campaigns:write Create + edit campaigns and sequence steps (reserved — not yet wired)
replies:read List inbound replies + their AI classification
sends:read List outbound sends + delivery state (reserved)
sends:trigger Force-send the next step on a lead (reserved — not yet wired)

Endpoints

GET /v1/outreach/me

No scope required. Returns the workspace owning this key + the scopes attached. Use this as a smoke-test for new integrations.

curl https://www.planmysaas.com/api/v1/outreach/me \
  -H "Authorization: Bearer pms_live_..."
{
  "workspace": { "id": "ws_abc", "name": "Acme Inc", "createdAt": "2025-09-01T..." },
  "key":       { "prefix": "pms_live_", "scopes": ["leads:read","leads:write","campaigns:read"] }
}

GET /v1/outreach/campaigns

Scope: campaigns:read. Lists campaigns the workspace owns. Returns the idyou’ll pass toPOST /v1/outreach/leads as campaignId.

curl https://www.planmysaas.com/api/v1/outreach/campaigns \
  -H "Authorization: Bearer pms_live_..."
{
  "data": [
    {
      "id": "camp_abc",
      "name": "Vibe coders Q2",
      "status": "active",
      "fromName": "Anita",
      "pitchContext": "We help solo SaaS founders ship in 7 days...",
      "dailyCap": 80,
      "_count": { "leads": 124, "sends": 312, "steps": 4, "replies": 27 },
      "createdAt": "2026-04-01T...",
      "updatedAt": "2026-04-26T..."
    }
  ]
}

GET /v1/outreach/leads

Scope: leads:read. Lists leads scoped to the workspace. Filter by campaignId, status,email, or paginate with cursor.

curl "https://www.planmysaas.com/api/v1/outreach/leads?campaignId=camp_abc&status=replied&limit=50" \
  -H "Authorization: Bearer pms_live_..."

POST /v1/outreach/leads

Scope: leads:write. Creates a lead inside an existing campaign. The lead enters the sequence at step 1 and starts firing on the next scheduler tick (within ~10 minutes).

curl -X POST https://www.planmysaas.com/api/v1/outreach/leads \
  -H "Authorization: Bearer pms_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaignId": "camp_abc",
    "email":      "founder@example.com",
    "firstName":  "Sam",
    "lastName":   "Altman",
    "company":    "Example Inc",
    "role":       "Founder",
    "linkedinUrl":"https://linkedin.com/in/sam"
  }'

Idempotency: the unique constraint on (campaignId, email)means re-posting the same lead returns the existing row, not a duplicate. Safe to retry on transient network errors.

GET /v1/outreach/leads/[id]

Scope: leads:read. Returns one lead with its sends, replies, and step position.

GET /v1/outreach/replies

Scope: replies:read. Lists inbound replies with the AI classification (interested, meeting_request,objection, not_interested,unsubscribe_request, out_of_office,auto_reply, wrong_person,referral, question, other).

curl "https://www.planmysaas.com/api/v1/outreach/replies?campaignId=camp_abc&needsAction=true" \
  -H "Authorization: Bearer pms_live_..."

Webhooks

For event-driven integrations, configure webhook URLs in Outreach → Integrations → Webhooks. Each event POSTs a JSON envelope with an HMAC-SHA256 signature so your endpoint can verify it came from us.

Events

EventFires when
lead.created A lead is created (UI, API, or auto-discovery)
lead.replied An inbound reply is matched to this lead
lead.bounced SMTP returns a hard-bounce on a send to this lead
lead.unsubscribedLead unsubscribes (footer link or AI-classified opt-out)
lead.completed Lead reaches the end of the sequence
send.sent An outbound email left the SMTP server cleanly
send.failed SMTP returned an error after retries
reply.received An inbound reply landed (matched or unmatched)
reply.classified AI classifier finished tagging an inbound reply
channel_touch.queuedA LinkedIn / Twitter touch was generated and queued for manual paste
channel_touch.sentThe founder marked a queued touch as sent

Signature verification

Every delivery includes these headers:

  • X-PMS-Signaturesha256=<hex> of the raw body, HMAC-keyed with the webhook secret you saved on creation
  • X-PMS-Event — the event name (e.g. reply.received)
  • X-PMS-Event-Id — unique id for this event delivery, used for idempotency
  • X-PMS-Attempt — 1 on first delivery, 2/3/4 on retries
// Node.js — verify the signature
import { createHmac } from "node:crypto"

export function verifyPmsWebhook(rawBody: string, headerSig: string, secret: string) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")
  // constant-time compare in production
  return expected === headerSig
}

Retries

Failed deliveries (network error or HTTP 5xx) retry on a backoff of 5 minutes → 30 minutes → 2 hours, then mark permanently failed. X-PMS-Event-Id stays constant across all retries so your endpoint can dedupe.

Rate limits

Per-workspace sliding-window caps protect against burst abuse and sustained leak. When you hit one, the response is 429 Too Many Requestswith a Retry-After header in seconds.

ActionPer minutePer hourPer day
Discovery (ICP / campaign)31550
Lead enrichment 302001,000
Reply draft 1060200
Channel-touch regenerate 1060200
Autopilot manual run 2 1050

Headers on a 429 response: Retry-After,X-RateLimit-Limit, X-RateLimit-Window(minute / hour / day),X-RateLimit-Remaining.

Error codes

StatusMeaningAction
400Bad request — malformed JSON or missing required fieldFix the request body and retry
401Unauthorized — missing / invalid / revoked Bearer tokenGenerate a fresh API key
402Plan upgrade required — feature not available on current planUpgrade or use a different feature
403Forbidden — key valid but missing the required scopeRecreate the key with the scope you need
404Not found — resource doesn’t exist or belongs to another workspaceVerify the id; cross-workspace access is rejected here
422Unprocessable — request was valid JSON but failed business rulesRead the error field for the specific reason
429Rate limited — see Rate limits sectionWait Retry-After seconds and retry
5xxServer error — transient platform issueRetry with exponential backoff (start at 1s, max 60s)

Versioning

The current API is v1. We follow additive semantics — new fields and endpoints can land any time without bumping the version. Breaking changes (rename / remove a field, change response shape) get a v2 path with a 12-month overlap on v1. Subscribe to the changelog by configuring a webhook on system.api_change (reserved — not yet emitting).

Support

Stuck? File an issue or message us via the in-app help. Include your key prefix (NOT the full key), the request id from the response header X-Request-Id, and a curl command that reproduces. We respond within 1 business day on Pro+.