REST API Reference

Integrate your applications with crmzero. Create and update contacts, deals, products, activities and more, and keep your CRM in sync — all via a simple REST API.

Quick Start

1. Get an API Key

Request a key from your CRM admin

2. Look Up Tags

Fetch available tags for your org

3. Send Contacts

Create or update contacts via API

Authentication

All API requests require a Bearer token in the Authorization header. API keys are issued by your CRM administrator.

http
Authorization: Bearer crm_XXXXXXXXXXXX...

Keep your API key secret. It grants access to create and modify CRM data. If compromised, ask your admin to revoke it immediately.

Scopes

Every key is issued with a set of scopes, chosen when the key is created. Each endpoint below names the scope it requires; a request from a key without it returns 403. Scopes are independent: a write scope does not include the matching read scope, so a key that only logs activities should hold activities:write and nothing more.

ScopePermits
contacts:readSearch and view contacts, and read the contact field schema
contacts:writeCreate and update contacts
activities:readView logged calls, emails, meetings and notes
activities:writeLog and edit activities
deals:readList and view deals — required by the Chrome extension
deals:writeCreate, update and delete deals
products:readView products and deal line items
products:writeManage products and deal line items
introductions:readView introductions
introductions:writeCreate, update and delete introductions
signals:writePush external intent signals into the CRM
enrichment:readView enrichment jobs, estimates and the credit balance
enrichment:writeRun enrichment, ingest funding events and spend credits
tags:readList available tags
job-posts:readSearch and view job posts and account opening counts
job-posts:writeImport job posts and update your organization's tags, notes and custom fields on them

Setting up the Chrome extension? Choose the "Chrome extension" preset when creating the key — it selects exactly the scopes the extension needs.

Base URL

text
https://your-crm-domain.com/api/v1

All endpoints are prefixed with /api/v1. Replace the domain with your actual CRM URL.

All endpoints

Every endpoint you can call with an API key, with the scope it needs. The sections below give request and response examples for the most common ones. Managing API keys and enrichment provider settings is done in the app, not through the API.

Account and keys

GET/auth/verifyCheck that a key is valid and see which organization it belongs to.any valid key

Contacts and accounts

POST/contactsCreate or update a contact (smart upsert by email or LinkedIn, with a merge strategy).contacts:write
GET/contacts/[id]Fetch one contact with emails, phones, socials, companies and tags.contacts:read
GET/contacts/searchExact lookup by ?email= or ?linkedin= (profile URL).contacts:read
GET/contacts/fieldsList custom field definitions so you can send valid customFields keys.contacts:read
POST/accountsFind or create an account by domain or name. Never updates an existing account.contacts:write
GET/tagsList public tags and their categories, optionally filtered by ?category=.tags:read

Enrichment

POST/contacts/[id]/enrichStart an email or phone enrichment run for a contact. Returns 202; the run is asynchronous.enrichment:write
GET/contacts/[id]/enrich/statusEnrichment status per type, including which provider resolved it.enrichment:read

Activities and signals

GET/activitiesRecent activities for a contact (?contactId= required, ?limit= 1 to 50).activities:read
POST/activitiesLog an activity (email, call, meeting, note, task) against a contact.activities:write
PATCH/activities/[id]Edit an activity's subject, content, completion time or privacy.activities:write
POST/signalsLog an intent signal (website visit, LinkedIn engagement); creates or links the contact and account.signals:write

Deals

GET/dealsDeals for a contact (?contactId= required).deals:read
POST/dealsCreate a deal.deals:write
GET/deals/[id]Fetch one deal.deals:read
PATCH/deals/[id]Update any subset of a deal's fields; null clears a field.deals:write
DELETE/deals/[id]Delete a deal.deals:write

Products and line items

GET/productsList products (active only unless ?includeInactive=true); ?search= and ?category= filters.products:read
POST/productsCreate a product (name and unitPrice required).products:write
GET/products/[id]Fetch one product.products:read
PUT/products/[id]Update a product.products:write
DELETE/products/[id]Deactivate a product. Existing line items keep referencing it.products:write
GET/deals/[id]/line-itemsList the line items on a deal.products:read
POST/deals/[id]/line-itemsAdd a line item to a deal (productId required).products:write
PUT/line-items/[id]Update a line item's quantity, price, discount or description.products:write
DELETE/line-items/[id]Remove a line item.products:write

Introductions

GET/introductionsList introductions, with filters for introducer, date range, value and type.introductions:read
POST/introductionsRecord an introduction between two contacts.introductions:write
PATCH/introductions/[id]Update an introduction; absent keys are unchanged, null clears.introductions:write
DELETE/introductions/[id]Delete an introduction.introductions:write

Job posts

POST/job-postsIngest a batch of job postings. Idempotent: re-sending the same batch creates nothing new.job-posts:write
GET/job-postsList job posts with search, account, status, date, tag and technology filters.job-posts:read
GET/job-posts/[id]Fetch one job post in full.job-posts:read
PATCH/job-posts/[id]Update your organization's tags, custom fields and hidden flag (and the facts of a posting you imported).job-posts:write

Endpoints

GET/tags

List all public tags available in your organization. Use this to discover tag IDs before sending contacts.

Query Parameters

ParamTypeDescription
categorystringOptional. Filter by category name

Scope Required

tags:read
bash
curl -H "Authorization: Bearer crm_XXXX" \
  https://your-crm.com/api/v1/tags
json
{
  "tags": [
    {
      "id": "cmlhl8g7k00058ckb...",
      "name": "Customer",
      "category": "relationship",
      "categoryId": "cmlhl8g7900018ckb...",
      "color": "#10B981"
    },
    {
      "id": "cmlhl8g7f00038ckb...",
      "name": "Prospect",
      "category": "relationship",
      "color": "#3B82F6"
    }
  ]
}

POST/contacts

Smart upsert — automatically finds existing contacts by email and merges data using your chosen strategy. Creates a new contact if no match is found.

Scope Required

contacts:write
FieldTypeRequiredDescription
emailstring✓Primary match field for deduplication
firstNamestringContact first name
lastNamestringContact last name
citystringAuto-normalized (e.g. 'SF' → 'San Francisco')
statestringAuto-normalized (e.g. 'CA' → 'California')
countrystringAuto-normalized (e.g. 'US' → 'United States')
geographystringnamer, emea, apac, latam (auto-detected)
locationstringFree-form location string (e.g. 'Tiaret, Tiaret, Algeria')
warmthstring'cold' or 'engaged' (default: 'engaged')
contactStatusstring'active', 'not_interested', 'do_not_contact', 'unsubscribed'
lifecycleStagestring'prospect', 'non_prospect', 'marketing_qualified', 'sales_qualified', 'customer', 'churned', 'disqualified'
phonesarrayArray of {phone, label?, primary?}
socialsarrayArray of {platform, profileName, profileUrl, label?}
companyobject{name, domain?, jobTitle?} — auto-creates account
tagsstring[]Tag IDs or tag names (auto-creates unknown names)
mergeStrategyobjectPer-field merge rules (see below)

When updating an existing contact, you control how each field is merged. Set a default strategy and override per-field as needed.

StrategyBehavior
fill_blanksdefaultOnly write if the CRM field is empty/null. Safe, non-destructive.
incoming_winsAlways overwrite with the incoming value, regardless of existing data.
existing_winsNever overwrite. Keeps whatever is already in the CRM.
json
"mergeStrategy": {
  "default": "fill_blanks",
  "firstName": "incoming_wins",
  "lastName": "incoming_wins",
  "warmth": "existing_wins"
}

POST/signals

Log an intent signal — a website visit or LinkedIn engagement — as a CRM activity. Automatically creates or matches contacts and accounts, so you can send raw signal data without worrying about whether records exist.

Scope Required

signals:write
FieldTypeRequiredDescription
typestring✓WEBSITE_VISIT or LINKEDIN_ENGAGE
contentstring✓Human-readable description of the signal
emailstringContact email (used for dedup/upsert)
linkedinUrlstringLinkedIn profile URL (used for dedup/upsert)
firstNamestringContact first name (for creation)
lastNamestringContact last name (for creation)
companyNamestringCompany name (creates/matches account)
companyDomainstringCompany domain (creates/matches account)
subjectstringShort summary (e.g. 'Visited /pricing')
metadataobjectArbitrary JSON — page URL, duration, post URL, etc.
occurredAtstringISO 8601 timestamp (defaults to now)
tagsstring[]Tag names to apply (auto-created if new)
warmthstring'cold' or 'engaged' (escalation only, never downgrades)

At least one identifier is required: email, linkedinUrl, companyName, or companyDomain. If only company data is provided (no email/LinkedIn), the activity is logged on the account with no contact.

Website Visit (contact + company)

bash
curl -X POST https://your-crm.com/api/v1/signals \
  -H "Authorization: Bearer crm_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "WEBSITE_VISIT",
    "email": "jane@acme.com",
    "firstName": "Jane",
    "lastName": "Doe",
    "companyName": "Acme Corp",
    "companyDomain": "acme.com",
    "subject": "Visited /pricing",
    "content": "Viewed pricing page for 45 seconds",
    "metadata": {
      "url": "/pricing",
      "duration": 45,
      "referrer": "google.com"
    },
    "tags": ["Website Visitor"]
  }'

LinkedIn Engagement

bash
curl -X POST https://your-crm.com/api/v1/signals \
  -H "Authorization: Bearer crm_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "LINKEDIN_ENGAGE",
    "linkedinUrl": "linkedin.com/in/janedoe",
    "firstName": "Jane",
    "lastName": "Doe",
    "content": "Liked post about product launch",
    "metadata": {
      "postUrl": "linkedin.com/posts/123",
      "engagementType": "like"
    }
  }'

Company-Only Visit (anonymous)

bash
curl -X POST https://your-crm.com/api/v1/signals \
  -H "Authorization: Bearer crm_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "WEBSITE_VISIT",
    "companyName": "Acme Corp",
    "companyDomain": "acme.com",
    "content": "Anonymous visit to /demo page"
  }'

Response

json
{
  "signal": {
    "activityId": "cmmfkkruf004k...",
    "type": "WEBSITE_VISIT",
    "contactId": 42,
    "contactStatus": "created",
    "accountId": "cmmfkkrst004b...",
    "accountStatus": "created"
  }
}

POST/activities

Log an activity (call, email, meeting, note, or task) on a contact. Requires the contact to already exist — use POST /contacts first if needed.

Scope Required

activities:write
bash
curl -X POST https://your-crm.com/api/v1/activities \
  -H "Authorization: Bearer crm_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "EMAIL",
    "contactId": 42,
    "subject": "Follow-up on demo",
    "content": "Sent product proposal attached",
    "metadata": { "template": "proposal-v2" }
  }'
json
{
  "activity": {
    "id": "abc123...",
    "type": "EMAIL",
    "subject": "Follow-up on demo",
    "content": "Sent product proposal attached",
    "contact": { "id": 42, "firstName": "Jane", "lastName": "Doe" }
  }
}