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" }
    }

    type is one of these. Anything else is stored as external_activity.

    jira_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.

    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_EXCEEDEDThe rate limit bucket is full. Wait for Retry-After seconds.
    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

    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-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.

    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 Center

    Need an API key? Sign in, then openSettingsCompany SettingsAPI

    Get started