developers

API reference

Versioned at /api/v1 · plain JSON over HTTPS · no SDK required · base URL https://www.emberlinecrm.com

Introduction

The Emberline API is the same one the app uses. Anything Emberline can do with a contact, a deal, a listing, or an inbound lead, your integration can do too — push leads from a custom form, sync contacts into your warehouse, or log notes from your dialer.

Requests and responses are JSON. Successful list calls return { data: [...], nextCursor }; single-resource calls return { data: {...} }; errors return { error: "message" } with an appropriate status code.

First request
curl -s "https://www.emberlinecrm.com/api/v1/contacts?tag=buyer" \
  -H "Authorization: Bearer embk_..."

Authentication

Every request carries a Bearer key: Authorization: Bearer embk_.... Keys are created under Settings → Developers in your workspace, with scopes chosen at creation — a key that ingests leads can't read your database. Rotate or revoke any time; revocation is immediate.

Scopes used by this reference: contacts:read, contacts:write, activities:write, deals:read, properties:read, tasks:read, ingest.

Endpoints

Contacts

GET/api/v1/contactscontacts:read

List contacts, newest first, 50 per page.

Query parameters
tagstringOnly contacts carrying this tag.
sourcestringOnly contacts from this lead source.
emailstringExact email match (any of the contact's addresses).
cursorstringCursor from the previous page's nextCursor.
Response 200
{
  "data": [
    {
      "id": "cmshq...",
      "firstName": "Maria",
      "lastName": "Delgado",
      "emails": ["maria@example.com"],
      "phones": ["+12145550134"],
      "source": "zillow",
      "tags": ["buyer", "frisco"],
      "custom": {},
      "consentEmail": true,
      "dnc": false,
      "score": 62,
      "nextAction": "Push the open deal to the next stage.",
      "createdAt": "2026-08-01T14:03:22.000Z",
      "updatedAt": "2026-08-06T09:41:07.000Z"
    }
  ],
  "nextCursor": "cmshq..."
}
POST/api/v1/contactscontacts:write

Create a contact. Runs the same intake gate as every other source — normalized, deduped (matching email/phone merges instead of duplicating), attributed. Returns 201 with { data, action: "created" | "merged" }.

Body fields
firstNamestringOptional — at least one identifying field required.
lastNamestringOptional.
emailstringPrimary email.
phonestringPrimary phone (E.164 preferred).
sourcestringAttribution slug, e.g. "my-integration".
tagsstring[]Initial tags.
notestringFirst timeline note.
Request
curl -s -X POST https://www.emberlinecrm.com/api/v1/contacts \
  -H "Authorization: Bearer embk_..." -H "Content-Type: application/json" \
  -d '{"firstName":"Maria","email":"maria@example.com","source":"my-integration","tags":["buyer"]}'
GET/api/v1/contacts/{id}contacts:read

One contact plus their 20 most recent timeline entries.

PATCH/api/v1/contacts/{id}contacts:write

Partial update — send only what changes: tags, custom fields, consentEmail/dnc, or nextAction.

POST/api/v1/contacts/{id}/notesactivities:write

Append a note to the contact's timeline: { "body": "..." }.

Deals

GET/api/v1/dealsdeals:read

List deals with stage, value, and linked contact.

Query parameters
statusenumOPEN · WON · LOST
cursorstringPagination cursor.

Properties

GET/api/v1/propertiesproperties:read

List property records / listings.

Query parameters
statusstringListing status filter.
citystringCity filter.
minPrice / maxPricenumberPrice band.
bedsnumberMinimum bedrooms.
cursorstringPagination cursor.
GET/api/v1/properties/{id}properties:read

One listing by id.

Tasks

GET/api/v1/taskstasks:read

List tasks and appointments.

Query parameters
statusenumOPEN · DONE
contactIdstringOnly tasks on this contact.
cursorstringPagination cursor.

Lead ingest

POST/api/v1/ingest/{sourceKey}ingest

Universal lead intake — point any form, portal, or Zapier at it. Accepts loose field names, maps them through the source's field map, dedupes, attributes, and arms speed-to-lead. Returns 202 immediately; processing is queued. This is the right endpoint for bulk pushes — it queues instead of counting against the interactive rate limit.

Pagination

List endpoints are cursor-paginated. Each page includes nextCursor — pass it back as ?cursor= for the next page; when it's null you've reached the end. Cursors are stable under concurrent writes; offsets are not, so we don't offer them.

Walking pages
let cursor = null;
do {
  const res = await fetch(`https://www.emberlinecrm.com/api/v1/contacts${cursor ? `?cursor=${cursor}` : ""}`, {
    headers: { Authorization: "Bearer embk_..." },
  });
  const { data, nextCursor } = await res.json();
  // ...use data
  cursor = nextCursor;
} while (cursor);

Errors

Errors are JSON — { "error": "human-readable message" } — with a conventional status code:

StatusMeaningWhat to do
400Malformed request / invalid JSONFix the body — the message names the field.
401Missing or invalid keyCheck the Authorization header and key status.
403Key lacks the required scopeCreate a key with the scope listed on the endpoint.
404Resource not in your workspaceIDs never leak across workspaces — verify the id.
422Valid JSON, unusable valuesThe message says which constraint failed.
429Rate limitedBack off and retry; see rate limits below.

Webhooks

Register an endpoint in the app and we POST JSON when things happen. Events: contact.created, contact.tagged, deal.stage_changed, deal.won.

Every delivery is signed. The X-Emberline-Signature header is t=<unix seconds>,v1=hex(hmac_sha256(secret, `${t}.${body}`)) — the secret is shown once when you register the endpoint.

Verify a signature (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

function verifySignature(secret, header, rawBody) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
}

Respond 2xx within a few seconds. Failed deliveries retry with backoff; 20 consecutive failures disable the endpoint. Registered endpoints get a delivery log and one-click redelivery in the app.

Rate limits

120 requests/minute per workspace with burst headroom. Beyond that you get 429 — back off exponentially and retry. Bulk-importing? Use the ingest endpoint (it queues) or ask us about a one-time migration.

API keys & request logs

Keys are created under Settings → Developers once you have a workspace — pick scopes at creation, rotate or revoke any time. Every call your keys make lands in the request log on the Developers page, so "the integration broke" takes seconds to diagnose.

No workspace yet? Create one or request a demo.