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.
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
/api/v1/contactscontacts:readList contacts, newest first, 50 per page.
| tag | string | Only contacts carrying this tag. |
| source | string | Only contacts from this lead source. |
| string | Exact email match (any of the contact's addresses). | |
| cursor | string | Cursor from the previous page's nextCursor. |
{
"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..."
}/api/v1/contactscontacts:writeCreate 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" }.
| firstName | string | Optional — at least one identifying field required. |
| lastName | string | Optional. |
| string | Primary email. | |
| phone | string | Primary phone (E.164 preferred). |
| source | string | Attribution slug, e.g. "my-integration". |
| tags | string[] | Initial tags. |
| note | string | First timeline note. |
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"]}'/api/v1/contacts/{id}contacts:readOne contact plus their 20 most recent timeline entries.
/api/v1/contacts/{id}contacts:writePartial update — send only what changes: tags, custom fields, consentEmail/dnc, or nextAction.
/api/v1/contacts/{id}/notesactivities:writeAppend a note to the contact's timeline: { "body": "..." }.
Deals
/api/v1/dealsdeals:readList deals with stage, value, and linked contact.
| status | enum | OPEN · WON · LOST |
| cursor | string | Pagination cursor. |
Properties
/api/v1/propertiesproperties:readList property records / listings.
| status | string | Listing status filter. |
| city | string | City filter. |
| minPrice / maxPrice | number | Price band. |
| beds | number | Minimum bedrooms. |
| cursor | string | Pagination cursor. |
/api/v1/properties/{id}properties:readOne listing by id.
Tasks
/api/v1/taskstasks:readList tasks and appointments.
| status | enum | OPEN · DONE |
| contactId | string | Only tasks on this contact. |
| cursor | string | Pagination cursor. |
Lead ingest
/api/v1/ingest/{sourceKey}ingestUniversal 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.
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:
| Status | Meaning | What to do |
|---|---|---|
| 400 | Malformed request / invalid JSON | Fix the body — the message names the field. |
| 401 | Missing or invalid key | Check the Authorization header and key status. |
| 403 | Key lacks the required scope | Create a key with the scope listed on the endpoint. |
| 404 | Resource not in your workspace | IDs never leak across workspaces — verify the id. |
| 422 | Valid JSON, unusable values | The message says which constraint failed. |
| 429 | Rate limited | Back 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.
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.