Outly AI REST API
Add leads from your website or CRM, put them into campaigns, launch and pause campaigns, and read results, from your own code. Available on every plan, Free included. JSON in, JSON out, field names in snake_case. To get events pushed to you as they happen, use webhooks.
Authentication
Create a key in Settings → API keys (workspace owners and admins). A key belongs to one workspace and is either read-only or read/write. It is shown once: store it in your server's environment variables and never put it in browser code or a public repository. If it leaks, revoke it on the same page; it stops working immediately.
Send it on every request as a Bearer token:
curl https://outlyai.originutech.in/api/v1/workspace \
-H "Authorization: Bearer $OUTLY_API_KEY"Base URL: https://outlyai.originutech.in/api/v1
Rate limits and plan limits
Each key can make 120 requests a minute. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with a Retry-After header in seconds.
The API follows your plan exactly as the app does: creating leads past the plan's lead limit, or launching a campaign past the active-campaign limit, is refused with 402 plan_limit. Bulk creates add what fits and report the rest.
Workspace
GET/workspaceread
The workspace the key belongs to, its plan, how many leads it holds and how many more fit.
Leads
GET/leadsread
Newest first. Query: search (email or name), status (new, contacted, replied, qualified, unqualified), tag, page, page_size (up to 100, default 25). Returns { data, page, page_size, total }.
POST/leadswrite
Create one lead: email (required), first_name, last_name, title, company, linkedin_url, tags. Returns 201 with the lead, or 409 conflict if the email is already a lead (the message includes its id) or the email or its domain is on the workspace's do-not-contact list.
Or create up to 500 at once with { "leads": [ ... ] }. Existing emails are skipped, never overwritten. Returns created, duplicates, over_limit, limit_message and suppressed (left out because they're on the do-not-contact list).
curl -X POST https://outlyai.originutech.in/api/v1/leads \
-H "Authorization: Bearer $OUTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "priya@example.com",
"first_name": "Priya",
"last_name": "Sharma",
"title": "Head of Growth",
"company": "Example Labs",
"tags": ["website-signup"]
}'{
"id": "2f6c1a0e-8b1d-4f7a-9c3e-5d2b1a0f9e8d",
"email": "priya@example.com",
"first_name": "Priya",
"last_name": "Sharma",
"title": "Head of Growth",
"company": { "id": "…", "name": "Example Labs" },
"linkedin_url": null,
"status": "new",
"verification_status": "unverified",
"tags": ["website-signup"],
"source": "api",
"created_at": "2026-10-21T09:30:00.000Z"
}GET/leads/{id}read
One lead.
PATCH/leads/{id}write
Change any of first_name, last_name, title, company, linkedin_url, tags, status. Send null to clear a text field. The email can't be changed; create a new lead instead.
DELETE/leads/{id}write
Deletes the lead and removes it from its campaigns. Returns 204.
Campaigns
Campaigns are written in the app, where you build the sequence and pick the sending inbox. The API lists them, feeds them leads and controls whether they send.
GET/campaignsread
Every campaign with status (draft, active, paused, completed) and stats: leads, sent, replies, positive_replies, meetings, reply_rate, meeting_rate.
GET/campaigns/{id}read
One campaign with its stats and sending schedule.
POST/campaigns/{id}/leadswrite
{ "lead_ids": [ ... ] }, up to 500. Leads already in the campaign are skipped, and ids that aren't leads in this workspace come back in not_found. If the campaign is active, the new leads start on its next sending run.
POST/campaigns/{id}/launchwrite
Launches a draft campaign with the same checks as the Launch button: a connected inbox, a complete sequence, at least one lead and room in the plan. If some leads have invalid or unverified emails, the first call returns 409 confirmation_required with the counts; call again with { "exclude_invalid": true } to launch without the invalid ones. A successful launch also returns domain_warnings: plain-English notes when the sending inbox's domain has no (or a broken) SPF or DMARC record. They never block the launch.
POST/campaigns/{id}/pausewrite
Stops an active campaign sending.
POST/campaigns/{id}/resumewrite
Starts a paused campaign again, subject to the plan's active-campaign limit.
Examples
Node.js (18+), adding a website signup to a campaign:
const BASE = "https://outlyai.originutech.in/api/v1";
const headers = {
Authorization: `Bearer ${process.env.OUTLY_API_KEY}`,
"Content-Type": "application/json",
};
// Create a lead, then add it to a campaign.
const lead = await fetch(`${BASE}/leads`, {
method: "POST",
headers,
body: JSON.stringify({ email: "priya@example.com", first_name: "Priya" }),
}).then((r) => r.json());
await fetch(`${BASE}/campaigns/${process.env.OUTLY_CAMPAIGN_ID}/leads`, {
method: "POST",
headers,
body: JSON.stringify({ lead_ids: [lead.id] }),
});Python, reading every lead that replied, page by page:
import os, requests
BASE = "https://outlyai.originutech.in/api/v1"
headers = {"Authorization": f"Bearer {os.environ['OUTLY_API_KEY']}"}
page = 1
while True:
r = requests.get(f"{BASE}/leads", headers=headers, params={"status": "replied", "page": page, "page_size": 100})
r.raise_for_status()
body = r.json()
for lead in body["data"]:
print(lead["email"], lead["status"])
if page * body["page_size"] >= body["total"]:
break
page += 1Errors
Every error has the same shape, so you can branch on code:
{
"error": {
"code": "plan_limit",
"message": "This workspace is on the Free plan. ..."
}
}400 invalid_request: the body or a query parameter is wrong; the message names the field.401 unauthorized: missing, invalid or revoked key.402 plan_limit: the plan has no room for this.403 forbidden: a read-only key tried to change something.404 not_found: no such lead or campaign in this workspace.409 conflict/confirmation_required: see the message.422 invalid_request: the campaign can't launch yet; the message says why.429 rate_limited: wait forRetry-Afterseconds.500 server_error: our fault. Retry, and tell us if it keeps happening.
Help
Questions, or an endpoint you need that isn't here? Email support@originutech.in and tell us what you're building.