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.
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.
| Scope | Permits |
|---|---|
| contacts:read | Search and view contacts, and read the contact field schema |
| contacts:write | Create and update contacts |
| activities:read | View logged calls, emails, meetings and notes |
| activities:write | Log and edit activities |
| deals:read | List and view deals — required by the Chrome extension |
| deals:write | Create, update and delete deals |
| products:read | View products and deal line items |
| products:write | Manage products and deal line items |
| introductions:read | View introductions |
| introductions:write | Create, update and delete introductions |
| signals:write | Push external intent signals into the CRM |
| enrichment:read | View enrichment jobs, estimates and the credit balance |
| enrichment:write | Run enrichment, ingest funding events and spend credits |
| tags:read | List available tags |
| job-posts:read | Search and view job posts and account opening counts |
| job-posts:write | Import 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
https://your-crm-domain.com/api/v1All 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/verify | Check that a key is valid and see which organization it belongs to. | any valid key |
Contacts and accounts
| POST | /contacts | Create 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/search | Exact lookup by ?email= or ?linkedin= (profile URL). | contacts:read |
| GET | /contacts/fields | List custom field definitions so you can send valid customFields keys. | contacts:read |
| POST | /accounts | Find or create an account by domain or name. Never updates an existing account. | contacts:write |
| GET | /tags | List public tags and their categories, optionally filtered by ?category=. | tags:read |
Enrichment
| POST | /contacts/[id]/enrich | Start an email or phone enrichment run for a contact. Returns 202; the run is asynchronous. | enrichment:write |
| GET | /contacts/[id]/enrich/status | Enrichment status per type, including which provider resolved it. | enrichment:read |
Activities and signals
| GET | /activities | Recent activities for a contact (?contactId= required, ?limit= 1 to 50). | activities:read |
| POST | /activities | Log 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 | /signals | Log an intent signal (website visit, LinkedIn engagement); creates or links the contact and account. | signals:write |
Deals
| GET | /deals | Deals for a contact (?contactId= required). | deals:read |
| POST | /deals | Create 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 | /products | List products (active only unless ?includeInactive=true); ?search= and ?category= filters. | products:read |
| POST | /products | Create 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-items | List the line items on a deal. | products:read |
| POST | /deals/[id]/line-items | Add 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 | /introductions | List introductions, with filters for introducer, date range, value and type. | introductions:read |
| POST | /introductions | Record 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-posts | Ingest a batch of job postings. Idempotent: re-sending the same batch creates nothing new. | job-posts:write |
| GET | /job-posts | List 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
/tagsList all public tags available in your organization. Use this to discover tag IDs before sending contacts.
Query Parameters
| Param | Type | Description |
|---|---|---|
| category | string | Optional. Filter by category name |
Scope Required
tags:readcurl -H "Authorization: Bearer crm_XXXX" \
https://your-crm.com/api/v1/tags{
"tags": [
{
"id": "cmlhl8g7k00058ckb...",
"name": "Customer",
"category": "relationship",
"categoryId": "cmlhl8g7900018ckb...",
"color": "#10B981"
},
{
"id": "cmlhl8g7f00038ckb...",
"name": "Prospect",
"category": "relationship",
"color": "#3B82F6"
}
]
}/contactsSmart 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| Field | Type | Required | Description |
|---|---|---|---|
| string | ✓ | Primary match field for deduplication | |
| firstName | string | Contact first name | |
| lastName | string | Contact last name | |
| city | string | Auto-normalized (e.g. 'SF' → 'San Francisco') | |
| state | string | Auto-normalized (e.g. 'CA' → 'California') | |
| country | string | Auto-normalized (e.g. 'US' → 'United States') | |
| geography | string | namer, emea, apac, latam (auto-detected) | |
| location | string | Free-form location string (e.g. 'Tiaret, Tiaret, Algeria') | |
| warmth | string | 'cold' or 'engaged' (default: 'engaged') | |
| contactStatus | string | 'active', 'not_interested', 'do_not_contact', 'unsubscribed' | |
| lifecycleStage | string | 'prospect', 'non_prospect', 'marketing_qualified', 'sales_qualified', 'customer', 'churned', 'disqualified' | |
| phones | array | Array of {phone, label?, primary?} | |
| socials | array | Array of {platform, profileName, profileUrl, label?} | |
| company | object | {name, domain?, jobTitle?} — auto-creates account | |
| tags | string[] | Tag IDs or tag names (auto-creates unknown names) | |
| mergeStrategy | object | Per-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.
| Strategy | Behavior |
|---|---|
fill_blanksdefault | Only write if the CRM field is empty/null. Safe, non-destructive. |
incoming_wins | Always overwrite with the incoming value, regardless of existing data. |
existing_wins | Never overwrite. Keeps whatever is already in the CRM. |
"mergeStrategy": {
"default": "fill_blanks",
"firstName": "incoming_wins",
"lastName": "incoming_wins",
"warmth": "existing_wins"
}/signalsLog 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| Field | Type | Required | Description |
|---|---|---|---|
| type | string | ✓ | WEBSITE_VISIT or LINKEDIN_ENGAGE |
| content | string | ✓ | Human-readable description of the signal |
| string | Contact email (used for dedup/upsert) | ||
| linkedinUrl | string | LinkedIn profile URL (used for dedup/upsert) | |
| firstName | string | Contact first name (for creation) | |
| lastName | string | Contact last name (for creation) | |
| companyName | string | Company name (creates/matches account) | |
| companyDomain | string | Company domain (creates/matches account) | |
| subject | string | Short summary (e.g. 'Visited /pricing') | |
| metadata | object | Arbitrary JSON — page URL, duration, post URL, etc. | |
| occurredAt | string | ISO 8601 timestamp (defaults to now) | |
| tags | string[] | Tag names to apply (auto-created if new) | |
| warmth | string | '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)
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
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)
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
{
"signal": {
"activityId": "cmmfkkruf004k...",
"type": "WEBSITE_VISIT",
"contactId": 42,
"contactStatus": "created",
"accountId": "cmmfkkrst004b...",
"accountStatus": "created"
}
}/activitiesLog 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:writecurl -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" }
}'{
"activity": {
"id": "abc123...",
"type": "EMAIL",
"subject": "Follow-up on demo",
"content": "Sent product proposal attached",
"contact": { "id": 42, "firstName": "Jane", "lastName": "Doe" }
}
}