Developer docs

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 += 1

Errors

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 for Retry-After seconds.
  • 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.