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
Step 1
Create a key
Grant only the scopes the integration uses. The key is shown once.
SettingsCompany SettingsAPIStep 2
Send a request
Put the key in the Authorization header of every call.
Bearer lmb_…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/v1Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/jsonOptional headers
X-Idempotency-KeyMakes a retried ingest safe. See Idempotency.X-Request-IdYour own trace id, echoed back in meta.request_id.
Every response
{
"success": true,
"data": { ... },
"meta": {
"request_id": "req_...",
"api_version": "1",
"timestamp": "..."
}
}{
"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
- 401
API key
Missing, malformed or revoked
- 403
Scope
Key lacks the scope for this call
- 429
Rate limit
The bucket is full
- 400404409
Validation
Bad field, unknown id or no such record
- 200201202
Done
Created, accepted, read, updated or deleted
- 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.
| Scope | Description |
|---|---|
| contacts:read | Read and list contacts |
| contacts:write | Create, update, and delete contacts |
| companies:read | Read and list companies |
| companies:write | Create, update, and delete companies |
| deals:read | Read and list deals |
| deals:write | Create, update, and delete deals |
| tasks:read | Read and list tasks |
| tasks:write | Create, update, and delete tasks |
| invoices:read | Read and list invoices |
| invoices:write | Create, update, and delete invoices |
| activities:read | Read and list activities |
| activities:write | Create and delete activities |
| inbound:submit | Send form submissions to an Inbound form, and nothing else. The key cannot read or change any record. |
| read | Read access to all entity types |
| admin | Full 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.
| Field | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| per_page | integer | Items per page, max 100 (default: 25) |
| sort_by | string | Field to sort by (varies per entity) |
| sort_order | string | "asc" or "desc" (default: "desc") |
| search | string | Free-text search across key fields |
{
"success": true,
"data": {
"items": [ ... ],
"pagination": { "page": 1, "per_page": 25, "total": 142, "total_pages": 6 }
},
"meta": { ... }
}Resources
Contacts
People, matched by email on ingest
POSTGETPATCHDELETE
Companies
Accounts, matched by domain
POSTGETPATCHDELETE
Deals
Pipeline opportunities
POSTGETPATCHDELETE
Tasks
To-dos, open or completed
POSTGETPATCHDELETE
Invoices
Billing records
POSTGETPATCHDELETE
Activities
Timeline events from any source
POSTGETDELETE
Inbound form submissions
Website form entries, run through your lead journey
POST
Contacts
Create or ingest a contact
/v1-public-contactsCreates a contact, or updates the one it matches. Can create or link the company by name or domain.
{
"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"
}| Field | Type | Description |
|---|---|---|
| first_name | string | Contact first name |
| last_name | string | Contact last name |
| string | Matches an existing contact | |
| phone | string | Phone number |
| title | string | Job title |
| company_name | string | Creates the company if it is new |
| company_domain | string | Matches or creates the company |
| source | string | Lead source label |
| signal_strength | number | Intent score, 0 to 100 (default: 50) |
| message | string | Free-text message, for example from a form |
| tags | string[] | Up to 20 labels |
{
"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
/v1-public-contacts-batchUp to 100 contacts per call, each with the same fields as a single ingest.
{
"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
/v1-public-contacts-crud?id={uuid}One contact by id, with its company.
/v1-public-contacts-crud?page=1&per_page=25A page of contacts.
| Filters | Type | Description |
|---|---|---|
| string | Exact email | |
| company_id | uuid | Contacts at this company |
| source | string | Exact lead source |
| search | string | Words in first name, last name or email |
Sort by: created_at, updated_at, first_name, last_name, email
Update a contact
/v1-public-contacts-crud?id={uuid}{ "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
/v1-public-contacts-crud?id={uuid}{ "success": true, "data": { "deleted": true, "id": "uuid" } }Companies
Create a company
/v1-public-companiesA company with the same domain is updated instead, and the call answers 200. Otherwise a new one is created (201), which needs a name.
{
"name": "Acme Inc",
"domain": "acme.com",
"industry": "Technology",
"size": "51-200",
"website": "https://acme.com",
"city": "San Francisco",
"country": "US"
}| Field | Type | Description |
|---|---|---|
| name | string | Required when creating |
| domain | string | Matches an existing company |
| industry | string | Industry |
| size | string | Size range, for example 51-200 |
| website | string | Website URL |
| phone | string | Phone number |
| city | string | City |
| state | string | State or region |
| country | string | Country code |
Get or list companies
/v1-public-companies?id={uuid}/v1-public-companies?page=1&per_page=25| Filters | Type | Description |
|---|---|---|
| domain | string | Exact domain |
| industry | string | Exact industry |
| search | string | Words in name or domain |
Sort by: created_at, updated_at, name, domain
Update a company
/v1-public-companies?id={uuid}{ "industry": "SaaS", "size": "201-500" }Updatable: every field above. Send only what changes.
Delete a company
/v1-public-companies?id={uuid}Deals
Create a deal
/v1-public-dealsNeeds a title, and a value or a company_id.
{
"title": "Enterprise License: Acme",
"value": 50000,
"currency": "USD",
"stage": "Proposal",
"expected_close_date": "2026-04-15",
"company_id": "uuid",
"contact_id": "uuid"
}| Field | Type | Description |
|---|---|---|
| title | string (required) | Deal title |
| value | number | Deal amount |
| currency | string | Currency code (default: the workspace currency) |
| stage | string | Stage name or stage id (default: the first stage) |
| status | string | open, won or lost (default: open) |
| probability | number | Win probability, 0 to 100 |
| expected_close_date | date | YYYY-MM-DD |
| company_id | uuid | Linked company |
| contact_id | uuid | Linked contact |
| pipeline_id | uuid | Pipeline (default: the workspace default) |
| assigned_to | uuid | Owner, a workspace member (default: the key creator) |
| notes | string | Free text |
Get or list deals
/v1-public-deals?id={uuid}/v1-public-deals?page=1&per_page=25| Filters | Type | Description |
|---|---|---|
| stage | string | Exact stage name |
| status | string | open, won or lost |
| company_id | uuid | Deals at this company |
| pipeline_id | uuid | Deals in this pipeline |
| search | string | Words in the title |
Sort by: created_at, updated_at, title, value, expected_close_date
Update a deal
/v1-public-deals?id={uuid}{ "stage": "Negotiation", "probability": 80 }A new pipeline_id without a stage moves the deal to that pipeline's first stage.
Delete a deal
/v1-public-deals?id={uuid}Tasks
Create a task
/v1-public-tasks{
"title": "Follow up with Acme",
"description": "Send proposal by Friday",
"priority": "high",
"due_date": "2026-03-01",
"contact_id": "uuid",
"deal_id": "uuid"
}| Field | Type | Description |
|---|---|---|
| title | string (required) | Task title |
| description | string | Task description |
| status | string | open or completed (default: open) |
| priority | string | low, medium or high (default: medium) |
| task_type | string | todo, email, call, meeting, linkedin_message, linkedin_connection or custom |
| due_date | date | YYYY-MM-DD |
| assigned_to | uuid | A workspace member (default: the key creator) |
| contact_id | uuid | Linked contact |
| company_id | uuid | Linked company |
| deal_id | uuid | Linked deal |
Get or list tasks
/v1-public-tasks?id={uuid}/v1-public-tasks?page=1&per_page=25| Filters | Type | Description |
|---|---|---|
| status | string | open or completed |
| priority | string | low, medium or high |
| assigned_to | uuid | Tasks for this member |
| contact_id | uuid | Tasks on this contact |
| company_id | uuid | Tasks on this company |
| deal_id | uuid | Tasks on this deal |
| search | string | Words in the title |
Sort by: created_at, updated_at, title, due_date, priority
Update a task
/v1-public-tasks?id={uuid}{ "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
/v1-public-tasks?id={uuid}Invoices
Create an invoice
/v1-public-invoicesNeeds invoice_number and total. The number must be unique in the workspace.
{
"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"
}| Field | Type | Description |
|---|---|---|
| invoice_number | string (required) | Unique in the workspace |
| total | number (required) | Total amount |
| status | string | draft, sent, paid, partially_paid, overdue or cancelled (default: draft) |
| issue_date | date | YYYY-MM-DD (default: today) |
| due_date | date | YYYY-MM-DD |
| subtotal | number | Subtotal amount |
| tax_amount | number | Tax amount |
| currency | string | Currency code |
| company_id | uuid | Billed company |
| notes | string | Free text |
| line_items | json | Line items, stored as sent |
Get or list invoices
/v1-public-invoices?id={uuid}/v1-public-invoices?page=1&per_page=25| Filters | Type | Description |
|---|---|---|
| status | string | Exact status |
| company_id | uuid | Invoices for this company |
| search | string | Invoice number contains |
Sort by: created_at, updated_at, invoice_number, due_date, total, issue_date
Update an invoice
/v1-public-invoices?id={uuid}{ "status": "sent", "due_date": "2026-04-01" }Delete an invoice
/v1-public-invoices?id={uuid}Activities
Create or ingest an activity
/v1-public-activitiesRecords an event from Jira, a helpdesk, a form or any webhook, on the timeline of the contact, company or deal it matches.
{
"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" }
}type is one of these. Anything else is stored as external_activity.
Linking
entity_emailThe contact with this emailentity_domainThe company with this domaincontact_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
/v1-public-activities-crud?id={uuid}/v1-public-activities-crud?page=1&per_page=25| Filters | Type | Description |
|---|---|---|
| type | string | Timeline type, for example note, call, email, meeting or task |
| contact_id | uuid | On this contact |
| company_id | uuid | On this company |
| deal_id | uuid | On this deal |
| search | string | Words in the title |
Sort by: created_at, type, title
Delete an activity
/v1-public-activities-crud?id={uuid}Inbound form submissions
Submit a form entry
/v1-public-inbound-submissionsFor 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.
{
"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"
}| Field | Type | Description |
|---|---|---|
| form_id | string (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_id | string | Your own id for this submission. Sending it again answers 200 duplicate and stores nothing new. Up to 200 characters. |
| string (required) | The person's email. Required, directly or through one of the names below. | |
| name | string | The full name. Or send first_name and last_name. |
| company | string | The company name. |
| company_domain | string | The company's website. Without it, the email domain is used, unless it is a free email provider. |
| phone, title | string | Set on a new contact. An existing contact, matched by email, is not changed. |
| message, … | string | Any other key, such as message or budget, is kept as an answer on the submission and the contact's timeline. |
| fields | object | Answers can also sit here, or nested deeper. Nested values are read by dot path, for example contact.email. |
| utm | object | utm_source, utm_medium and the rest, for attribution. |
| page_url, referrer | string | The 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.
{
"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
| Status | Code | Meaning |
|---|---|---|
| 202 | accepted | Stored, and the lead journey ran. |
| 202 | held | Stored, but held as likely spam until someone reviews it in Data Hygiene. |
| 200 | duplicate | This 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. |
| 400 | VALIDATION_ERROR | No valid email was found, or form_id is malformed. details.unmapped_keys lists what was sent but not read. |
| 403 | EXTERNAL_FORMS_DISABLED | External forms are switched off in Inbound, Setup, Lead capture. |
| 404 | NOT_FOUND | No form with this form_id in the key's workspace. |
| 409 | FORM_NOT_EXTERNAL | The form is one built in Lumenbase. It takes its embed snippet, not this endpoint. |
| 409 | FORM_NOT_PUBLISHED | The 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.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | A field is missing, malformed or not allowed. details.field names it when there is one. |
| 401 | UNAUTHORIZED | Missing, malformed or revoked API key. Send Authorization: Bearer <key>. |
| 403 | FORBIDDEN | The key lacks the scope. details.required_scope names it. |
| 404 | NOT_FOUND | No such record in this workspace, or it was deleted. |
| 405 | METHOD_NOT_ALLOWED | The endpoint does not support this method. |
| 409 | CONFLICT | A record with this unique value already exists, for example an invoice number. |
| 410 | GONE | A retired legacy endpoint past its sunset date. The body names its successor. |
| 413 | PAYLOAD_TOO_LARGE | Body over the size limit: 50 KB, or 500 KB for batch. Some endpoints report this as 400. |
| 429 | RATE_LIMIT_EXCEEDED | The rate limit bucket is full. Wait for Retry-After seconds. |
| 500 | INTERNAL_ERROR | An error on our side. Send us meta.request_id. |
| 503 | SERVICE_UNAVAILABLE | A 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
Limits are per workspace, counted in one-minute windows. Each resource has its own bucket.
- Every resource: 100 requests per minute. Batch ingest and Inbound form submissions spend the contacts bucket.
- Contacts: also 10,000 requests per day, Inbound form submissions included.
- Activities: also 50,000 requests per day.
- A batch call counts once toward the daily cap, however many contacts it carries.
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.X-DailyLimit-RemainingRequests left today. Contacts and activities only.X-DailyLimit-ResetWhen the day resets, in Unix seconds. Contacts and activities only.Retry-AfterOn a 429, the wait in seconds.
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-6c5e8a2b1d34Use 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
DeprecationtrueSunsetthe 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 2026POST /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.
https://lumenbase.io/api/openapi.jsoncurl -s https://lumenbase.io/api/openapi.json -o lumenbase-openapi.jsonBest practices
- 1Give each integration its own key, so you can revoke one without breaking the rest.
- 2Send an email with every contact. It is the most reliable match.
- 3Send X-Idempotency-Key when retrying an ingest.
- 4Load contacts in bulk through the batch endpoint.
- 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.
- 6Watch X-RateLimit-Remaining, and on a 429 wait for Retry-After.
- 7Log meta.request_id from every response. Support finds your request by it.
Known limitations
Behaviour that does not match what the rest of this page would lead you to expect. Listed here because finding it at runtime is worse.
- Two requests with the same X-Idempotency-Key sent at the same moment can both write.
- The activity_id in a contact ingest response cannot be fetched through the Activities endpoints.
- An ingested activity keeps its type only in the ingest log. On the timeline its type is note.
- Requests turned away by the per-minute limit still count toward the daily cap.
- Inbound form submissions keep X-Idempotency-Key in the contacts space. A key already used on POST /v1-public-contacts replays that answer. Send external_id instead, which is kept per form.
Not a developer?
Use Zapier to connect Lumenbase to 6,000+ apps, or browse the Help Center.
Help CenterNeed an API key? Sign in, then openSettingsCompany SettingsAPI
Get started