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 | /me | Your account, and whether LinkedIn is connected. |
| GET | /campaigns | Your campaigns, with lead counts by status. |
| GET | /campaigns/{id} | One campaign. |
| POST | /campaigns/{id}/leads | Add up to 1,000 leads. Anyone already in one of your campaigns is skipped. |
| POST | /campaigns/{id}/start | Start the campaign. LinkedIn must be connected. |
| POST | /campaigns/{id}/pause | Pause the campaign. |
| GET | /leads | Leads, 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}/stop | Stop a lead’s sequence, for example when they booked a call elsewhere. |
| GET | /webhooks | Your webhook endpoints, and the events you can subscribe to. |
| POST | /webhooks | Add 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.invitedA connection request was sent. Includes the note, if there was one.lead.acceptedA lead accepted your connection request.lead.message_sentA first message or follow-up was sent. Includes the text.message.receivedA lead sent you a message. Sent for every message, including after their first reply.lead.repliedA lead replied for the first time. Their sequence has stopped.lead.finishedA sequence ended with no reply.lead.not_acceptedA 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.

