Contacts
The project's customer base — the Contact object, custom contact fields, tags, the activity journal, duplicate search, merging and CSV export.
A contact is a customer, lead or partner of the project. Besides its built-in fields, a contact can hold values for the
project's custom fields (meta) and carry any number of tags. It can be assigned to a project member, and that
assignment decides who sees it. Orders link to contacts through contact_uuid.
Deleting a contact is permanent. Its orders stay and lose their contact_uuid.
Field types follow the field notation.
Access
Every endpoint on this page requires the projects OAuth scope. All endpoints except custom fields also require the
contacts scope. The user must be a member of the project, and the owner's subscription must be active. Otherwise the
endpoint returns 404 RES_NOT_FOUND. The project owner bypasses permission checks.
| Permission | Grants |
|---|---|
can_view_own_contacts |
Read contacts assigned to the user (user_uuid) |
can_view_role_contacts |
Read contacts assigned to members with the same role as the user |
can_view_all_contacts |
Read every contact in the project |
can_edit_contacts |
Create, update, delete, merge and export contacts; manage tags; add journal entries |
can_edit_project |
Manage custom contact fields |
Every endpoint that addresses a contact, including updating, deleting, merging and adding journal entries, sees only
the contacts within the user's widest view permission. A contact outside that range behaves as if it does not exist
(404 RES_NOT_FOUND).
user_uuid must be a member of the project and tag_uuids must be tags of the project; otherwise the request fails
with 400 RES_NOT_FOUND. On update, user_uuid is checked only when it changes.
Contact Object
Contact Structure
| Field | Type | Description |
|---|---|---|
| uuid | uuid | Contact ID |
| name | string | Name, up to 255 characters. May be empty |
| string | Email, up to 64 characters. May be empty. Not validated as an email address | |
| phone | string | Phone, up to 32 characters. May be empty. Stored as sent |
| address | string | Address, up to 512 characters |
| note | string | Free-text note, up to 1024 characters |
| type | string | lead, client or partner |
| status | string | active or archive |
| user_uuid | ?uuid | Project member the contact is assigned to |
| meta | object | Custom field values keyed by field name. Every value is a string |
| tags | array of tag objects | Tags on the contact: uuid, name and color only |
| total_spent | decimal | Sum of paid_amount over this contact's orders in a done-type status. Only in List Contacts |
| orders_count | integer | Number of this contact's orders that are not deleted. Only in List Contacts |
| last_order_date | ?ISO8601 datetime | When the contact's latest order was created. Only in List Contacts |
| created_at | ISO8601 datetime | When the contact was created |
| updated_at | ISO8601 datetime | When the contact was last changed |
total_spent and orders_count are computed by Postgres and are returned as strings, e.g. "1470.00" and "3".
Example Contact
{
"uuid": "0199a4d0-11aa-7b0c-9e8f-7a6b5c4d3e2f",
"name": "Acme Windows LLC",
"email": "[email protected]",
"phone": "+1 555 010 2030",
"address": "12 Industrial Rd, Springfield",
"note": "",
"type": "client",
"status": "active",
"user_uuid": "0198e7aa-91f0-7c22-a4d1-0f9e8d7c6b5a",
"meta": {
"inn": "7701234567",
"delivery_address": "Warehouse 4, gate B"
},
"tags": [
{
"uuid": "0199a1aa-0001-7000-8000-000000000001",
"name": "Wholesale",
"color": "emerald"
}
],
"total_spent": "1470.00",
"orders_count": "3",
"last_order_date": "2026-10-01T09:30:00.000Z",
"created_at": "2026-08-14T07:12:00.000Z",
"updated_at": "2026-09-30T16:45:10.000Z"
}
Contact Input
The body of Create Contact, Modify Contact and Replace Contact.
| Field | Type | Description |
|---|---|---|
| name | string | Up to 255 characters |
| string | Up to 64 characters | |
| phone | string | Up to 32 characters |
| address | string | Up to 512 characters |
| note | string | Up to 1024 characters |
| type | string | lead, client or partner |
| status | string | active or archive |
| user_uuid | ?uuid | Assigned member; null unassigns |
| meta | object | Custom field values, {name: value}; values must be strings. See Custom field values |
| tag_uuids | array of uuid | Tags to put on the contact. Duplicates are ignored. Replaces the current tags |
Custom field values
meta keys are custom field names. Values are stored as strings of up to 512 characters, whatever the field's
type, so send numbers and dates as strings, e.g. "2026-10-01". On update, only the keys you send change. An empty
string removes that value; keys you leave out are kept.
Values are not checked against the field definitions: an unknown key is stored, and is_required is not enforced by
the API. Required fields are enforced in one place only — when an order moves into a status that requires them, see
Modify Order.
List Contacts
GET /api/v1/projects/{project.uuid}/contacts
Returns a list of contact objects visible to the user, newest first. Unless a status filter is
given, only active contacts are returned.
Requires one of can_view_own_contacts, can_view_role_contacts, can_view_all_contacts.
Query String Params
| Field | Type | Description |
|---|---|---|
| page? | integer | Page number, from 1. Default 1 |
| limit? | integer | Page size, 1–100. Default 50 |
| filters? | string | A URL-encoded JSON array of filter objects. Invalid JSON is ignored |
Filters combine with AND. The response is a plain array with no total count.
Contact Filter Structure
| Field | Type | Description |
|---|---|---|
| field | string | What to filter on, see below |
| operator | string | equal (default), include, starts_with, greater, less, is_empty |
| value | any | Value to compare with |
field |
Matches |
|---|---|
name, email, phone, address, type, status |
The contact's own column. include and starts_with are case-sensitive |
total_spent, orders_count, last_order_date |
The computed values above; is_empty is ignored |
tags |
value is a tag UUID or an array of them; matches contacts with any of the tags. operator is ignored |
| any other string | A custom field name. is_empty matches contacts with no value for it |
Example Request
GET /api/v1/projects/0198e7a0-0000-7000-8000-000000000001/contacts?filters=%5B%7B%22field%22%3A%22type%22%2C%22value%22%3A%22client%22%7D%5D
Authorization: Bearer <access_token>
where filters is the encoded form of:
[
{
"field": "type",
"operator": "equal",
"value": "client"
},
{
"field": "tags",
"value": [
"0199a1aa-0001-7000-8000-000000000001"
]
}
]
Response
200 OK — an array of contact objects.
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role has no contact view permission |
| 404 | RES_NOT_FOUND |
The project does not exist or is not accessible |
Get Contact
GET /api/v1/projects/{project.uuid}/contacts/{contact.uuid}
Returns a single contact, without total_spent, orders_count and last_order_date.
Requires one of can_view_own_contacts, can_view_role_contacts, can_view_all_contacts.
Response
200 OK — a contact object.
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role has no contact view permission |
| 404 | RES_NOT_FOUND |
The project or contact does not exist, or the contact is outside the user's visibility |
Create Contact
POST /api/v1/projects/{project.uuid}/contacts
Creates a contact and writes a created entry to its journal.
Requires can_edit_contacts.
JSON Params
All fields of Contact Input are optional. Defaults: empty strings for the text fields, type: "lead",
status: "active", no member, no custom values, no tags.
The API does not check for duplicates. To find existing contacts with the same phone or email, use Find Duplicate Contacts.
Example Request
{
"name": "Acme Windows LLC",
"email": "[email protected]",
"phone": "+1 555 010 2030",
"type": "client",
"meta": {
"inn": "7701234567"
},
"tag_uuids": [
"0199a1aa-0001-7000-8000-000000000001"
]
}
Response
201 Created
{
"uuid": "0199a4d0-11aa-7b0c-9e8f-7a6b5c4d3e2f"
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema |
| 400 | RES_NOT_FOUND |
user_uuid is not a project member, or a tag is not in this project |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project does not exist or is not accessible |
Modify Contact
PATCH /api/v1/projects/{project.uuid}/contacts/{contact.uuid}
Updates only the fields you send. tag_uuids, if sent, replaces all tags; [] removes them. meta changes only the
keys it contains. Changes are recorded in the journal as an updated entry.
Requires can_edit_contacts.
JSON Params
Any fields of Contact Input. At least one must be present.
Example Request
{
"status": "archive",
"meta": {
"delivery_address": ""
}
}
Response
204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema |
| 400 | REQ_NO_DATA_PROVIDED |
The body has no fields |
| 400 | RES_NOT_FOUND |
A new user_uuid is not a project member, or a tag is not in this project |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project or contact does not exist, or the contact is outside the user's visibility |
Replace Contact
PUT /api/v1/projects/{project.uuid}/contacts/{contact.uuid}
Same as Modify Contact, but name, email, phone, address, note, type and status are
required. user_uuid, meta and tag_uuids stay optional and are left untouched when omitted.
Requires can_edit_contacts.
Response
204 No Content
Errors
Same as Modify Contact, except REQ_NO_DATA_PROVIDED.
Delete Contact
DELETE /api/v1/projects/{project.uuid}/contacts/{contact.uuid}
Permanently deletes the contact with its custom values and tags. Its orders remain with contact_uuid set to null.
Requires can_edit_contacts.
Response
204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project or contact does not exist, or the contact is outside the user's visibility |
Contact Journal
Contact Journal Entry Structure
| Field | Type | Description |
|---|---|---|
| project_uuid | uuid | Project ID |
| contact_uuid | ?uuid | The contact |
| user_uuid | ?uuid | Who made the entry; null for system and automation entries |
| user_name | string | Author's full name; empty string if none |
| entity_type | ?string | What the entry is attached to besides the contact, e.g. order |
| entity_uuid | ?uuid | ID of that entity |
| action | string | What happened, see below |
| message | ?string | Note text, or the merged contact's name for merged |
| payload | ?object | Action details. For updated, a field-level diff of the contact, its meta and tags |
| created_at | ISO8601 datetime | When the entry was recorded |
| Action | Meaning |
|---|---|
created |
The contact was created |
updated |
Fields, custom values or tags changed |
merged |
Another contact was merged into this one; message holds its name |
note |
A note, written by a user or through Create Contact Journal Entry |
email |
An email was sent to the contact by an automation |
task_created, task_completed |
A task linked to the contact was created or completed; see Tasks |
Other actions may be added over time. Clients should show unknown actions as generic entries rather than fail.
Get Contact Journal
GET /api/v1/projects/{project.uuid}/contacts/{contact.uuid}/logs
Returns the contact's whole journal, oldest first, as an array of journal entries. This includes notes made on the contact's orders.
Requires one of can_view_own_contacts, can_view_role_contacts, can_view_all_contacts.
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role has no contact view permission |
| 404 | RES_NOT_FOUND |
The project or contact does not exist, or the contact is outside the user's visibility |
Create Contact Journal Entry
POST /api/v1/projects/{project.uuid}/contacts/{contact.uuid}/logs
Adds an entry to the contact's journal. The author is the authenticated user.
Requires can_edit_contacts.
JSON Params
| Field | Type | Description |
|---|---|---|
| message? | ?string | Entry text |
| action? | string | Entry action. Default note |
| entity_type? | string | Attached entity type. Default order |
| entity_uuid? | ?uuid | Attached entity ID |
| payload? | object | Extra data. Default {} |
For an ordinary note, send only message. To note an order, prefer
Create Order Note, which also works before the order has a contact.
Example Request
{
"message": "Called back, waiting for the signed quote."
}
Response
201 Created with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project or contact does not exist, or the contact is outside the user's visibility |
Find Duplicate Contacts
GET /api/v1/projects/{project.uuid}/contacts/duplicates
Finds groups of active contacts that share a phone number or an email. Phones are compared by digits only, so
+1 (555) 010-2030 matches 15550102030. Emails are compared trimmed and lower-cased. Names are never compared. Only
contacts visible to the user are considered.
Requires can_edit_contacts.
Response
200 OK
| Field | Type | Description |
|---|---|---|
| groups | array | Groups of duplicates; phone groups first, then email groups |
Duplicate Group Structure
| Field | Type | Description |
|---|---|---|
| match_type | string | phone or email |
| match_value | string | The normalized value the contacts share |
| contacts | array of contact objects | The matching contacts, oldest first. No updated_at or computed fields |
A contact can appear in both a phone group and an email group.
Example Response
{
"groups": [
{
"match_type": "phone",
"match_value": "15550102030",
"contacts": [
{
"uuid": "0199a4d0-11aa-7b0c-9e8f-7a6b5c4d3e2f",
"name": "Acme Windows LLC",
"phone": "+1 555 010 2030",
"...": "..."
},
{
"uuid": "0199a6f2-2b3c-7d4e-8f50-617283940a1b",
"name": "Acme",
"phone": "15550102030",
"...": "..."
}
]
}
]
}
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project does not exist or is not accessible |
Merge Contacts
POST /api/v1/projects/{project.uuid}/contacts/merge
Merges loser_uuid into winner_uuid in a single transaction:
fieldsare written to the winner. Send the final value for each field you resolved; fields you omit keep the winner's value.- The loser's orders, journal, warehouse receipts and tasks are moved to the winner.
- Tags are combined.
- The loser's custom values that the winner does not have are copied over. Values in
fields.metatake precedence. - The loser is deleted, and a
mergedentry is added to the winner's journal.
Requires can_edit_contacts.
JSON Params
| Field | Type | Description |
|---|---|---|
| winner_uuid | uuid | Contact that remains |
| loser_uuid | uuid | Contact that is merged and deleted; must differ from winner_uuid |
| fields? | object | Final values for the winner: any of name, email, phone, address, note, type, status, user_uuid, meta, with the same rules as Contact Input |
Example Request
{
"winner_uuid": "0199a4d0-11aa-7b0c-9e8f-7a6b5c4d3e2f",
"loser_uuid": "0199a6f2-2b3c-7d4e-8f50-617283940a1b",
"fields": {
"email": "[email protected]",
"meta": {
"inn": "7701234567"
}
}
}
Response
204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema, or the two UUIDs are equal |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project or either contact does not exist, or one is outside the user's visibility |
Export Contacts
GET /api/v1/projects/{project.uuid}/contacts/export
Exports every contact matching filters, without pagination, as a CSV file. Only contacts visible to the user are
included. filters works as in List Contacts.
Requires can_edit_contacts.
Query String Params
| Field | Type | Description |
|---|---|---|
| filters? | string | A URL-encoded JSON array of filter objects |
Response
200 OK with Content-Type: text/csv; charset=utf-8 and Content-Disposition: attachment; filename="contacts.csv".
The file is UTF-8 with a byte-order mark, comma-separated, with CRLF line endings.
Columns: Name, Email, Phone, Address, Note, Type, Status, Owner (assigned member's name), Tags
(comma-separated names), Created at (ISO8601), then one column per custom field, titled with its label, in field
order.
Name,Email,Phone,Address,Note,Type,Status,Owner,Tags,Created at,INN,Delivery address
Acme Windows LLC,[email protected],+1 555 010 2030,"12 Industrial Rd, Springfield",,client,active,Anna Petrova,Wholesale,2026-08-14T07:12:00.000Z,7701234567,"Warehouse 4, gate B"
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project does not exist or is not accessible |
Contact Field Object
A custom field defined for all contacts of the project. Its values live in each contact's meta.
Contact Field Structure
| Field | Type | Description |
|---|---|---|
| name | string | Key in meta, unique within the project: lowercase letters, digits and _, up to 64 characters. Cannot be changed |
| label | string | Display name, up to 128 characters |
| type | string | text, number, date, datetime or select. A hint for input UIs; values are always strings |
| options | array | Choices for select fields |
| is_required | boolean | Marks the field as required in the app. Not enforced by the API |
| order | integer | Sort position |
| created_at | ISO8601 datetime | When the field was created |
| updated_at | ISO8601 datetime | When the field was last changed |
Example Contact Field
{
"name": "delivery_address",
"label": "Delivery address",
"type": "text",
"options": [],
"is_required": false,
"order": 2,
"created_at": "2026-08-14T07:00:00.000Z",
"updated_at": "2026-08-14T07:00:00.000Z"
}
List Contact Fields
GET /api/v1/projects/{project.uuid}/contacts/fields
Returns the project's contact fields, sorted by order. Any project member can list them.
Create Contact Field
POST /api/v1/projects/{project.uuid}/contacts/fields
Requires can_edit_project.
JSON Params
| Field | Type | Description |
|---|---|---|
| name | string | Key, ^[a-z0-9_]+$, 1–64 characters |
| label | string | 1–128 characters |
| type? | string | One of the types above. Default text |
| options? | array | Choices for select. Default [] |
| is_required? | boolean | Default false |
| order? | integer | Sort position. Default: after the last field |
Example Request
{
"name": "source",
"label": "Lead source",
"type": "select",
"options": [
"Website",
"Exhibition",
"Referral"
]
}
Response
201 Created
{
"name": "source"
}
Modify Contact Field
PATCH /api/v1/projects/{project.uuid}/contacts/fields/{field.name}
Updates any of label, type, options, is_required, order. At least one must be present.
Requires can_edit_project.
Response
204 No Content
Replace Contact Field
PUT /api/v1/projects/{project.uuid}/contacts/fields/{field.name}
Same as Modify Contact Field, but label, type, options, is_required and order are
all required.
Requires can_edit_project.
Response
204 No Content
Delete Contact Field
DELETE /api/v1/projects/{project.uuid}/contacts/fields/{field.name}
Deletes the field, all contacts' values for it, and the requirement for it on any order status.
Requires can_edit_project.
Response
204 No Content
Contact Field Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema |
| 400 | REQ_NO_DATA_PROVIDED |
Modify only: the body has no fields |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects |
| 403 | AUTH_NO_PROJECT_ACCESS |
Create, modify, replace, delete: the user's role lacks can_edit_project |
| 404 | RES_NOT_FOUND |
The project or field does not exist |
| 409 | RES_ALREADY_EXISTS |
Create only: a field with this name already exists |
Tag Object
A label that can be put on contacts. Tag names are unique within the project.
Tag Structure
| Field | Type | Description |
|---|---|---|
| uuid | uuid | Tag ID |
| name | string | Name, 1–64 characters, unique within the project |
| color | string | Colour name, see below |
| created_at | ISO8601 datetime | When the tag was created |
The app's palette is gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky,
blue, indigo, violet, fuchsia, purple, pink, rose. The API accepts any string of up to 16 characters,
but the app may not display other values correctly.
Example Tag
{
"uuid": "0199a1aa-0001-7000-8000-000000000001",
"name": "Wholesale",
"color": "emerald",
"created_at": "2026-08-11T09:04:00.000Z"
}
List Tags
GET /api/v1/projects/{project.uuid}/tags
Returns the project's tags, sorted by name. Any project member can list tags.
Create Tag
POST /api/v1/projects/{project.uuid}/tags
Requires can_edit_contacts.
JSON Params
| Field | Type | Description |
|---|---|---|
| name | string | 1–64 characters, trimmed |
| color? | string | Up to 16 characters. Default blue |
Response
201 Created
{
"uuid": "0199a1aa-0001-7000-8000-000000000001"
}
Modify Tag
PATCH /api/v1/projects/{project.uuid}/tags/{tag.uuid}
Updates name and/or color. At least one must be present.
Requires can_edit_contacts.
Response
204 No Content
Delete Tag
DELETE /api/v1/projects/{project.uuid}/tags/{tag.uuid}
Deletes the tag and removes it from every contact.
Requires can_edit_contacts.
Response
204 No Content
Tag Errors
| Status | Code | When |
|---|---|---|
| 400 | REQ_VALIDATION_FAILED |
The body does not match the schema |
| 400 | REQ_NO_DATA_PROVIDED |
Modify only: the body has no fields |
| 400 | RES_ALREADY_EXISTS |
Create, modify: a tag with this name already exists |
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or contacts |
| 403 | AUTH_NO_PROJECT_ACCESS |
Create, modify, delete: the user's role lacks can_edit_contacts |
| 404 | RES_NOT_FOUND |
The project or tag does not exist |