Updraft API

Add leads from your own tools, read where each one is, and get a webhook the moment someone accepts or replies. Included in every plan.

Getting started

Create a key in Settings → API and webhooks. Send it with every request:

Authorization: Bearer upd_your_key

The base URL is https://app.elevate-outreach.com/api/v1. Requests and answers are JSON. Keep keys on your server, never in a web page.

A key works while your plan is active. Each key can make 120 requests a minute; past that you get 429 with a Retry-After header. Errors look like { "error": { "code": "not_found", "message": "Lead not found" } }.

Endpoints

GET/meYour account, and whether LinkedIn is connected.
GET/campaignsYour campaigns, with lead counts by status.
GET/campaigns/{id}One campaign.
POST/campaigns/{id}/leadsAdd up to 1,000 leads. Anyone already in one of your campaigns is skipped.
POST/campaigns/{id}/startStart the campaign. LinkedIn must be connected.
POST/campaigns/{id}/pausePause the campaign.
GET/leadsLeads, oldest change first. Filter with campaign_id, status and updated_since; page with after.
GET/leads/{id}One lead with every message sent and received.
POST/leads/{id}/stopStop a lead’s sequence, for example when they booked a call elsewhere.
GET/webhooksYour webhook endpoints, and the events you can subscribe to.
POST/webhooksAdd an endpoint: { "url": "https://…", "events": ["lead.replied"] }. The answer includes its signing secret, once.
DELETE/webhooks/{id}Remove an endpoint.

Adding leads

Each lead needs a linkedin_url (a linkedin.com/in/ profile). Optional fields are first_name, last_name, full_name, company, title, country and timezone. Anything else, at the top level or inside custom, is kept and can be used in templates and AI prompts as {{pain_point}}.

curl -X POST https://app.elevate-outreach.com/api/v1/campaigns/CAMPAIGN_ID/leads \
  -H "Authorization: Bearer upd_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "leads": [
      {
        "linkedin_url": "https://www.linkedin.com/in/jane-doe",
        "first_name": "Jane",
        "company": "Acme",
        "title": "Head of Sales",
        "custom": { "pain_point": "slow onboarding" }
      }
    ]
  }'

The answer:

{
  "added": 1,
  "already_in_a_campaign": 0,
  "duplicates_in_request": 0,
  "invalid": [],
  "invalid_count": 0,
  "lead_ids": ["5a403088-203a-42e7-b463-72c14036d899"]
}

Leads are sent once the campaign is running, within its sending hours and your daily limits.

Reading leads

GET /leads returns up to 200 leads per call (limit), ordered by when they last changed. If there are more, the answer has a next_cursor: pass it back as after. To poll for changes, pass the time of your last sync as updated_since.

A lead's status is one of queued, invited, connected, replied, finished, not_accepted, stopped or failed. current_step is the next step of the sequence: 0 is the connection request.

Webhooks

Add an endpoint in Settings or with POST /webhooks. It must be a public https:// URL. These are the events:

  • lead.invited A connection request was sent. Includes the note, if there was one.
  • lead.accepted A lead accepted your connection request.
  • lead.message_sent A first message or follow-up was sent. Includes the text.
  • message.received A lead sent you a message. Sent for every message, including after their first reply.
  • lead.replied A lead replied for the first time. Their sequence has stopped.
  • lead.finished A sequence ended with no reply.
  • lead.not_accepted A lead did not accept within the campaign’s give-up period.

Every delivery is a POST like this, with the lead, its campaign and, where there is one, the message:

{
  "id": "evt_3f1c9a0b7e2d4c55a1b2c3d4e5f60718",
  "type": "lead.replied",
  "created_at": "2026-11-10T09:15:02.114Z",
  "data": {
    "lead": {
      "id": "5a403088-203a-42e7-b463-72c14036d899",
      "campaign_id": "0b6e2a9c-1f43-4d0e-9a57-3c2b1e8f7d10",
      "linkedin_url": "https://www.linkedin.com/in/jane-doe",
      "first_name": "Jane", "last_name": "Doe",
      "company": "Acme", "title": "Head of Sales", "country": "US",
      "status": "replied", "current_step": 2, "outcome": null,
      "custom": { "pain_point": "slow onboarding" },
      "invited_at": "…", "accepted_at": "…", "replied_at": "…",
      "last_message_at": "…", "created_at": "…", "updated_at": "…"
    },
    "campaign": { "id": "0b6e2a9c-…", "name": "SaaS founders" },
    "message": { "direction": "in", "body": "Sounds interesting, how does it work?", "at": "…" }
  }
}

Answer with any 2xx within 10 seconds. Anything else is retried after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours. Every delivery has an id, Updraft-Delivery, and the event id in the body. A retry repeats both, so ignore ones you have already handled.

Checking signatures

Each delivery has an Updraft-Signature header: t=<unix seconds>,v1=<signature>. The signature is the HMAC-SHA256 of t, a dot and the raw body, keyed with your endpoint's signing secret. Check it, and that t is recent, before trusting a delivery.

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (a string or Buffer, not parsed JSON)
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1 || '');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && given.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(expected), given);
}

// verify(rawBody, req.headers['updraft-signature'], 'whsec_…')

Zapier, Make and n8n

Use their "catch hook" or "webhook" trigger and paste its URL as a webhook endpoint. To add leads from them, use an HTTP request step to POST /campaigns/{id}/leads with your key in the Authorization header.

Questions? Book a call and we'll help you set it up.