API / Contacts
Contacts
People in your CRM. Create or update them by email, including company, tags, lifecycle stage and any custom fields you have defined.
List contacts
GET/api/v1/contacts
Contacts in the workspace, newest first, in pages of up to 100. Filter by exact email, a free-text search, or a tag. Pass nextCursor back as cursor to fetch the following page; it is null on the last page.
Query parameters
| Parameter | Type | Description |
|---|---|---|
email | string | Exact, case-insensitive match. Returns at most one contact and ignores the other filters. |
q | string | Matches first name, last name, email or company name. |
tag | string | Only contacts carrying this tag. |
limit | number | 1 to 100. Default 50. |
cursor | string | The nextCursor value from the previous page. |
Responses
| Status | Meaning |
|---|---|
| 200 | Body: { contacts: [...], nextCursor: string | null }. |
Example request
curl "https://crowdstack.org/api/v1/contacts?tag=outbound-q4&limit=2" \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"contacts": [
{
"id": "cmf3k2x9a0001abcd",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"jobTitle": "Head of Growth",
"phone": null,
"website": "https://acme.com",
"linkedinUrl": "https://linkedin.com/in/janedoe",
"lifecycleStage": "lead",
"tags": ["outbound-q4"],
"fields": { "demo_url": "https://example.com/demos/acme", "plan_interest": "Growth" },
"company": { "id": "cmf3k2x9a0002abcd", "name": "Acme", "domain": "acme.com" },
"createdAt": "2026-09-15T10:12:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
],
"nextCursor": "cmf3k2x9a0001abcd"
}Get a contact
GET/api/v1/contacts/:id
One contact by id, in the same shape the list and upsert endpoints return.
Responses
| Status | Meaning |
|---|---|
| 200 | Body: { contact }. |
| 404 | No contact with that id in this workspace. |
Example request
curl https://crowdstack.org/api/v1/contacts/cmf3k2x9a0001abcd \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"contact": {
"id": "cmf3k2x9a0001abcd",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"jobTitle": "Head of Growth",
"phone": null,
"website": "https://acme.com",
"linkedinUrl": "https://linkedin.com/in/janedoe",
"lifecycleStage": "lead",
"tags": ["outbound-q4"],
"fields": { "demo_url": "https://example.com/demos/acme", "plan_interest": "Growth" },
"company": { "id": "cmf3k2x9a0002abcd", "name": "Acme", "domain": "acme.com" },
"createdAt": "2026-09-15T10:12:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
}Create or update a contact
POST/api/v1/contacts
Looks the contact up by email inside your workspace. Creates it if it does not exist, otherwise updates it. Keys you leave out are left alone, null clears a value, tags are added to the existing set, and custom fields merge key by key.
Custom field keys must exist under Settings → CRM → Fields. Unknown keys are rejected with 422 rather than dropped, so a typo never reaches a live campaign as a blank merge field. Select fields accept either the option value or its label.
Company matching tries the website's domain first, then a case-insensitive name match, and creates the company when neither matches. Pass null to unlink the contact from its company.
Request body
| Field | Type | Description |
|---|---|---|
emailrequired | string | Lower-cased and used as the match key. |
firstName | string | null | Defaults to the part of the email before @ when a new contact has no name. |
lastName | string | null | — |
jobTitle | string | null | — |
phone | string | null | Stored as given; no formatting is applied. |
website | string | null | Normalised to a URL. |
linkedinUrl | string | null | Normalised to a linkedin.com profile URL. |
lifecycleStage | string | null | One of your workspace's lifecycle stages, by value or label. |
tags | string[] | Added to the contact's tags. Never removes existing tags. Up to 50. |
fields | object | Custom field values keyed by field key. Strings, numbers, booleans or null, matching each field's type. |
company | object | null | { name?, website? }. Matched or created as described above. |
Responses
| Status | Meaning |
|---|---|
| 201 | A new contact was created. Body: { contact, created: true }. |
| 200 | An existing contact was updated. Body: { contact, created: false }. |
| 422 | Validation failed; see details for the offending paths. |
Example request
curl -X POST https://crowdstack.org/api/v1/contacts \
-H "Authorization: Bearer $CROWDSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@acme.com",
"firstName": "Jane",
"lastName": "Doe",
"jobTitle": "Head of Growth",
"lifecycleStage": "lead",
"tags": ["outbound-q4"],
"company": { "name": "Acme", "website": "https://acme.com" },
"fields": { "demo_url": "https://example.com/demos/acme" }
}'Example response
{
"created": true,
"contact": {
"id": "cmf3k2x9a0001abcd",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"jobTitle": "Head of Growth",
"phone": null,
"website": "https://acme.com",
"linkedinUrl": "https://linkedin.com/in/janedoe",
"lifecycleStage": "lead",
"tags": ["outbound-q4"],
"fields": { "demo_url": "https://example.com/demos/acme", "plan_interest": "Growth" },
"company": { "id": "cmf3k2x9a0002abcd", "name": "Acme", "domain": "acme.com" },
"createdAt": "2026-09-15T10:12:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
}