API reference
One workspace's leads, lists, campaigns and pipelines, reachable from outside the
dashboard — the same data under the same isolation.
Every request carries a key as a bearer token:
Authorization: Bearer lum_…. Keys are created per user in the dashboard under
Settings, shown once at creation, and stored only as a hash — so a key cannot be
recovered, only replaced. A key reaches exactly one workspace and cannot create or revoke
keys, which is what makes a leaked one containable by revoking it.
Open the dashboard
Authentication
Every request carries a key as a bearer token. Keys are created per user in Settings, shown once at creation, and stored only as a SHA-256 — so a key cannot be recovered, only replaced. Revoking takes effect immediately. A key reaches exactly one workspace: the one it was created in.
Check a key
GET
/api/whoami
Answers with the workspace the key belongs to and how the request authenticated. Touches no data, so it is the safe way to confirm a key works before wiring anything to it.
Request
curl https://luminnaleads.com.ng/api/whoami \
-H "Authorization: Bearer lum_..."
Response
{
"tenantId": "0e2c…",
"authenticatedVia": "api_key"
}
Leads
People, with the company they work at attached.
List leads
GET
/api/leads
Newest first. Without filters this returns the most recent 200; the ceiling is 1000 per call.
Query parameters
listId
string
Only leads on this list.
status
string
Email status: valid, risky, invalid or unknown.
q
string
Matches name, email, job title, company name or company domain.
limit
number
Default 200, maximum 1000.
Request
curl "https://luminnaleads.com.ng/api/leads?status=valid&limit=50" \
-H "Authorization: Bearer lum_..."
Response
[
{
"id": "…",
"fullName": "Ada Okafor",
"email": "ada@acme.com",
"title": "Head of Operations",
"emailStatus": "valid",
"companyName": "Acme Ltd",
"companyDomain": "acme.com",
"listId": "…",
"createdAt": "2026-09-21T09:12:00.000Z"
}
]
Add a lead
POST
/api/leads
Creates one lead and files it in the warehouse at the same time, under the company its address belongs to. An address that already exists answers 409 rather than merging quietly.
Body
emailrequired
string
The address. Must be valid.
firstName
string
Up to 120 characters.
lastName
string
Up to 120 characters.
title
string
Job title, up to 200 characters.
companyName
string
Used when the address gives no usable domain.
country
string
Separate from location, which is a city — the warehouse filters on country.
linkedinUrl
string
The person’s own profile.
listId
string
Adds them to this list as well.
Request
curl -X POST https://luminnaleads.com.ng/api/leads \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"email":"ada@acme.com","firstName":"Ada","title":"Head of Operations","companyName":"Acme Ltd","country":"NG"}'
Response
{ "id": "…", "email": "ada@acme.com", "fullName": "Ada" }
Lists
Named sets of leads. A campaign sends to lists, so this is how a batch becomes sendable.
List lists
GET
/api/lead-lists
Newest first, each with how many leads it holds.
Request
curl https://luminnaleads.com.ng/api/lead-lists \
-H "Authorization: Bearer lum_..."
Response
[
{ "id": "…", "name": "Apify Master List", "notes": null, "count": 6129 }
]
Create a list
POST
/api/lead-lists
Returns 201 with the new list.
Body
namerequired
string
What the list is called.
Request
curl -X POST https://luminnaleads.com.ng/api/lead-lists \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"Nigeria — agencies"}'
Response
{ "id": "…", "name": "Nigeria — agencies" }
Warehouse search
The searchable store behind the Leads page: companies and the people at them, filterable on every attribute the dashboard offers.
Search the warehouse
GET
/api/warehouse/search
Returns a page of rows and, when asked, counts for every filter dimension. Paginate with the cursor rather than an offset: a cursor costs the same on page 500 as on page 1.
Query parameters
mode
string
contact for one row per person, business for one per company. Default business.
q
string
Full-text across company and contact.
country
string
Repeatable. ISO-3166 code.
industry
string
Repeatable, normalised industry.
emailStatus
string
Repeatable: valid, risky, invalid, unknown.
seniority
string
Repeatable: owner, director, manager, vp, c_suite, partner…
jobTitle
string
Case-insensitive “contains” on the person’s title.
limit
number
Default 50, maximum 200.
cursor
string
From nextCursor on the previous page.
facets
boolean
facets=0 skips the counts, which is most of the query’s cost.
count
boolean
count=0 skips the total. Rows arrive sooner without it.
Request
curl "https://luminnaleads.com.ng/api/warehouse/search?mode=contact&country=NG&emailStatus=valid&limit=50&facets=0" \
-H "Authorization: Bearer lum_..."
Response
{
"rows": [ { "id": "…", "fullName": "Ada Okafor", "title": "Director", "email": "ada@acme.com", "companyName": "Acme Ltd" } ],
"total": 23176,
"nextCursor": "eyJ…",
"facets": { "country": [ { "value": "NG", "count": 412 } ] }
}
Imports
Upload a file, confirm how its columns map, then start the import. Two steps on purpose: a mapping confirmed before 100,000 rows land beats one guessed at afterwards.
Upload a file
POST
/api/warehouse/imports/upload
Multipart. Reads the header and a sample, and answers with a batch id and a suggested column mapping. Nothing is written to the warehouse yet.
Request
curl -X POST https://luminnaleads.com.ng/api/warehouse/imports/upload \
-H "Authorization: Bearer lum_..." \
-F "file=@leads.csv"
Response
{ "batchId": "…", "headers": ["Name","Email"], "mapping": { "Email": "contact_email" } }
Start the import
POST
/api/warehouse/imports/{batchId}/start
Runs the batch with the mapping you confirm. At least one of company_name, domain or website_url must be mapped — rows need something to identify the business by. Duplicates are merged, not doubled: a company matches on domain, a person on their address.
Path parameters
batchIdrequired
string
From the upload step.
Body
mappingrequired
object
Column name to field name.
listId
string
Add everyone this file brings in to an existing list.
listName
string
Or to a new list of this name. Not both.
Request
curl -X POST https://luminnaleads.com.ng/api/warehouse/imports/BATCH_ID/start \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"mapping":{"Email":"contact_email","Company":"company_name"},"listName":"March import"}'
Response
{ "batchId": "…", "jobId": "…" }
Campaigns
Sequences and how they are performing.
List campaigns
GET
/api/campaigns
Every campaign in the workspace with its steps, most recently updated first.
Request
curl https://luminnaleads.com.ng/api/campaigns \
-H "Authorization: Bearer lum_..."
Response
[
{ "id": "…", "name": "Q4 agencies", "status": "active", "dailyCap": 80, "steps": [ { "order": 0, "type": "email", "subject": "…" } ] }
]
Get a campaign
GET
/api/campaigns/{id}
One campaign with its steps.
Path parameters
idrequired
string
Campaign id.
Request
curl https://luminnaleads.com.ng/api/campaigns/CAMPAIGN_ID \
-H "Authorization: Bearer lum_..."
Campaign stats
GET
/api/campaigns/{id}/stats
Sends, opens, clicks and replies for one campaign.
Path parameters
idrequired
string
Campaign id.
Request
curl https://luminnaleads.com.ng/api/campaigns/CAMPAIGN_ID/stats \
-H "Authorization: Bearer lum_..."
Deliverability read
GET
/api/campaigns/{id}/analysis
The same analysis the dashboard shows: what is helping or hurting this campaign’s delivery.
Path parameters
idrequired
string
Campaign id.
Request
curl https://luminnaleads.com.ng/api/campaigns/CAMPAIGN_ID/analysis \
-H "Authorization: Bearer lum_..."
Create a campaign
POST
/api/campaigns
Creates it as a draft. It does not send — see the note on launching below.
Body
namerequired
string
Up to 200 characters.
leadListIds
string[]
Lists to enrol from.
inboxIds
string[]
Mailboxes to send from. The daily cap is split across them.
steps
object[]
Up to 50. Each has type, subject, body and a delay.
dailyCap
number
1–2000 across the whole campaign.
sendWindowStart
string
"09:00".
sendWindowEnd
string
"17:00".
sendWindowTimezone
string
IANA zone. An unknown one is refused rather than silently becoming UTC.
sendDays
string[]
mon…sun.
stopOnReply
boolean
Stop mailing someone once they answer.
Request
curl -X POST https://luminnaleads.com.ng/api/campaigns \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"Q4 agencies","dailyCap":80,"sendWindowStart":"09:00","sendWindowEnd":"17:00","sendWindowTimezone":"Africa/Lagos"}'
Delete a campaign
DELETE
/api/campaigns/{id}
Removes the campaign and everything recorded against it — its steps, its enrolled recipients and their send history. There is no undo and no archive: if you only want it to stop, pause it instead.
Path parameters
idrequired
string
Campaign id.
Request
curl -X DELETE https://luminnaleads.com.ng/api/campaigns/CAMPAIGN_ID \
-H "Authorization: Bearer lum_..."
Response
{ "ok": true }
Launch a campaign
POST
/api/campaigns/{id}/launch
Starts the campaign sending.
Not available with an API key. A key lives in a script, a CI config and a shell history, and one leaking should not include the ability to start mailing a workspace’s entire list from its own domain. Launch from the dashboard instead.
Request
# Sign in to the dashboard and press Launch.
Sequence templates
The saved email templates campaign steps are built from.
List templates
GET
/api/email-templates
Newest first.
Request
curl https://luminnaleads.com.ng/api/email-templates \
-H "Authorization: Bearer lum_..."
Response
[ { "id": "…", "name": "Agency intro", "subject": "quick question, {{first_name}}", "body": "…" } ]
Create a template
POST
/api/email-templates
Bodies take the same variables the editor offers, so a template written here renders identically to one written in the dashboard: {{first_name}}, {{company}}, {{title}}, {{first_line}}. A variable with no value renders empty, so give it a fallback — {{title|there}}.
Body
namerequired
string
Up to 200 characters.
subjectrequired
string
Up to 500 characters.
bodyrequired
string
Up to 20,000 characters.
Request
curl -X POST https://luminnaleads.com.ng/api/email-templates \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"Agency intro","subject":"quick question, {{first_name}}","body":"Hi {{first_name}} — saw {{company}} …"}'
Delete a template
DELETE
/api/email-templates/{id}
Removes the template. Campaign steps already written from it keep their own copy of the wording — a step holds text, not a reference — so deleting one changes nothing that is already scheduled or sent.
Path parameters
idrequired
string
Template id.
Request
curl -X DELETE https://luminnaleads.com.ng/api/email-templates/TEMPLATE_ID \
-H "Authorization: Bearer lum_..."
Response
{ "ok": true }
Pipelines
The CRM boards. Cards move themselves — a lead that replies to a campaign is moved by the product — so reading a pipeline tells you where a contact currently sits.
List pipelines
GET
/api/pipelines
Each pipeline with its stages and the cards on them.
Request
curl https://luminnaleads.com.ng/api/pipelines \
-H "Authorization: Bearer lum_..."
Response
[ { "id": "…", "name": "Cold outreach", "stages": [ { "name": "Enrolled", "cards": [] } ] } ]
Pipeline templates
GET
/api/pipelines/templates
The starting layouts a new pipeline can be created from.
Request
curl https://luminnaleads.com.ng/api/pipelines/templates \
-H "Authorization: Bearer lum_..."
Create a pipeline
POST
/api/pipelines
From a template, and optionally bound to a campaign so its cards follow that campaign’s leads.
Body
namerequired
string
Up to 120 characters.
template
string
A key from the templates endpoint.
campaignId
string
Bind the board to this campaign.
Request
curl -X POST https://luminnaleads.com.ng/api/pipelines \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"Cold outreach","template":"cold-outreach"}'
Delete a pipeline
DELETE
/api/pipelines/{id}
Removes the board, its stages and every card on it. The leads themselves are untouched — a card is a position, not the person — so deleting a board loses where everyone stood, not who they are.
Path parameters
idrequired
string
Pipeline id.
Request
curl -X DELETE https://luminnaleads.com.ng/api/pipelines/PIPELINE_ID \
-H "Authorization: Bearer lum_..."
Response
{ "ok": true }
ICPs & offers
The definitions the AI copy and the lead filters read. Changing one changes what later campaigns are written against.
List ICPs
GET
/api/icps
Every saved ideal-customer profile.
Request
curl https://luminnaleads.com.ng/api/icps \
-H "Authorization: Bearer lum_..."
Create an ICP
POST
/api/icps
All fields optional except the name.
Body
name
string
What this profile is called.
industry
string
Free text.
companySize
string
For example "11-50".
geography
string
Where they are.
jobTitles
string
Who you are writing to.
painPoints
string[]
What they are stuck on.
goals
string[]
What they want.
Request
curl -X POST https://luminnaleads.com.ng/api/icps \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"NG agencies","industry":"Marketing","companySize":"11-50","geography":"Nigeria"}'
Update an ICP
PATCH
/api/icps/{id}
Same fields as creation. 404 when the id is not in this workspace.
Path parameters
idrequired
string
ICP id.
Request
curl -X PATCH https://luminnaleads.com.ng/api/icps/ICP_ID \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"geography":"Nigeria and Ghana"}'
Delete an ICP
DELETE
/api/icps/{id}
Removes the profile. Offers pointing at it are kept and unlinked rather than deleted with it — the response says how many — because an offer is a thing you sell and survives the audience definition it was written for.
Path parameters
idrequired
string
ICP id.
Request
curl -X DELETE https://luminnaleads.com.ng/api/icps/ICP_ID \
-H "Authorization: Bearer lum_..."
Response
{ "ok": true, "unlinkedOffers": 2 }
List offers
GET
/api/offers
Every saved offer.
Request
curl https://luminnaleads.com.ng/api/offers \
-H "Authorization: Bearer lum_..."
Create an offer
POST
/api/offers
What you are selling, to which ICP, and how the email asks for the meeting.
Body
name
string
What this offer is called.
icpId
string
The profile it is aimed at.
whatItIs
string
One line.
valueProp
string
Why it matters to them.
socialProof
string
Evidence it works.
Request
curl -X POST https://luminnaleads.com.ng/api/offers \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"name":"Warmup audit","whatItIs":"A 20-minute deliverability review","ctaText":"Worth a look?"}'
Update an offer
PATCH
/api/offers/{id}
Same fields as creation. 404 when the id is not in this workspace.
Path parameters
idrequired
string
Offer id.
Request
curl -X PATCH https://luminnaleads.com.ng/api/offers/OFFER_ID \
-H "Authorization: Bearer lum_..." \
-H "Content-Type: application/json" \
-d '{"price":"₦250,000"}'
Delete an offer
DELETE
/api/offers/{id}
Removes the offer. The ICP it was aimed at is left alone, and campaigns already written against it keep their copy.
Path parameters
idrequired
string
Offer id.
Request
curl -X DELETE https://luminnaleads.com.ng/api/offers/OFFER_ID \
-H "Authorization: Bearer lum_..."
Response
{ "ok": true }