Try for freeTry free

emyCRM API

Ready-made requests for integrations: companies, contacts, leads and deals, products, invoices, custom fields, activities, calls, messenger. Create a token in your workspace — Settings → API & apps → “Create token” (it takes the “Issue API tokens to employees” permission) — and use it instead of <TOKEN>; replace <your-workspace>.emycrm.com with the address of your workspace. A token acts on behalf of its owner — with their permissions and visibility scope.

Download OpenAPI (JSON)Import it into Postman or Insomnia, or generate a client.

Basics#

Every request goes to https://<your-workspace>.emycrm.com/api/v1/… with the authorization header; for POST/PATCH add the JSON header. Check your access:

curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/?page_size=5"

A token has no expiry: it works until it is revoked on the “API tokens” page or its owner is deactivated (then the API answers 401). A token can be limited to one part of the API — such a token gets 403 everywhere else; tokens created on the page have full access. A few actions are closed to tokens altogether: merging companies, work-day check-in / check-out, renaming built-in fields.

Rate limits: up to 150 requests per 10 seconds per token and up to 300 per 10 seconds for all tokens of the workspace combined. Above that the API answers 429 Too Many Requests with a Retry-After header (seconds) — wait that long and retry the same request. For bulk syncs fetch pages with ?page_size= instead of one record per request, and pause between batches. These are the default limits; your workspace may have its own.

Conventions: pages, formats, errors#

Pages

Lists come in pages: {count, next, previous, results: […]}, 50 records per page by default, ?page= picks a page. On the main lists — companies, contacts, leads, deals, invoices, products, tasks — ?page_size= goes up to 100: a larger value is cut down to 100, and a page number past the end returns the last page. Elsewhere page_size may be ignored, so always walk the pages by following next until it is null. Custom field definitions and sections come in one page with everything. Product variants, invoice currencies and invoice line items come as a plain array, without the wrapper.

{
  "count": 137,
  "next": "https://<your-workspace>.emycrm.com/api/v1/companies/?page=2",
  "previous": null,
  "results": [{"id": 15, "name": "Acme", …}, …]
}

Formats

Date-times are ISO 8601 in UTC: 2026-07-03T09:30:00Z; send them with a time zone (Z or +05:00) — without one the value is taken as UTC. Dates are 2026-07-03. Money and other decimals come as strings ("250000.00"); you may send strings or numbers. Links to other records are ids (numbers).

Errors

400 — validation: {"field": ["message"]} or {"detail": "message"}. 401 — no token, a wrong token, or its owner is deactivated. 403 — no permission for the action, or the token is limited to another part of the API. 404 — no such record, or it is outside the owner’s visibility scope; if the owner can see a record but may not edit it, PATCH and DELETE answer 404 as well. 402 — the workspace is frozen for non-payment: reading works, writing does not ("code": "tenant_frozen"). 409 — a conflict (for example, converting a disqualified lead). 413 — the file is too large. 423 — the token owner is on a break: working actions are paused ("code": "break_active"). 429 — the rate limit, see above.

# 400 — what was wrong, per field
{"stage": ["Stage does not belong to the chosen pipeline."]}

# 429 — wait Retry-After seconds and repeat the same request
HTTP/1.1 429 Too Many Requests
Retry-After: 4

Message texts come in the token owner’s language — their personal language, else the workspace language, else Russian; Accept-Language is not used. Build your logic on status codes and field names, not on the wording.

Calling from a browser, retries

The API is for server-to-server calls: a browser on another site cannot call it (cross-origin requests are closed). To send website forms straight from the page, use the Site Connector app — it is built for that and does not need a token in the page.

There is no idempotency key: repeating a POST that has already gone through creates a second record. Before retrying a create after a timeout, look the record up first (by phone — see “Finding duplicates” below). There is no bulk create or update — send one record per request and mind the rate limits.

Companies, contacts, leads, deals#

The four main CRM records work the same way: a list with ?search=, GET …/<id>/, POST to create, PATCH to change, DELETE to delete (deleted records go to the Trash, where an admin can restore them for 30 days). All fields of a record, custom ones included — GET /api/v1/companies/schema/ (and contacts, leads, deals). A name may be left empty: the record then gets an automatic name with its id. A workspace can make more built-in fields required — then the API answers 400 naming the field.

Phones, e-mails, websites

A company, a contact and a lead can have several of each. Send them as arrays of {value, label}; label is free text up to 16 characters and may be omitted; a website may go without https://:

"phones":   [{"value": "+998 90 123 45 67", "label": "mobile"}, {"value": "+998 71 200 00 00"}],
"emails":   [{"value": "[email protected]", "label": "work"}],
"websites": [{"value": "acme.uz"}]

An array replaces the whole list: to add a number, send the current numbers plus the new one. The first item is also copied into the single fields phone / email / website (kept for older integrations). In responses every item is {id, value, label, order, is_primary} (leads — without is_primary).

Read, change, delete

curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/companies/15/"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/contacts/8/"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/leads/77/"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/124/"

# change only what you send (an array — phones, e-mails — replaces the whole list)
curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"phones": [{"value": "+998 90 123 45 67"}, {"value": "+998 71 200 00 00"}]}' \
  "https://<your-workspace>.emycrm.com/api/v1/companies/15/"

# delete — the record goes to the Trash for 30 days
curl -X DELETE -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/124/"

Owner

owner is an employee id (GET /api/v1/people/employees/?search=…), not a user account id. If you do not send it, the token owner becomes the owner.

Companies

# search by name, legal name, tax id, e-mail or phone (any format)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/companies/?search=901234567"

# create a company with two phones and a contact person
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "name": "Acme",
    "legal_name": "Acme Trading LLC",
    "tax_id": "301234567",
    "address": "Tashkent, Amir Temur 1",
    "phones": [{"value": "+998 90 123 45 67", "label": "mobile"}, {"value": "+998 71 200 00 00", "label": "office"}],
    "emails": [{"value": "[email protected]"}],
    "owner": 3,
    "new_contacts": [{"full_name": "Aziz Karimov", "phone": "+998 90 765 43 21"}]
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/companies/"
# → 201 {"id": 15, "name": "Acme", "owner": 3,
#        "phones": [{"id": 31, "value": "+998 90 123 45 67", "label": "mobile", "order": 0, "is_primary": true}, …],
#        "contacts": [{"id": 8, "full_name": "Aziz Karimov", …}], "extra_fields": {}, …}

new_contacts creates contact persons together with the company; to link existing contacts instead, send "contact_ids": [8, 9]. Custom fields go into extra_fields, see “Custom fields” below.

Contacts

# contacts of a company
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/contacts/?company=15"

# create a contact and link it to company 15
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "full_name": "Aziz Karimov",
    "phones": [{"value": "+998 90 765 43 21"}],
    "emails": [{"value": "[email protected]"}],
    "birthday": "1990-05-12",
    "company": 15
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/contacts/"

company links the contact to that company (on PATCH it replaces the link). The response lists the links in company_links.

Leads

# a request from a website form
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "title": "Request from the website",
    "source": "website",
    "contact_name": "Aziz Karimov",
    "contact_phone": "+998 90 765 43 21",
    "contact_email": "[email protected]",
    "company_name": "Acme",
    "description": "Wants a quote for 20 desks",
    "value": "5000000.00",
    "currency": "UZS"
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/leads/"

status: new | in_progress | qualified | disqualified | converted (converted is set by the conversion below). Leads have no pipeline or stage — the status plays that role. source is free text up to 120 characters. Always send currency: without it the lead gets USD.

Lead → deal

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"pipeline": 1, "stage": 2}' \
  "https://<your-workspace>.emycrm.com/api/v1/leads/77/convert/"
# → {"lead": {…, "status": "converted", "converted_deal": 124}, "deal": {…}}

Everything in the body is optional. Without pipeline / stage the default pipeline and its first open stage are used. A company and a contact are created from the lead’s data; to attach existing ones pass company / contact ids, to skip them — "create_company": false / "create_contact": false; "create_deal": false converts without a deal. Repeating the call on a converted lead is safe — it returns the same result; a disqualified lead answers 409.

Pipelines and stages

# deal pipelines, each with its stages
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/sales/pipelines/?entity=deal"

A pipeline is {id, name, is_default, stages: […]}, a stage — {id, name, order, probability, kind, required_field_codes}. kind: open | won | lost. required_field_codes — custom fields that must be filled before a deal can enter the stage (otherwise 400 naming them).

Deals

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "title": "20 writing desks",
    "pipeline": 1,
    "stage": 2,
    "value": "5000000.00",
    "currency": "UZS",
    "company": 15,
    "contacts": [8],
    "expected_close_at": "2026-08-15"
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/deals/"

pipeline and stage are required, and the stage must belong to the pipeline. Always send currency: without it the deal gets USD. Instead of ids you can create the company or the contact in the same request: "new_company": {"name": "…", "phone": "…"}, "new_contact": {"full_name": "…", "phone": "…"}.

closed_at is when a won or lost deal was closed. Moving the deal into a won or lost stage sets it to the current time; to change it (say, for deals brought over from another CRM) send a day — "closed_at": "2024-05-12" keeps the time of day, in the account’s time zone — or an exact time. An open deal has no close date, and a date in the future is rejected (400). closed_on returns the close day in the account’s time zone.

# deals of a company / in a stage
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/?company=15"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/?pipeline=1&stage=2"

# move to another stage of the same pipeline
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"stage": 3}' "https://<your-workspace>.emycrm.com/api/v1/deals/124/move/"

status (open / won / lost) cannot be set directly — it follows the stage: to close a deal as won, move it to a stage with kind won. To move a deal to another pipeline, PATCH {"pipeline": …, "stage": …} together.

Finding duplicates

# which companies / contacts already have these numbers
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"phones": ["+998 90 123 45 67"], "match": "tail"}' \
  "https://<your-workspace>.emycrm.com/api/v1/duplicates/phones/"
# → {"companies": [{"id": 15, "name": "Acme", "matched_phone": "+998 90 123 45 67"}], "contacts": […]}

# the first record with this number: a contact, a company or a lead
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/telephony/lookup/?phone=%2B998901234567"
# → {"kind": "company", "id": 15, "name": "Acme", …}   or {} if nothing matches

match: exact (default) compares all digits, ignoring spaces, brackets and dashes; tail compares the last 9 digits and finds the number even when the country code was written differently. The search covers the whole workspace, not only the records visible to the token owner (the answer has only ids and names); leads are not checked there — telephony/lookup checks them.

Filtering, sorting, lighter responses#

Simple filters

Every list takes ?search=. Shortcuts: deals — ?company=, ?pipeline=, ?stage=, ?contact=, ?lead=; contacts — ?company=; invoices — ?company=, ?deal=; products — ?section=; tasks — see “Activities”.

Any condition: ?filter=

?filter= is a JSON array of conditions {"field", "op", "value"}, encoded as base64url (the URL-safe variant: - and _ instead of + and /, padding may be dropped). All conditions must hold (AND). Works on companies, contacts, leads, deals, invoices, products, tasks and custom entity records.

# deals of pipeline 1 worth more than 1 000 000, changed since 1 October
FILTER=$(printf '%s' '[{"field":"pipeline","op":"eq","value":1},{"field":"value","op":"gt","value":1000000},{"field":"updated_at","op":"gte","value":"2026-10-01T00:00:00Z"}]' | base64 | tr '+/' '-_' | tr -d '=\n')
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/?filter=$FILTER"
# Python
import base64, json
f = base64.urlsafe_b64encode(json.dumps(conditions).encode()).decode().rstrip("=")

// Node.js
const f = Buffer.from(JSON.stringify(conditions)).toString("base64url");

Operators: eq; in — a list of values; icontains — text contains, case-insensitive; gt / gte / lt / lte; between — [from, to], one side may be null; is_null; is_empty / is_not_empty — for text and custom fields. The last three take no value.

Fields — companies: id, name, legal_name, tax_id, phone, email, website, address, notes, owner, created_by, created_at, updated_at, last_activity_at, last_call_at. Contacts: id, full_name, phone, email, notes, owner, created_by, created_at, updated_at. Leads: id, title, status, source, value, currency, company_name, contact_name, contact_phone, contact_email, description, owner, created_by, created_at, updated_at. Deals: id, title, status, value, currency, company, pipeline, stage, notes, owner, created_by, expected_close_at, closed_at, created_at, updated_at. The operators each field accepts are listed in the 400 answer if you pick a wrong one.

Custom fields: "field": "extra:<code>". Text — eq, icontains; number, money, date — eq, gt…lte, between; select — eq, in, icontains; multiselect, employee and record links — eq, in; checkbox — eq; any — is_empty, is_not_empty.

With a token the filter is strict: an unknown field, a wrong operator or a value of the wrong type answers 400 with the list of problems. The interface silently drops such conditions; an integration must not — otherwise a typo returns the whole database.

Sorting: ?ordering=

?ordering=updated_at, - for descending, several fields separated by commas. The main lists sort by id, created_at, updated_at, owner and their own main fields (name, title, value, status, stage…).

Lighter responses: ?fields=

?fields=id,name,phones returns only these keys — on reads, lists and writes. Works on companies, contacts, leads, deals, invoices, products, tasks and employees.

All ids under a filter

curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/companies/filter-ids/?filter=$FILTER"
# → {"ids": [15, 16, …], "count": 2140, "capped": false}

The same ?search= / ?filter= / shortcuts as the list, no pages, up to 100 000 ids (capped: true — narrow the filter). Companies, contacts, leads, deals, invoices, products, custom entity records.

Getting changes (no webhooks)#

emyCRM does not send webhooks about CRM changes (“a deal was created”, “the stage changed”). Pick changes up by polling: remember when you last synced and ask for the records changed since then, oldest change first.

# deals changed since the last sync (the time you stored last time)
FILTER=$(printf '%s' '[{"field":"updated_at","op":"gte","value":"2026-10-23T09:00:00Z"}]' | base64 | tr '+/' '-_' | tr -d '=\n')
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/deals/?filter=$FILTER&ordering=updated_at&page_size=100"

Walk all pages (follow next), then store the largest updated_at you saw as the new mark. gte may return the last record once more — match records by id. Polling every 1–5 minutes is plenty and stays far below the rate limits. Deleted records simply stop appearing; to notice deletions, compare your ids with …/filter-ids/ from time to time.

Products#

List and search

# all products (one page)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/?page_size=50"

# search by name / SKU / description / catalog section
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/?search=desk"

# products of a catalog section + sort by price
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/?section=3&ordering=-price"

Single product

curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/42/"

Create

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "name": "Writing desk",
    "sku": "ST-100",
    "kind": "product",
    "price": "250000.00",
    "currency": "UZS",
    "unit": "pcs",
    "vat_rate": "12",
    "price_includes_vat": true,
    "purchase_price": "180000.00",
    "description": "Oak, 120x60"
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/products/"

kind: product | service. vat_rate: null = “VAT exempt” — but on create a null or missing vat_rate takes the workspace’s default VAT (and its “price includes VAT” setting) when one is set. section: catalog section id (optional). Custom fields go as an object "extra_fields": {"code": value}; look up field codes in GET /api/v1/products/schema/.

Update / delete

curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"price": "270000.00"}' "https://<your-workspace>.emycrm.com/api/v1/products/42/"

curl -X DELETE -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/42/"

Product variants

# variants of product 42
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/product-variants/?product=42"

# add a variant (an empty price is inherited from the product — see effective_price)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"product": 42, "name": "White / 140 cm", "sku": "ST-100-W", "price": "260000.00", "attributes": {"color": "white"}}' \
  "https://<your-workspace>.emycrm.com/api/v1/product-variants/"

Cover image

# upload / replace (JPEG, PNG, WEBP, GIF up to 10 MB)
curl -X POST -H "Authorization: Token <TOKEN>" -F "[email protected]" "https://<your-workspace>.emycrm.com/api/v1/products/42/image/"

# remove the image
curl -X DELETE -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/products/42/image/"

Invoices#

List and filters

# search by number / title / currency
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/invoices/?search=INV-2026"

# invoices of a company / a deal
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/invoices/?company=15"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/invoices/?deal=7"

Create

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "title": "Furniture invoice",
    "status": "issued",
    "currency": "UZS",
    "company": 15,
    "contact": 8,
    "due_at": "2026-07-31"
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/invoices/"

status: draft | issued | paid | overdue | cancelled. Relations are ids: company / contact / deal / lead / owner. The total (amount) can be sent right away, or you can add line items — then it is recalculated automatically. An empty number (number) is filled in by auto-numbering. Custom fields — extra_fields (codes: GET /api/v1/invoices/schema/).

Currencies (reference list)

currency is a currency code, up to 8 characters (UZS, KZT, USD, EUR, BYN, UAH…). The available currencies come from a managed reference list; if the workspace made the currency required, only active codes from that list are accepted.

# active currencies of the reference list (?all=1 — switched-off ones too)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/invoice-currencies/"

# add a new currency (workspace admin permissions required)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"name": "Euro", "code": "EUR"}' \
  "https://<your-workspace>.emycrm.com/api/v1/invoice-currencies/"

name — the label in the dropdown (required); code — the value stored in the invoice (optional: falls back to name). Reading the list — any token; adding and switching off — workspace admin only. There is no DELETE: a currency is switched off with PATCH {"is_active": false} on /api/v1/invoice-currencies/<id>/; the last active currency cannot be switched off.

Change status / delete

curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"status": "paid", "paid_at": "2026-07-05"}' "https://<your-workspace>.emycrm.com/api/v1/invoices/123/"

curl -X DELETE -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/invoices/123/"

Invoice line items

One request replaces the whole set of line items and recalculates the invoice total. product / variant are optional — a line may be free-form (for example, “Delivery”).

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "entity_type": "invoice",
    "entity_id": 123,
    "items": [
      {"product": 42, "name": "Writing desk", "unit": "pcs", "quantity": "2", "price": "250000.00", "vat_rate": "12"},
      {"name": "Delivery", "quantity": "1", "price": "50000.00"}
    ]
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/line-items/bulk/"

# current line items of the invoice
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/line-items/?entity_type=invoice&entity_id=123"

Generate a document for an invoice

# list of document templates
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/documents/templates/?entity=invoice&active=1"

# generate (format: pdf | docx) → the response contains download_url
# optional "vat_clause": "incl" | "excl" picks the {Items.VatClause} text when the
# VAT clause is on in document settings; omitted → decided by the line items
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"invoice": 123, "format": "pdf"}' \
  "https://<your-workspace>.emycrm.com/api/v1/documents/templates/5/generate/"

Custom fields: creating them via the API#

Fields belong to a specific entity: entity_type = company | contact | lead | deal | invoice | product | task | user (the employee card), or the slug of a custom entity. The token owner needs the “Custom fields” permission (customfield.manage); reading the list of fields is open to anyone.

Step 1 — get a field code

The code is the stable key of the value inside extra_fields. Taking it from the counter is safer (it never reuses numbers of deleted fields); your own slug also works — Latin letters, digits, underscores and hyphens, unique within the entity.

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"entity": "product"}' \
  "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/next-code/"
# → {"code": "product_7"}

Step 2 — create the field

# a number field on products, shown as a list column and in the filter
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "entity_type": "product",
    "code": "product_7",
    "label": "Warranty, months",
    "kind": "number",
    "show_in_list": true,
    "show_in_filter": true
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/"

# a dropdown on invoices (select/multiselect require options)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "entity_type": "invoice",
    "code": "invoice_4",
    "label": "Payment method",
    "kind": "select",
    "options": [
      {"value": "cash", "label": "Cash"},
      {"value": "card", "label": "Card"},
      {"value": "transfer", "label": "Bank transfer"}
    ]
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/"

Types (kind): text, textarea, number, select, multiselect, date, datetime, checkbox, url, email, phone, money, file, user_ref (employee), crm_ref (link to a CRM record — crm_ref_entity is then required as well). Other fields: is_required, is_multiple, section, order, default_value, help_text.

List / update / delete fields

# all fields of an entity
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/?entity=product"

# rename / extend the options
curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"label": "Warranty (months)"}' \
  "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/15/"

curl -X DELETE -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/customfields/definitions/15/"

Step 3 — write a value into a record

curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"extra_fields": {"product_7": 24, "product_3": "red"}}' \
  "https://<your-workspace>.emycrm.com/api/v1/products/42/"

Value formats by type: text/url/email/phone — a string; number — a number; checkbox — true/false; date — “2026-07-03”; datetime — ISO; select — the value of an option; multiselect — a list of values; money — {"amount": "1000.00", "currency": "UZS"}; user_ref — an employee id; crm_ref — a record id; file — the object returned by POST /api/v1/customfields/upload/ (multipart, field file): {"key": "customfields/…", "name": "contract.pdf"}.

select / multiselect: a value that is not among the options does not reject the request — the field falls back to its default value (if that one is still in the list) or stays empty, and the record is saved. For multiselect, unknown items are simply dropped. The exception is a required field: if it ends up empty after the fallback, you get a 400. The current list of values is in the field definition (GET /api/v1/customfields/definitions/?entity=…) or in GET /api/v1/<entity>/schema/ for companies, contacts, leads, deals, invoices and products.

Unknown field names: a key the server does not recognise (a typo, or a field that has since been deleted) does not reject the request either — the record is saved without it. So that the loss is not silent, the write response then carries "ignored_fields": ["industri", …] listing exactly what was skipped. The key is present only when there is something to report, and only in responses to POST / PUT / PATCH — it never appears on reads. Applies to both wire formats: a top-level key and one inside extra_fields.

Field sections (groups on the card)

# create a section and put a field into it (field: "section": <section id>)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"entity_type": "product", "name": "Warranty and service"}' \
  "https://<your-workspace>.emycrm.com/api/v1/customfields/sections/"

curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/customfields/sections/?entity=product"

Activities: events and tasks#

“Activities” are events (calls, meetings; with a start and an end, attached to a record) and tasks (with a due date and a status). A planned activity is one that is not completed yet.

Events (calls / meetings)

# events attached to a company (calls, meetings, etc.)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/events/?related_object_type=company&related_object_id=15"

# only PLANNED (not completed yet) activities
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/events/?planned=1"

# only completed ones
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/events/?completed=1"

# within a date window (ISO 8601) + planned calls of a contact
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/events/?related_object_type=contact&related_object_id=8&planned=1&after=2026-07-01T00:00:00Z&before=2026-08-01T00:00:00Z"

Event fields: title, start_at / end_at, event_type (call_external — a call to a client, call_internal — an internal call, meeting — a meeting…), related_object_type / related_object_id, completed_at (empty = planned). Without the pair related_object_type + related_object_id the list returns only events where the token owner is the organizer or an attendee. Cancelled events are hidden — add include_cancelled=1. To complete an activity, set completed_at:

curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"completed_at": "2026-07-07T10:00:00Z"}' \
  "https://<your-workspace>.emycrm.com/api/v1/events/123/"

Tasks

# tasks of a company / a contact
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?related_company=15"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?related_contact=8"

# open tasks; or by status (several allowed)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?open=1"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?status=new&status=in_progress"

# by urgency
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?due=overdue"     # overdue (open ones)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?due=today"        # due today
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/tasks/?due=week"         # due within 7 days

# create a task
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"title": "Call back", "due_at": "2026-07-10T12:00:00Z", "related_company": 15}' \
  "https://<your-workspace>.emycrm.com/api/v1/tasks/"

Statuses: new, in_progress, waiting_review, snoozed, done, cancelled. Open (planned) ones are all but done and cancelled — ?open=1. due=today / week count from midnight UTC and include closed tasks. A new task is assigned to the token owner unless you send assignee. Filters: due=overdue|today|week|unscheduled, status, assignee, related_company / related_contact, due_after / due_before (ISO 8601).

Messenger: sending messages#

One endpoint covers both cases — a message to an existing chat and a direct message to an employee: POST /api/v1/chat/send/. The author is the token owner, as if they typed it themselves; delivery is identical to the interface (realtime + push notification).

Direct message to an employee

user_id is the employee id — the same one used everywhere in the CRM (the “Assignee” field, /team/member/<id>). The 1:1 chat is found or created automatically, so no setup is needed.

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"user_id": 8, "body": "Invoice 19/0408/4 has been paid"}' \
  "https://<your-workspace>.emycrm.com/api/v1/chat/send/"

Message to an existing chat

chat_id is the id of any chat — direct, group or channel. chat/send/ does not create groups or channels: create them in the interface once, then post by id.

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"chat_id": 12, "body": "Nightly sync finished: 42 records updated"}' \
  "https://<your-workspace>.emycrm.com/api/v1/chat/send/"

Pass exactly one address — chat_id or user_id, never both. The response is 201 with the created message plus chat_id (useful after a direct chat was created on the fly).

Finding the ids

# chats you are a member of — id, kind (direct/group/channel), title
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/chat/conversations/"

# employees — the id to use as user_id
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/people/employees/?search=Ivanov"

Attachments

Upload each file first (multipart, field name file), then cite the returned ids. body may be empty when there is at least one attachment.

# 1) upload → {"id": 77, …}
curl -X POST -H "Authorization: Token <TOKEN>" -F "[email protected]" "https://<your-workspace>.emycrm.com/api/v1/chat/attachments/"

# 2) send with the attachment
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"user_id": 8, "body": "Monthly report", "attachment_ids": [77]}' \
  "https://<your-workspace>.emycrm.com/api/v1/chat/send/"

Errors

400 — neither body nor attachment_ids; not exactly one address; a message to yourself. 403 — the token owner is not a member of that chat, or the chat’s posting rule does not let them write (for example, only admins post in a channel). 404 — no such chat or employee (or the employee has no account). Access rules are the same as in the interface: a token can only write where its owner can.

Calls#

# calls you can see: view=mine (default) | dept | all
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/telephony/calls/?view=all&direction=in&status=missed&started_after=2026-10-01T00:00:00Z"

# one call; a link to its recording (valid for 10 minutes)
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/telephony/calls/501/"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/telephony/calls/501/recording-url/"
# → {"url": "https://…", "download_url": "https://…", "ttl_seconds": 600}

view widens the list up to what the token owner’s telephony role allows; without it you get only the owner’s own calls. Filters: direction (in | out), status (answered | missed | no_answer | rejected | failed), employee, department, sip_account, started_after / started_before (date and time, ISO 8601 — a bare date is ignored), min_duration / max_duration (seconds), has_recording, has_transcript, sentiment, search (part of the number); several values — repeat the parameter. ordering: started_at or duration_seconds.

A call has direction, status, remote_number, started_at, ended_at, duration_seconds, employee, the linked record (related_company / related_contact / related_lead / related_deal), recording_status (none | pending | uploaded | failed) and recording_transcript. If the workspace also has calls from the mobile app, next is empty in this list — page through with ?page= until a page comes back shorter than the page size.

Logging a call made elsewhere

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "direction": "out",
    "status": "answered",
    "remote_number": "+998901234567",
    "started_at": "2026-10-23T09:30:00Z",
    "ended_at": "2026-10-23T09:34:10Z",
    "duration_seconds": 250,
    "related_company": 15,
    "call_token": "my-pbx-7f3a9c"
  }' \
  "https://<your-workspace>.emycrm.com/api/v1/telephony/calls/"

The call is recorded for the token owner and appears in the company’s feed. call_token (up to 64 characters) makes the request safe to repeat: a second POST with the same token updates the same call instead of creating another one.

Comments, feed and files#

Comments in a record’s feed

curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"entity_type": "deal", "entity_id": 124, "body": "Client confirmed the order"}' \
  "https://<your-workspace>.emycrm.com/api/v1/timeline/comment/"

# the feed of a record: comments, calls, tasks…
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/activities/?entity_type=deal&entity_id=124"

entity_type: company | contact | lead | deal | invoice | product, or the slug of a custom entity. The author is the token owner. Feed filters: kind (comment, call, task, note, callback — several separated by commas), search, after / before (ISO 8601), author (employee id). Only the author can edit or delete a comment.

Files

# 1) upload (multipart, field "file", up to 100 MB)
curl -X POST -H "Authorization: Token <TOKEN>" -F "[email protected]" "https://<your-workspace>.emycrm.com/api/v1/customfields/upload/"
# → 201 {"key": "customfields/…/contract.pdf", "name": "contract.pdf", "size": 48213, "content_type": "application/pdf"}

# 2a) attach to a comment (up to 10 files)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"entity_type": "deal", "entity_id": 124, "body": "Signed contract",
       "payload": {"attachments": [{"key": "customfields/…/contract.pdf", "name": "contract.pdf"}]}}' \
  "https://<your-workspace>.emycrm.com/api/v1/timeline/comment/"

# 2b) or put it into a “file” custom field
curl -X PATCH -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"extra_fields": {"deal_5": {"key": "customfields/…/contract.pdf", "name": "contract.pdf"}}}' \
  "https://<your-workspace>.emycrm.com/api/v1/deals/124/"

# download: redirects to a temporary link (valid for an hour)
curl -L -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/customfields/file/?key=customfields/…/contract.pdf"

Employees and departments#

# active employees, everyone on one page
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/people/employees/?is_active=true&page_size=all"

# by name, e-mail or phone; by department
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/people/employees/?search=Karimov"
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/people/employees/?department=4"

# departments
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/settings/departments/"

id is the employee id — the one used in owner, assignee, employee and in chat user_id; user is the login account id. Other fields: full_name, email, phone, position, internal_ext, departments (ids), manager, is_active. A department is {id, name, parent, head}.

Custom entities#

# entity types of the workspace: id, slug, name…
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/custom-entities/types/"

# records of one type — always pass ?entity=<slug>, otherwise all types are mixed
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/custom-entities/records/?entity=contracts"

# fields of the type (codes for "data")
curl -H "Authorization: Token <TOKEN>" "https://<your-workspace>.emycrm.com/api/v1/objects/schema/?entity=contracts"

# create a record: entity_type is the type id (a number, not the slug)
curl -X POST -H "Authorization: Token <TOKEN>" -H "Content-Type: application/json" \
  -d '{"entity_type": 3, "title": "Contract 2026/41", "data": {"contracts_2": "2026-11-01"}, "links": {"company": 15}}' \
  "https://<your-workspace>.emycrm.com/api/v1/custom-entities/records/"

data holds the custom field values by their codes, links — links to other records ({"company": 15}; only the kinds set up for this type). A new record starts in the first open stage. Lists take ?search=, ?filter=, ?ordering= (created_at, updated_at, title) and ?linked=company:15. Access follows the role’s rights for that entity.

Website forms and WordPress#

Forms from your website

For website forms you do not need a token or your own server: turn on the Site Connector app (Marketplace → Site Connector) and copy the address it shows. It works straight from the browser (cross-origin allowed) and creates a lead, a deal or a contact — as set in the app.

curl -X POST -H "Content-Type: application/json" \
  -d '{"name": "Aziz", "phone": "+998 90 765 43 21", "email": "[email protected]", "message": "Call me back", "source": "landing"}' \
  "https://<your-workspace>.emycrm.com/api/v1/site-connector/submit/?token=<SITE TOKEN>"
# → 201 {"ok": true, "id": 77, "type": "lead"}

JSON or an ordinary form post. Field names are recognised in several spellings (name / fio / contact_name, phone / tel, message / comment…); unknown fields are kept on the record. The token may go in the X-Site-Token header instead of ?token=. Up to 10 form submissions a minute from one IP. A repeat request from someone who already has an open lead is added to that lead. Orders from an online shop go to /api/v1/site-connector/order/ with an items list (matched to products by sku).

WordPress and WooCommerce

The ready plugin sends website forms and WooCommerce orders to the CRM: download it from /downloads/emycrm-wordpress.zip, turn on the WordPress app in the Marketplace and paste the connection key from it into WordPress → Settings → emyCRM.