Developers

Lumenbase API Reference

REST endpoints for contacts, companies, deals, tasks, invoices and activities, plus a submit-only endpoint for website forms. JSON in, JSON out, authenticated with a workspace API key.

Quick start

  1. Step 1

    Create a key

    Grant only the scopes the integration uses. The key is shown once.

    SettingsCompany SettingsAPI
  2. Step 2

    Send a request

    Put the key in the Authorization header of every call.

    Bearer lmb_…
  3. Step 3

    Read the envelope

    Check success, read data, and log meta.request_id for support.

    meta.request_id

Base URL

https://api.lumenbase.io/functions/v1
Every request sends
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

Optional headers

  • X-Idempotency-Key Makes a retried ingest safe. See Idempotency.
  • X-Request-Id Your own trace id, echoed back in meta.request_id.

Every response

Success
{
  "success": true,
  "data": { ... },
  "meta": {
    "request_id": "req_...",
    "api_version": "1",
    "timestamp": "..."
  }
}
Error
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "...",
    "details": { "field": "..." }
  },
  "meta": { ... }
}

Authentication

Every request carries a workspace API key as a Bearer token. There is no other accepted form.

Authorization: Bearer lmb_0f2a...

What each request passes through

  1. API key

    Missing, malformed or revoked

    401
  2. Scope

    Key lacks the scope for this call

    403
  3. Rate limit

    The bucket is full

    429
  4. Validation

    Bad field, unknown id or no such record

    400404409
  5. Done

    Created, accepted, read, updated or deleted

    200201202
  • A key is lmb_ followed by 64 hexadecimal characters.
  • Keys are stored hashed. We cannot show one again, so save it when you create it.
  • Revoking takes effect on the next request. Create the replacement first so rotation has no gap.
  • A key belongs to one workspace. It can only read records in that workspace, and only link to them.
  • Records a key creates are credited to the person who created the key, or to a workspace admin once they have left.

Scopes

Each key carries one or more scopes. Grant only what the integration uses.

ScopeDescription
contacts:readRead and list contacts
contacts:writeCreate, update, and delete contacts
companies:readRead and list companies
companies:writeCreate, update, and delete companies
deals:readRead and list deals
deals:writeCreate, update, and delete deals
tasks:readRead and list tasks
tasks:writeCreate, update, and delete tasks
invoices:readRead and list invoices
invoices:writeCreate, update, and delete invoices
activities:readRead and list activities
activities:writeCreate and delete activities
inbound:submitSend form submissions to an Inbound form, and nothing else. The key cannot read or change any record.
readRead access to all entity types
adminFull access to all endpoints

contact_ingest and activity_ingest are older names that still work. Each grants read and write on contacts or activities.

Pagination

Every list endpoint takes these query parameters.

FieldTypeDescription
pageintegerPage number (default: 1)
per_pageintegerItems per page, max 100 (default: 25)
sort_bystringField to sort by (varies per entity)
sort_orderstring"asc" or "desc" (default: "desc")
searchstringFree-text search across key fields
Paginated response
{
  "success": true,
  "data": {
    "items": [ ... ],
    "pagination": { "page": 1, "per_page": 25, "total": 142, "total_pages": 6 }
  },
  "meta": { ... }
}

Resources

Contacts

Create or ingest a contact

POST/v1-public-contacts
Scopecontacts:writeorcontact_ingest

Creates a contact, or updates the one it matches. Can create or link the company by name or domain.

Request body
{
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@example.com",
  "phone": "+1-555-123-4567",
  "title": "Product Manager",
  "company_name": "Acme Inc",
  "company_domain": "acme.com",
  "source": "website_form",
  "signal_strength": 75,
  "message": "Interested in your enterprise plan"
}
FieldTypeDescription
first_namestringContact first name
last_namestringContact last name
emailstringMatches an existing contact
phonestringPhone number
titlestringJob title
company_namestringCreates the company if it is new
company_domainstringMatches or creates the company
sourcestringLead source label
signal_strengthnumberIntent score, 0 to 100 (default: 50)
messagestringFree-text message, for example from a form
tagsstring[]Up to 20 labels
Response
{
  "success": true,
  "data": {
    "contact_id": "uuid",
    "company_id": "uuid",
    "is_new_contact": true,
    "is_new_company": false
  },
  "meta": { ... }
}

201 when a contact is created, 200 when an existing one is updated.

Batch ingest contacts

POST/v1-public-contacts-batch
Scopecontacts:writeorcontact_ingest

Up to 100 contacts per call, each with the same fields as a single ingest.

Request body
{
  "contacts": [
    { "email": "a@example.com", "first_name": "Alice" },
    { "email": "b@example.com", "first_name": "Bob" }
  ],
  "options": { "stop_on_error": false }
}

201 when all were written, 207 when some failed (read results[].success), 400 when none were. Max 500 KB.

Get or list contacts

GET/v1-public-contacts-crud?id={uuid}

One contact by id, with its company.

GET/v1-public-contacts-crud?page=1&per_page=25

A page of contacts.

Scopecontacts:read
FiltersTypeDescription
emailstringExact email
company_iduuidContacts at this company
sourcestringExact lead source
searchstringWords in first name, last name or email

Sort by: created_at, updated_at, first_name, last_name, email

Update a contact

PATCH/v1-public-contacts-crud?id={uuid}
Scopecontacts:write
Request body
{ "title": "Senior Product Manager", "phone": "+1-555-999-0000" }

Updatable: first_name, last_name, email, phone, title, company_id, source, birthday. Send only what changes.

Delete a contact

DELETE/v1-public-contacts-crud?id={uuid}
Scopecontacts:write
Response
{ "success": true, "data": { "deleted": true, "id": "uuid" } }

Companies

Create a company

POST/v1-public-companies
Scopecompanies:write

A company with the same domain is updated instead, and the call answers 200. Otherwise a new one is created (201), which needs a name.

Request body
{
  "name": "Acme Inc",
  "domain": "acme.com",
  "industry": "Technology",
  "size": "51-200",
  "website": "https://acme.com",
  "city": "San Francisco",
  "country": "US"
}
FieldTypeDescription
namestringRequired when creating
domainstringMatches an existing company
industrystringIndustry
sizestringSize range, for example 51-200
websitestringWebsite URL
phonestringPhone number
citystringCity
statestringState or region
countrystringCountry code

Get or list companies

GET/v1-public-companies?id={uuid}
GET/v1-public-companies?page=1&per_page=25
Scopecompanies:read
FiltersTypeDescription
domainstringExact domain
industrystringExact industry
searchstringWords in name or domain

Sort by: created_at, updated_at, name, domain

Update a company

PATCH/v1-public-companies?id={uuid}
Scopecompanies:write
Request body
{ "industry": "SaaS", "size": "201-500" }

Updatable: every field above. Send only what changes.

Delete a company

DELETE/v1-public-companies?id={uuid}
Scopecompanies:write

Deals

Create a deal

POST/v1-public-deals
Scopedeals:write

Needs a title, and a value or a company_id.

Request body
{
  "title": "Enterprise License: Acme",
  "value": 50000,
  "currency": "USD",
  "stage": "Proposal",
  "expected_close_date": "2026-04-15",
  "company_id": "uuid",
  "contact_id": "uuid"
}
FieldTypeDescription
titlestring (required)Deal title
valuenumberDeal amount
currencystringCurrency code (default: the workspace currency)
stagestringStage name or stage id (default: the first stage)
statusstringopen, won or lost (default: open)
probabilitynumberWin probability, 0 to 100
expected_close_datedateYYYY-MM-DD
company_iduuidLinked company
contact_iduuidLinked contact
pipeline_iduuidPipeline (default: the workspace default)
assigned_touuidOwner, a workspace member (default: the key creator)
notesstringFree text

Get or list deals

GET/v1-public-deals?id={uuid}
GET/v1-public-deals?page=1&per_page=25
Scopedeals:read
FiltersTypeDescription
stagestringExact stage name
statusstringopen, won or lost
company_iduuidDeals at this company
pipeline_iduuidDeals in this pipeline
searchstringWords in the title

Sort by: created_at, updated_at, title, value, expected_close_date

Update a deal

PATCH/v1-public-deals?id={uuid}
Scopedeals:write
Request body
{ "stage": "Negotiation", "probability": 80 }

A new pipeline_id without a stage moves the deal to that pipeline's first stage.

Delete a deal

DELETE/v1-public-deals?id={uuid}
Scopedeals:write

Tasks

Create a task

POST/v1-public-tasks
Scopetasks:write
Request body
{
  "title": "Follow up with Acme",
  "description": "Send proposal by Friday",
  "priority": "high",
  "due_date": "2026-03-01",
  "contact_id": "uuid",
  "deal_id": "uuid"
}
FieldTypeDescription
titlestring (required)Task title
descriptionstringTask description
statusstringopen or completed (default: open)
prioritystringlow, medium or high (default: medium)
task_typestringtodo, email, call, meeting, linkedin_message, linkedin_connection or custom
due_datedateYYYY-MM-DD
assigned_touuidA workspace member (default: the key creator)
contact_iduuidLinked contact
company_iduuidLinked company
deal_iduuidLinked deal

Get or list tasks

GET/v1-public-tasks?id={uuid}
GET/v1-public-tasks?page=1&per_page=25
Scopetasks:read
FiltersTypeDescription
statusstringopen or completed
prioritystringlow, medium or high
assigned_touuidTasks for this member
contact_iduuidTasks on this contact
company_iduuidTasks on this company
deal_iduuidTasks on this deal
searchstringWords in the title

Sort by: created_at, updated_at, title, due_date, priority

Update a task

PATCH/v1-public-tasks?id={uuid}
Scopetasks:write
Request body
{ "status": "completed", "description": "Proposal sent" }

Send status completed to complete a task and open to reopen it. A task with open sub-tasks cannot be completed.

Delete a task

DELETE/v1-public-tasks?id={uuid}
Scopetasks:write

Invoices

Create an invoice

POST/v1-public-invoices
Scopeinvoices:write

Needs invoice_number and total. The number must be unique in the workspace.

Request body
{
  "invoice_number": "INV-2026-001",
  "status": "draft",
  "issue_date": "2026-02-19",
  "due_date": "2026-03-19",
  "subtotal": 5000,
  "tax_amount": 500,
  "total": 5500,
  "currency": "USD",
  "company_id": "uuid",
  "notes": "Net 30"
}
FieldTypeDescription
invoice_numberstring (required)Unique in the workspace
totalnumber (required)Total amount
statusstringdraft, sent, paid, partially_paid, overdue or cancelled (default: draft)
issue_datedateYYYY-MM-DD (default: today)
due_datedateYYYY-MM-DD
subtotalnumberSubtotal amount
tax_amountnumberTax amount
currencystringCurrency code
company_iduuidBilled company
notesstringFree text
line_itemsjsonLine items, stored as sent

Get or list invoices

GET/v1-public-invoices?id={uuid}
GET/v1-public-invoices?page=1&per_page=25
Scopeinvoices:read
FiltersTypeDescription
statusstringExact status
company_iduuidInvoices for this company
searchstringInvoice number contains

Sort by: created_at, updated_at, invoice_number, due_date, total, issue_date

Update an invoice

PATCH/v1-public-invoices?id={uuid}
Scopeinvoices:write
Request body
{ "status": "sent", "due_date": "2026-04-01" }

Delete an invoice

DELETE/v1-public-invoices?id={uuid}
Scopeinvoices:write

Activities

Create or ingest an activity

POST/v1-public-activities
Scopeactivities:writeoractivity_ingest

Records an event from Jira, a helpdesk, a form or any webhook, on the timeline of the contact, company or deal it matches.

Request body
{
  "type": "jira_issue",
  "entity_email": "john.doe@example.com",
  "entity_domain": "acme.com",
  "title": "PROJ-123: New Feature Request",
  "description": "Customer requested dashboard export",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": { "issue_key": "PROJ-123", "priority": "high" }
}

note, call, email and meeting set what the timeline shows. The other values are source labels: they are accepted and show as a note. Anything else is stored as external_activity.

notecallemailmeetingjira_issuesupport_ticketform_submissioncontact_submissionwebhookexternal_activity

Linking

  • entity_emailThe contact with this email
  • entity_domainThe company with this domain
  • contact_id · company_id · deal_idThat record, when it is in your workspace

With no match the event is still recorded, activity_id is null, and nothing appears on a timeline.

Get or list activities

GET/v1-public-activities-crud?id={uuid}
GET/v1-public-activities-crud?page=1&per_page=25
Scopeactivities:read
FiltersTypeDescription
typestringTimeline type, for example note, call, email, meeting or task
contact_iduuidOn this contact
company_iduuidOn this company
deal_iduuidOn this deal
searchstringWords in the title

Sort by: created_at, type, title

Delete an activity

DELETE/v1-public-activities-crud?id={uuid}
Scopeactivities:write

Inbound form submissions

Submit a form entry

POST/v1-public-inbound-submissions
Scopeinbound:submit

For a form on your own site or in another form tool. Each submission is stored first, then runs the lead journey set in Inbound: contact, company, Leads board, owner, notice, response target and follow-up task. Create the form in Inbound, Forms, New form, Connect an external form, and publish it.

Request body
{
  "form_id": "website-contact",
  "external_id": "wpforms-48213",
  "email": "anna.larsen@northwind.example",
  "name": "Anna Larsen",
  "company": "Northwind Advisory",
  "phone": "+46 70 123 45 67",
  "message": "We need a CRM for 20 sellers.",
  "budget": "10-20k",
  "utm": { "utm_source": "linkedin", "utm_medium": "paid" },
  "page_url": "https://northwind.example/contact"
}
FieldTypeDescription
form_idstring (required)The form's ID from its Connection tab: the readable Form ID when one is set, or the long id. Both keep working.
external_idstringYour own id for this submission. Sending it again answers 200 duplicate and stores nothing new. Up to 200 characters.
emailstring (required)The person's email. Required, directly or through one of the names below.
namestringThe full name. Or send first_name and last_name.
companystringThe company name.
company_domainstringThe company's website. Without it, the email domain is used, unless it is a free email provider.
phone, titlestringSet on a new contact. An existing contact, matched by email, is not changed.
message, …stringAny other key, such as message or budget, is kept as an answer on the submission and the contact's timeline.
fieldsobjectAnswers can also sit here, or nested deeper. Nested values are read by dot path, for example contact.email.
utmobjectutm_source, utm_medium and the rest, for attribution.
page_url, referrerstringThe page the form was on, and where the visitor came from.

Names read without any setup

  • emailemaile_mailemail_addressyour_emailmailwork_email
  • namenamefull_namefullnameyour_namecontact_name
  • first_namefirst_namefirstnamefirstgiven_namefname
  • last_namelast_namelastnamelastfamily_namesurnamelname
  • companycompanycompany_nameorganizationorganisationbusinessaccount
  • company_domaincompany_domaindomainwebsitecompany_websiteurlsite
  • phonephonephone_numbertelephonetelyour_phonemobile
  • titletitlejob_titleroleposition
  • messagemessageyour_messagecommentscommentinquiryenquirybodytextnotenotesquestion

Keys match ignoring case, with spaces, dots and dashes read as underscores, so Your-Email counts as your_email. For any other name, map it once on the form's Fields tab. Nothing changes on your side.

Response (202)
{
  "success": true,
  "data": { "submission_id": "0b6f1c9e-4d2a-4e8b-9a7c-5f3e2d1c0b9a", "status": "accepted" },
  "meta": { "request_id": "req_...", "api_version": "1", "timestamp": "2026-09-28T10:00:00.000Z" }
}

Responses

StatusCodeMeaning
202acceptedStored, and the lead journey ran.
202heldStored, but held as likely spam until someone reviews it in Data Hygiene.
200duplicateThis external_id was already received for the form, or, without one, the same email sent the same answers within 10 minutes. Nothing new was stored.
400VALIDATION_ERRORNo valid email was found, or form_id is malformed. details.unmapped_keys lists what was sent but not read.
403EXTERNAL_FORMS_DISABLEDExternal forms are switched off in Inbound, Setup, Lead capture.
404NOT_FOUNDNo form with this form_id in the key's workspace.
409FORM_NOT_EXTERNALThe form is one built in Lumenbase. It takes its embed snippet, not this endpoint.
409FORM_NOT_PUBLISHEDThe form is a draft or archived. Publish it first.

The answer names the submission and nothing else. A key that can only submit is not told which records exist.

Send from your server, never from page code: anyone can read a key in the browser. Retry a 503 with the same external_id, so it cannot be stored twice.

Jobs

Submit a job application

POST/v1-public-job-applications
Scopejobs:apply

Sends one application for a published job post from the agency's own site. The workspace comes from the API key, so the same form works for every workspace the key belongs to. A candidate can apply once per job and email address; the second attempt answers duplicate.

Request body
curl -X POST "https://api.lumenbase.io/functions/v1/v1-public-job-applications"   -H "Authorization: Bearer $LUMENBASE_API_KEY"   -F "post=lagermedarbetare-goteborg"   -F "first_name=Ada"   -F "last_name=Lind"   -F "email=ada@example.com"   -F "phone=+46701234567"   -F "message=Available from June"   -F "consent=true"   -F "consent_text=I agree that Agency AB keeps my application."   -F "cv=@cv.pdf"

The CV is optional and must be a PDF, DOC or DOCX file of at most 5 MB. Send the request as multipart/form-data with a Content-Length header. The applicant's address and the workspace are both rate limited, and the same hourly allowance covers the hosted career page.

201 with the application id. 400 when a field is missing or invalid, or the CV is empty or not a PDF, DOC or DOCX. 413 when the body or the CV is too large. 404 when no published job has that post slug. 409 when this email address already applied for this job. 429 when the workspace or address allowance is used up. 503 when the CV could not be stored, so nothing was saved.

Errors

StatusCodeMeaning
400VALIDATION_ERRORA field is missing, malformed or not allowed. details.field names it when there is one.
401UNAUTHORIZEDMissing, malformed or revoked API key. Send Authorization: Bearer <key>.
403FORBIDDENThe key lacks the scope. details.required_scope names it.
404NOT_FOUNDNo such record in this workspace, or it was deleted.
405METHOD_NOT_ALLOWEDThe endpoint does not support this method.
409CONFLICTA record with this unique value already exists, for example an invoice number.
410GONEA retired legacy endpoint past its sunset date. The body names its successor.
413PAYLOAD_TOO_LARGEBody over the size limit: 50 KB, or 500 KB for batch. Some endpoints report this as 400.
429RATE_LIMIT_EXCEEDEDA ceiling is full: the minute, the daily record cap, or the monthly fair-use cap. details.window names it. Wait for Retry-After seconds; the refused request spent nothing.
500INTERNAL_ERRORAn error on our side. Send us meta.request_id.
503SERVICE_UNAVAILABLEA database step did not answer in time. See Rate limits before retrying a create.

Branch on error.code, not on the message. Messages are written for people and can change; codes are part of the contract.

Rate limits

Three ceilings apply, and each one means something different. All are per workspace, and a refused request spends nothing.

  • Every resource: 100 requests per minute. Batch ingest spends the contacts bucket. Inbound form submissions have their own bucket with the same limits.
  • Contacts: also 10,000 records per day. This counts records written, so a batch of 100 spends 100 and a read spends nothing.
  • Activities: also 50,000 records per day, counted the same way.
  • Batch: no separate daily number. A batch spends one contact record per contact it carries.
  • Every workspace: 2,000,000 requests per calendar month across the API and inbound capture. This is the fair-use ceiling, far above what an integration needs. Website tracking has its own limits and does not spend it.

Successful responses carry

  • X-RateLimit-LimitRequests allowed per minute in this bucket.
  • X-RateLimit-RemainingRequests left in this minute.
  • X-RateLimit-ResetWhen the minute resets, in Unix seconds. Always the minute, whichever ceiling refused the last request.
  • X-DailyLimit-RemainingRecords left today. Contacts, activities and inbound form submissions.
  • X-DailyLimit-ResetWhen the day resets, in Unix seconds. Contacts, activities and inbound form submissions.
  • X-MonthlyLimit-LimitRequests allowed this calendar month.
  • X-MonthlyLimit-RemainingRequests left this calendar month.
  • X-MonthlyLimit-ResetWhen the month resets, in Unix seconds.
  • Retry-AfterOn a 429, the wait in seconds for the ceiling that refused it.

A 503 SERVICE_UNAVAILABLE means a database step did not answer in time. Reads are safe to retry. A create may already have been written, so retry an ingest with the same X-Idempotency-Key, and read before retrying any other create.

Idempotency

Send X-Idempotency-Key so a retry after a timeout cannot create a duplicate. Replaying the same key inside 24 hours returns the original result instead of writing again.

Honored on POST /v1-public-contacts, POST /v1-public-activities and POST /v1-public-inbound-submissions only. Every other endpoint ignores the header, so make those retries safe by reading first.

X-Idempotency-Key: 7d3f9c1e-2b84-4a17-9f0d-6c5e8a2b1d34

Use 8 to 128 characters, unique per operation. A UUID works. A key outside that length is ignored, not rejected.

Keys are per workspace and resource and last 24 hours. A replay answers 200 with the first response. Idempotency-Key works as an alias.

Matching and connectors

  • Contacts match by email, then phone.
  • Companies match by domain (taken from a work email when none is sent), then website, then name.
  • An activity without a deal_id joins its company's most recent open deal.
  • A connector maps your own field names onto these fields for the ingest endpoints. Nested JSON uses dot notation, for example user.profile.email.

Versioning

The version is in the path and echoed in every response as meta.api_version and the X-API-Version header.

New fields can appear in a response at any time and are not treated as a breaking change. Parse defensively and ignore fields you do not recognise, rather than validating against an exact shape.

A breaking change means a new path prefix. The existing one keeps working through the deprecation process below.

The application version shown elsewhere in the product is a release date, not an API version. The two move independently.

Deprecation policy

When an endpoint is retiring, it says so in its own responses before it stops working.

A retiring endpoint returns

  • Deprecationtrue
  • Sunsetthe date it stops answering, as an HTTP date.
  • Linkthe replacement, with rel pointing at the successor version.

The sunset date is at least six months after the deprecation is announced. After it passes the endpoint returns 410 Gone, naming its successor.

Currently deprecated

  • POST /public-contactsPOST /v1-public-contactsSunset 26 November 2026
  • POST /public-activitiesPOST /v1-public-activitiesSunset 31 March 2027

Log these headers. They are the earliest warning you will get, and they arrive on responses that are otherwise succeeding.

OpenAPI description

The full contract is published as an OpenAPI 3.1 document. Import it into Postman or Insomnia, generate a client, or diff it between releases to see exactly what changed.

Document URL
https://lumenbase.io/api/openapi.json
Fetch it
curl -s https://lumenbase.io/api/openapi.json -o lumenbase-openapi.json

Best practices

  1. 1Give each integration its own key, so you can revoke one without breaking the rest.
  2. 2Send an email with every contact. It is the most reliable match.
  3. 3Send X-Idempotency-Key when retrying an ingest.
  4. 4Load contacts in bulk through the batch endpoint.
  5. 5Send website forms to POST /v1-public-inbound-submissions with a key that holds only inbound:submit. A leaked key can then send submissions and nothing else.
  6. 6Watch X-RateLimit-Remaining, and on a 429 wait for Retry-After.
  7. 7Log meta.request_id from every response. Support finds your request by it.

Not a developer?

Use Zapier to connect Lumenbase to 6,000+ apps, or browse the Help Center.

Help Center

Need an API key? Sign in, then openSettingsCompany SettingsAPI

Get started