# Solk Docs (English) — full content --- # Build with Solk > Everything you need to connect Solk CRM to your own software, automation tools, and AI assistants. Solk is a CRM built for B2B sales teams: companies, people, opportunities (sales projects), visits and phone calls, actions, calendar, fair leads, and email threads all live in one place. Each company has its own separate installation that runs at its own address (`https://.solk.app`); the examples in these docs use `https://ornek.solk.app`. The guides on this site show you how to build integrations, move data in and out, and connect the CRM to other tools. The reference sections list every endpoint's parameters, with sample responses taken from a real installation. ## Developer platform :::cards - [REST API](/rest-api/overview) code | Read and write records as JSON. The same endpoints the mobile app uses, with the same permission rules and the same stage gates. - [MCP connector](/mcp/overview) spark | Connect your CRM to AI assistants like Claude: ask questions, and create tasks and log calls right from the chat. - [Webhooks](/guides/webhooks) bolt | Receive signed JSON when an opportunity is won, a new company is created, or a form submission arrives. - [Inbound hooks](/guides/inbound) inbox | Send data into the CRM from forms, your phone system, your data warehouse, and scheduling tools. ::: ## Where to start :::cards - [Quickstart](/quickstart) rocket | Create your first API key and your first task in five minutes. - [Authentication](/rest-api/authentication) key | How API keys, OAuth, and mobile session tokens differ. - [Standard objects](/objects/companies) layers | The fields of companies, people, opportunities, and actions, and how they link together. - [Apps](/guides/apps) grid | Slack, Teams, Zapier, Google Drive, and more than 40 ready-made connections. ::: ## Core principles **One address per installation.** Each customer's data lives in its own server folder and its own database. You send API requests to your installation's address; there is no shared `api.solk.app`. **Runs with user permissions.** Every token belongs to a user. The records the API can see and the changes it can make are exactly what that user can see and do in the web interface: module permissions, record visibility, and edit rights all apply in the API too. **Business rules are never bypassed.** Stage gates when you change an opportunity's stage, duplicate protection when you create a company, the action chain when you log an activity — whatever happens in the web form happens in the API too. Your integration can't break the CRM's rules. **Turkish and English.** These docs are published in two languages. API field names are in English; user-facing error messages (`message`) and some value lists (task priorities, loss reasons) are in the installation's language, Turkish. :::note These docs cover Solk **v22**. To see your installation's version, call `GET /health` or check the `app.version` field of the [identity endpoint](/rest-api/auth/me). Changes are listed in the [changelog](/changelog). ::: ## For AI tools This entire site is also published in a machine-readable form: - [`/llms.txt`](/llms.txt) — a short index of every page - [`/llms-full.txt`](/llms-full.txt) — all content in a single file - [`/openapi.json`](/openapi.json) — the OpenAPI 3.1 definition of the REST API - Append `.md` to any page to get its Markdown version (e.g. [`/quickstart.md`](/quickstart.md)). --- # Quickstart > Create an API key, authenticate, create your first task, and subscribe to an event. By the end of this guide, you'll have an API key, you'll have read data from the CRM and created a task, and you'll have set up a webhook that notifies you when an opportunity is won. :::steps ### Create an API key Sign in to the CRM with an **admin** account and open the **Settings → Developers** (*Ayarlar → Geliştiriciler*) page (`https://ornek.solk.app/settings/developers`). Give the key a recognizable name (e.g. "Accounting sync") and click **Create key** (*Anahtar oluştur*). The key starts with `sk_` and is shown **only once**; save it to a password manager right away. The key runs with the permissions of the admin who created it and stays valid until it's revoked. :::warning An API key is like a password with full access. Don't put it in code that runs in the browser, in a mobile app, or in a public repository. For apps that act on behalf of a user, use [OAuth](/rest-api/oauth). ::: ### Authenticate Add the `Authorization: Bearer ` header to every request. Make [`GET /api/v1/me`](/rest-api/auth/me) your first call: it returns who the key belongs to, the enabled modules, and the installation's stage keys. :::code ```bash cURL curl https://ornek.solk.app/api/v1/me \ -H "Authorization: Bearer $SOLK_API_KEY" ``` ```javascript JavaScript const res = await fetch("https://ornek.solk.app/api/v1/me", { headers: { Authorization: `Bearer ${process.env.SOLK_API_KEY}` }, }); const me = await res.json(); console.log(me.user.full_name, me.constants.stages); ``` ```python Python import os, requests r = requests.get("https://ornek.solk.app/api/v1/me", headers={"Authorization": f"Bearer {os.environ['SOLK_API_KEY']}"}) me = r.json() print(me["user"]["full_name"], me["constants"]["stages"]) ``` ::: `ok: true` in the response means the request succeeded. Failed requests return `ok: false`, a machine-readable `error` code, and a `message` you can show to the user — see [Errors](/rest-api/errors) for details. ### Read records Fetch open opportunities sorted by value: ```bash curl "https://ornek.solk.app/api/v1/opportunities?scope=all&sort=value&per=10" \ -H "Authorization: Bearer $SOLK_API_KEY" ``` List endpoints return `rows`, `total`, `page`, and `pages`. To get a single record in full, request it by ID: [`GET /api/v1/opportunities/{id}`](/rest-api/deals/get). ### Create a task ```bash curl https://ornek.solk.app/api/v1/tasks \ -H "Authorization: Bearer $SOLK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Fiyat listesini gönder", "customer": "Kuzey Plastik Sanayi", "due_date": "2026-10-05", "due_time": "10:30", "priority": "Yüksek" }' ``` The task is assigned to the key's owner (use `assigned_to` to assign it to someone else). The company is matched by name; no project is created automatically. The task shows up immediately in the CRM's **Actions** (*Aksiyonlar*) list and in its owner's phone calendar. ### Subscribe to events Open **Settings → Apps → Webhook** (*Ayarlar → Uygulamalar → Web kancası*), enter your receiving URL, and select the **Opportunity won** (*Fırsat kazanıldı*) event. When the event occurs, the CRM sends signed JSON to your URL: ```json { "event": "opp_won", "title": "Fırsat kazanıldı", "text": "Kuzey Plastik Sanayi · Streç film tedariki · 338.400 € · Sorumlu: Deniz Aksoy", "url": "https://ornek.solk.app/opportunities/53", "data": { "id": 53, "rid": "006Xq3…", "name": "Streç film tedariki", "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "stage": "Win", "stage_label": "Win", "value": 338400.0, "currency": "EUR", "owner": "Deniz Aksoy" }, "workspace": "Örnek Kimya", "app": "Solk CRM", "sent_at": "2026-10-02T14:05:11" } ``` To learn how to verify the signature, see [Webhooks](/guides/webhooks). ::: ## Next steps :::cards - [Authentication](/rest-api/authentication) key | Key types, permissions, and revocation. - [Opportunities](/rest-api/deals/list) target | Stage changes, BANT, steps. - [Log an activity](/rest-api/activities/log) phone | Log a call or visit in a single request. - [MCP](/mcp/overview) spark | Do the same things from Claude in natural language. ::: --- # Installations and addresses > Each company's Solk installation runs at its own address. The API, MCP, hook, and discovery URLs are all derived from it. Instead of "workspaces", Solk has **installations**: each customer company gets its own subdomain, its own database, and its own settings. There is no shared API server; you send requests directly to the installation's address. | What | Address | |---|---| | Web app | `https://.solk.app` | | REST API | `https://.solk.app/api/v1/…` | | MCP connector | `https://.solk.app/mcp` | | OAuth discovery | `https://.solk.app/.well-known/oauth-authorization-server` | | SCIM 2.0 | `https://.solk.app/api/scim/v2` | | Inbound hooks | `https://.solk.app/in//` | | Version and health | `https://.solk.app/health` | | Calendar (CalDAV) | `https://.solk.app/radicale/` | If you don't know the installation address, ask your users: it's the address they open the CRM at in their browser. The [solk.app](https://solk.app) sign-in page redirects to the right installation when you type the company name. ## Version All installations are updated from the same codebase, but because each one is updated on its own schedule, two installations may briefly run different versions. To find out which version is running: ```bash curl https://ornek.solk.app/health ``` ```json { "ok": true, "version": "v22", "code": "bff893f1", "started": "2026-10-02T09:12:40", "restart_needed": false, "time": "2026-10-02T14:20:03", "schema_missing": {}, "last_backup": "2026-10-02", "last_sync": "2026-10-02T14:10" } ``` Checking the version before you use a new API field or endpoint keeps you from getting a `404` on an installation that's still on an older version. New fields are **added** to responses; the meaning of existing fields never changes, and they're never removed. ## License and modules Each installation's license determines which modules are enabled: Customers, Opportunities, Visits, Tasks, Calendar, Fair leads, Reports, Sales forecast, Support tickets, Contracts, Email automation, AI. Endpoints of a disabled module return `403 module`. The enabled modules are listed in the [`GET /api/v1/me`](/rest-api/auth/me) response under `license.modules` and, per user, `user.modules`. When the license expires, the installation stays open to admins only; API requests from other users get `403 license`. ## Time zone and date format Installations run on Turkey time (UTC+3). The API returns and expects dates as `YYYY-MM-DD` and date-times as `YYYY-MM-DDTHH:MM:SS` (local time) without a time zone suffix. In inbound hooks, you can send ISO 8601 (with a time zone offset) or a Unix timestamp; it's converted to local time. --- # Users and roles > Admin and sales roles, module permissions, record visibility, and how API tokens relate to them. Every API request runs on behalf of a user. This page covers the three layers that determine what that user can see and change. ## Roles {#roles} | Role | Key | What they can do | |---|---|---| | Admin | `admin` | All records, settings, users, apps, the Developers page, license, and invoices. | | Sales | `sales` | Their own records, plus whatever the visibility rule allows; settings pages are off-limits. | Only admins can create API keys (**Settings → Developers** (*Ayarlar → Geliştiriciler*)), and a key runs as the admin who created it. If you need an integration that runs with a specific sales rep's permissions, that user must grant access through [OAuth](/rest-api/oauth). ## Module permissions {#modules} Modules enabled in the license can be further restricted per user (**Settings → Users → user → Modules** (*Ayarlar → Kullanıcılar → kullanıcı → Modüller*)). Endpoints of a module the user doesn't have permission for return `403 module`: ```json { "ok": false, "error": "module", "message": "Bu modül lisansınızda / izinlerinizde yok.", "module": "fairs" } ``` Each endpoint's reference page states which module it belongs to. The identity, Today, search, notification, email, and Ask endpoints don't depend on any module. ## Record visibility {#visibility} **Settings → Security → Record visibility** (*Ayarlar → Güvenlik → Kayıt görünürlüğü*) selects one of two modes: - **Everyone** (*Herkes*) — sales reps see all companies, but can edit only their own records (or those with no owner). - **Team** (*Takım*) — sales reps see the companies of users in their own department and companies with no owner; companies in other departments, and records linked to them such as opportunities, tasks, and emails, don't appear in lists. The API applies this rule exactly: hidden records don't appear in lists, and requesting one by ID returns `403 forbidden` or `404 not_found`. The `scope` parameter on list endpoints (`mine` · `all` · a user ID) doesn't widen visibility; it only filters within what's already visible. ## User lifecycle - **Invite** — an admin invites the user by email, and the user sets their own password. To provision users automatically from your identity provider, use [SCIM](/rest-api/scim). - **Deactivate** — a deactivated user can't sign in; their mobile sessions and API keys are revoked immediately, and their OAuth grants become invalid. Their records and history aren't deleted. - **Seats** — if all seats in the license are taken, you can't create a new user or reactivate a deactivated one. ## Two-factor authentication and IP restrictions Users with two-factor authentication enabled must send the `otp` field to the [mobile sign-in](/rest-api/auth/login) endpoint. API keys and OAuth tokens don't ask for a second factor — they were already authorized by a signed-in user when they were created. If an admin has set an IP restriction on a user, requests made with that user's tokens are also accepted only from allowed networks (`403 ip`). --- # Companies > Customer and prospect companies — the core object every CRM record links to. A company (`customer` in the API) is the center of the CRM. People, opportunities, visits, trials, quotes, orders, contracts, support tickets, and email threads all link to a company. The company card shows all of them on one page; in the API, [`GET /api/v1/customers/{id}`](/rest-api/companies/get) returns the same content. ## Status | Value | Meaning | |---|---| | `Prospect` | Prospect — a company you haven't sold to yet. The default for new companies. | | `AC` | Active customer. A company automatically becomes `AC` when an opportunity is **won**. | Companies are never deleted; they're **deactivated** (`active: false`). Inactive companies drop out of lists and reminders, and their history is preserved. The API has no delete endpoint; `GET /api/v1/customers?active=0` lists inactive companies. Details: [Archiving and deletion](/concepts/archiving). ## Fields List endpoints return each company in the **row** format; the single-record endpoint returns the **card** format. The card includes every field in the row. | Field | Type | Description | |---|---|---| | `id` | integer | ID within the installation. | | `rid` | string | 15-character permanent record ID (`001…`). [Record IDs](/concepts/record-ids). | | `name` | string | Company name (up to 200 characters). | | `status`, `status_label` | string | `AC` / `Prospect` and its display name. | | `owner` | user | Owning sales rep (`id`, `full_name`, `role`). | | `city`, `sector`, `phone` | string | Contact details. | | `main_contact` | object | The main contact's name, phone, email, and WhatsApp number (`wa`). | | `visit_state` | string | Visit schedule: `overdue` overdue, `soon` coming up, `ok`, `none` no schedule. | | `last_visit`, `next_visit_due` | date | Date of the last visit and of the next scheduled visit. | | `open_opps` | integer | Number of open opportunities. | | `at_risk` | boolean | The customer is at risk (no visits or orders for a long time). | | `potential_kg`, `unit` | number, string | Monthly potential and the installation's base unit. [Units and currency](/concepts/units-currency). | | `currency` | string | The company's transaction currency. | | `pending_state` | string | Status of the most urgent open action. | The card also includes `address`, `website`, `note`, `supplier_note` (current supplier), `barrier` (obstacle), `mgmt_support`, `visit_period_days`, share fields (`share_start`, `share_current`, `share_target`), volume targets (`our_kg_current`, `our_kg_target`, `commit_kg`), `contacts`, `opps`, `visits`, `demos`, `offers`, `orders`, `open_actions`, `events` (timeline), `competitors`, `prices`, `threads`, and `can_edit`. ## Duplicate protection When you create a company, its name is compared against existing companies: - If the same name exists (ignoring case, company-type suffixes, and punctuation), you get `409 exists`, and the existing company is returned in the `customer` field. - If similar names exist, you get `409 similar`, and the candidates are returned in the `similar` list. Pick the right company, or send `confirm_new: true` to create the new one anyway. This rule is the same for [create a company](/rest-api/companies/create), [log an activity](/rest-api/activities/log), and inbound hooks (forms, data sync). ## Automatically created companies Email sync, form submissions, and the Segment and data sync hooks can create new companies. Companies created from email are identified by their domain (`auto_domain`) and show an "auto-created" (*otomatik açıldı*) banner on the company card; if the match is wrong, the user can undo it with one click. ## Related endpoints :::cards - [List companies](/rest-api/companies/list) list | Filtering, sorting, pagination. - [Create a company](/rest-api/companies/create) plus | Create with duplicate protection, together with the first contact. - [Update a company](/rest-api/companies/update) pencil | Partial update. - [Postpone a visit](/rest-api/companies/postpone) clock | Move the visit due date later. ::: --- # People > Contacts at companies — activities, emails, and quotes link to people. A person (`contact` in the API) always belongs to a company. Each company has one **main contact**: that's the person shown in the company list and behind the call / WhatsApp buttons in the mobile app. A company's first person automatically becomes its main contact. ## Fields | Field | Type | Description | |---|---|---| | `id` | integer | ID. | | `rid` | string | Permanent record ID (`003…`). | | `customer_id`, `customer` | integer, string | The company the person belongs to. | | `name` | string | Full name. | | `title`, `department` | string | Job title and department. | | `phone`, `email` | string | Contact details. | | `wa` | string | Only the digits of the phone number — for `https://wa.me/` links. | | `is_main` | boolean | The company's main contact. | | `is_former` | boolean | Has left the company. People who leave aren't deleted; they're hidden from lists, and their name stays on past activities. | | `note` | string | Short note (250 characters). | ## How people are created - The [Add a person](/rest-api/people/create) endpoint or the company card. - The `contact_*` fields when [creating a company](/rest-api/companies/create). - When you [log an activity](/rest-api/activities/log) with a name that doesn't exist at the company yet. - Email sync: senders of emails from the company's domain (automatically created people are flagged `auto` and don't send a `contact_created` event to apps). - Form submissions, Segment `identify` events, and the data sync hook. ## Matching by email Incoming emails and hooks match a person **by email address** first, and if none is found, by company + name. Avoid using the same email address at two companies; the match goes to the first person found. ## Related endpoints :::cards - [List people](/rest-api/people/list) list | People across all companies, with search. - [Add a person](/rest-api/people/create) plus | Add a person to a company. - [Update a person](/rest-api/people/update) pencil | Make main contact, mark as former. ::: --- # Opportunities > Sales projects — with stage, value, health, BANT, and checklist steps. An opportunity (`opportunity` in the API, `opp` for short) is a sales project you run with a company. The UI calls it a **Project** (*Proje*) or an **Opportunity** (*Fırsat*). Every opportunity belongs to a company, sits in a stage, and moves forward through stage gates until it closes. ## Stages Stages vary by installation (**Settings → Stages** (*Ayarlar → Aşamalar*)). New installations start with two open stages (*Aday* · *Devam ediyor*, meaning prospect and in progress); industrial installations such as Makro use five open stages and stage gates: `Qualify` → `Viable` → `Present Solution` → `Negotiation` → `Expect to Close` → `Win` / `Lost` / `Cancel` The closing keys are `Win`, `Lost`, and `Cancel` on every installation; their display names can differ. The installation's stage keys and names are returned in `constants.stages`, `constants.open_stages`, and `constants.stage_labels` in the [`GET /api/v1/me`](/rest-api/auth/me) response. Details: [Stages and gates](/concepts/pipeline). ## Account follow-up containers The ongoing relationship with won customers is kept in **Account Follow-up** (*Müşteri Takibi*) containers in the `Account` stage. These containers don't count toward the sales pipeline, and visits and orders link to them. Fetch them from the list with `stage=account`; they carry `is_account: true`. ## Fields | Field | Type | Description | |---|---|---| | `id`, `rid` | integer, string | ID and permanent record ID (`006…`). | | `name` | string | Display name (generated from the product and type if there's no name). | | `customer_id`, `customer` | integer, string | Company. | | `stage`, `stage_label` | string | Stage key and display name. | | `is_open`, `is_account` | boolean | Open opportunity · account follow-up container. | | `potential_kg`, `offer_price`, `payment_days` | number | Monthly volume, unit price, payment term. | | `unit`, `pu`, `cur_sym` | string | The product's unit, price unit (e.g. `€/kg`), and currency symbol. | | `value`, `value_short` | number, string | Annual value = volume × price × 12 (otherwise the manually entered value). | | `value_base`, `base_currency` | number, string | The value converted to the installation's base currency. | | `probability`, `prob`, `fc` | number, string | Probability (%) and forecast category. | | `health`, `health_color` | object, string | Health score and color (`g` green, `y` yellow, `r` red). | | `health_manual`, `health_note` | boolean, string | Whether the user set the color manually, and their note. | | `sla_days`, `sla_left`, `days_in_stage` | integer | Stage duration target, days left, days spent in the stage. | | `lifetime_left` | integer | Days left in the project's lifetime. | | `next_step`, `close_date` | string, date | Next step and expected close date. | | `bant` | integer | Number of BANT criteria met (0–4). | | `owner` | user | Owner. | The single-record endpoint also returns `steps` (checklist), `gate_ok`, `gate_problems`, `next_stage`, `bant_b/a/n/t`, `offers`, `visits`, `demos`, `orders`, `tasks`, `logs` (stage history), `notes`, `tags`, `threads`, and, if the opportunity is closed, `close_category`, `close_reason`, and `closed_at`. ## How value is calculated If a product, monthly volume, and unit price are set, the annual value is **volume × price × 12** and can't be edited manually. Otherwise, the manually entered value in the `value` field is used. Dashboards and totals are always calculated with `value_base`, in the base currency. ## Health The health score (0–100) is calculated automatically. Exceeding the stage SLA or the project lifetime, time since the last activity, the next step's date and postponements, a close date in the past, stage gate problems, and a missing quoted price during negotiation lower the score; activity in the last 7 days and a positive trial raise it. 75 and above is green, 50–74 is yellow, and anything lower is red. Users can pick the color manually and write a short note; a manually chosen color overrides the calculated one (`health_manual: true`). `sort=health` sorts the list with red opportunities first. ## Related endpoints :::cards - [List opportunities](/rest-api/deals/list) list | Stage, sorting, pagination. - [Change the stage](/rest-api/deals/stage) arrow | With stage gates enforced. - [Update an opportunity](/rest-api/deals/update) pencil | Volume, price, close date, forecast. - [Add a step](/rest-api/deals/steps-create) check | Checklist. ::: --- # Actions and tasks > Tasks, follow-up actions from visits and trials, and dated project steps in a single to-do list. A sales rep's to-dos come from four different kinds of records. The CRM shows them in a single list on the **Actions** (*Aksiyonlar*) page; in the API, [`GET /api/v1/actions`](/rest-api/actions/list) returns the same list. | Kind (`kind`) | Source | Example | |---|---|---| | `task` | Task — created manually, by a workflow, from a form, or by a hook. | "Send the price list" | | `visit` | The **next action** of a visit / phone call. | "Send quote · October 5" entered during the call | | `demo` | The next action of a trial (product trial). | "Get the trial result" | | `step` | A dated checklist step on an opportunity. | "Schedule a line trial" | Each row's `key` field has the form `kind:id` (e.g. `task:26`). The complete, postpone, and close endpoints use the path `/api/v1/actions/{kind}/{rid}/…`, so you can handle every action with the same code, whatever its kind. ## Status | `state` | Meaning | |---|---| | `overdue` | Past its due date. | | `soon` | Due in 7 days or less. | | `open` | Due later. | | `done` | Completed. | ## Chain When you close an action, you can open the next one ([Close and open next](/rest-api/actions/close)). This way, the work for a company forms a chain; the single-record endpoint returns the chain in the `chain_before` and `chain_after` fields. Logging an activity ([Log an activity](/rest-api/activities/log)) automatically closes matching open actions at the same company. ## Task fields | Field | Type | Description | |---|---|---| | `id`, `rid` | integer, string | ID and permanent record ID (`00T…`). | | `title`, `detail` | string | Title and details. | | `due_date`, `due_time` | date, string | Due date and optional time (`HH:MM`). Tasks with a time appear at that time in the calendar. | | `status` | string | `Açık` (open), `Devam Ediyor` (in progress), `Tamamlandı` (done). | | `priority` | string | `Düşük` (low), `Normal`, `Yüksek` (high), `Acil` (urgent). | | `assignee` | user | Assigned user. | | `customer_id`, `opp_id` | integer | Linked company and project (both optional). | | `result`, `completed_at` | string, datetime | Result note and completion time. | | `source_key` | string | The source that created the task (e.g. `form:3`, a workflow). | ## Completion date Completion endpoints accept a past date in `done_on`: when a user marks a task done today that they actually did yesterday, reports count it on the correct day. If you omit it, the current time is used. ## Notifications The assigned user gets an in-app notification and, depending on their preferences, an email as well. A digest email of overdue items goes out every morning; users choose which events trigger email or in-app notifications under **Settings → Notifications** (*Ayarlar → Bildirimler*). --- # Fair leads > Potential customers collected at a trade fair booth — quick capture, owner, next action, and conversion to a company. A fair lead (`lead` in the API) is the first record of a company you met at a trade fair. A lead isn't a company yet: once it's qualified and moved to the **Converted** (*Aktarıldı*) status, a company, a person, and, if needed, an opportunity are created (from the web interface). Fairs (`fair`) group leads; each fair has a name, city, venue, and dates. ## Status | `status` | Meaning | |---|---| | `Yeni` | New — captured at the booth. | | `İletişime geçildi` | Contacted — first contact made after the fair. | | `Teklif verildi` | Quote sent. | | `Aktarıldı` | Converted to a company (`customer_id` and `opp_id` are filled in). | | `Vazgeçildi` | Dropped — won't be followed up. | `status_eff` returns the displayed status: `Kazanıldı` (won) if the linked project was won, `Kaybedildi` (lost) if it was lost or canceled, and the stored status otherwise. ## Fields | Field | Type | Description | |---|---|---| | `id`, `rid` | integer, string | ID and permanent record ID (`00Q…`). | | `fair_id`, `fair` | integer, string | Fair. | | `company`, `contact_name`, `title`, `phone`, `email` | string | Company and contact details. | | `city`, `sector`, `website` | string | Company details. | | `visitor_type` | string | `Potansiyel müşteri` (potential customer), `Mevcut müşteri` (existing customer), `Tedarikçi` (supplier), `Rakip` (competitor), `Diğer` (other). | | `interest` | string | Interest: `Sıcak` (hot), `Ilık` (warm), `Soğuk` (cold). | | `products`, `products_text` | string | IDs of the products of interest (comma-separated) and free text. | | `potential_kg`, `unit` | number, string | Monthly potential and unit. | | `timing` | string | `Hemen` (immediately), `3 ay içinde` (within 3 months), `6 ay içinde` (within 6 months), `1 yıl+` (1 year+), `Belirsiz` (unknown). | | `next_action`, `next_date` | string, date | Next action and its date — creates a task for the owner. | | `owner` | user | The sales rep who will follow up on the lead. | | `customer_id`, `opp_id`, `task_id` | integer | Company and opportunity, if converted; follow-up task. | ## Owner and follow-up task The person who captures a lead and the person who follows it up can be different (`owner_id`). When a next action and date are entered, a task is created for the owner; when the lead is updated, the task is updated too. By default, sales reps see only their own leads (`owner=me`). ## Related endpoints :::cards - [List fairs](/rest-api/fairs/list) flag | Fairs and their lead counts. - [Add a lead](/rest-api/fairs/leads-create) plus | Quick capture at the booth. - [Update a lead](/rest-api/fairs/leads-update) pencil | Status and fields. ::: --- # Email threads > Threads synced from users' connected mailboxes, and how they link to records. When users connect their Gmail / Google Workspace, Microsoft 365, or company mail (IMAP/SMTP) accounts, their emails sync to the CRM. Messages on the same subject are grouped into a **thread** (`thread`). A thread links to a company and, optionally, to a project, and appears in the Emails section on the cards of the related records. ## Linking rules 1. If the other party's email address matches a person's, the thread links to that person's company. If it doesn't, the company is found from the domain, or (depending on the organization's rules) a prospect company and person are created automatically. 2. The project is chosen from these sources, in order: the reply chain, the first message sent from the CRM, the company's only open project, a quote / project number in the subject, and the company's most recent project (as a suggestion). The reason for the choice is in the `opp_how` field. 3. Users can move a thread to another project, link it to a visit / trial / task / quote / ticket / contract, or dismiss the suggestion ([Link a thread to a record](/rest-api/email/threads-action)). Noise such as newsletters, notifications, and auto-replies isn't synced. On the **Email account** (*E-posta hesabı*) page, admins can use the **Organization email rules** (*Kuruluş e-posta kuralları*) card to skip certain domains entirely or to stop creating people from them. ## Privacy The mailbox owner chooses how it's shared: **full** (*tam*), where the team sees message content, or **headers only** (*yalnız üst bilgi*), where the team sees subjects and people but the content is hidden. Users who aren't allowed to see the content receive messages with `body: null`, `hidden: true`. This rule also applies to the API and MCP. ## Thread fields | Field | Type | Description | |---|---|---| | `id` | integer | Thread ID. | | `subject` | string | Subject. | | `customer_id`, `customer` | integer, string | Linked company. | | `opp_id`, `opp`, `opp_how` | integer, string | Linked project and why it was chosen (`chain`, `compose`, `single`, `number`, `recent`, `manual`…). | | `suggested` | boolean | The project is only a suggestion (`recent`). | | `n`, `last_at`, `last_dir` | integer, datetime, string | Message count, time of the last message, and its direction (`in` incoming, `out` outgoing). | | `waiting` | boolean | The last message was incoming — awaiting a reply. | | `peer` | string | The last external person in the thread. | The single-record endpoint also returns `messages`, `links`, `companies`, `suggestions`, `linkables`, `may_edit`, and a ready-made `reply` for responding (recipient, `Re:` subject, `in_reply_to`). ## Sending The [Send an email](/rest-api/email/send) endpoint sends the message from the user's **own account**; the message also lands in the Sent folder and is linked to the thread. Bulk sending and email sequences are available only in the web interface (Email Automation module). --- # Record IDs > The difference between the numeric ID (id) and the 15-character permanent record ID (rid), and when to use each. Every record has two IDs: | | `id` | `rid` | |---|---|---| | Format | Integer (`42`) | 15 characters (`001aB3xY7…`) | | Scope | Unique within the record type | Unique within the installation; also identifies the type | | Use | API paths and parameters | Storing in external systems, search, Excel matching | | Can it change? | No | No | API endpoints always take the `id` (`/api/v1/customers/42`). The `rid` is for storing a reference to a CRM record in another system, showing it to users, or matching records during an Excel import: on its own, it also carries the record's type. ## Prefixes The first three characters of a `rid` identify the record type: | Prefix | Type | Prefix | Type | |---|---|---|---| | `001` | Company | `00T` | Task | | `003` | Person | `00Q` | Fair lead | | `006` | Opportunity | `0Q0` | Quote | | `00U` | Visit / call | `801` | Order | | `a0D` | Trial | `800` | Contract | | `01t` | Product | `500` | Support ticket | The remaining 12 characters are case-sensitive random letters and digits. ## Finding a record by rid If you pass a 15-character `rid` to the [search endpoint](/rest-api/workspace/search), the response's `record` field resolves it to that record: ```json { "ok": true, "record": { "kind": "Customer", "url": "/customers/42", "customer_id": 42 }, "customers": [], "contacts": [], "opps": [], "leads": [] } ``` In the web interface, the search in the top bar and the command palette (Ctrl/⌘ + K) also accept a `rid`. ## External IDs To store an ID from another system in the CRM, use the [data sync hook](/guides/inbound#sync): it links the `external_id` field to the record and updates the same record on later sends. External records created by apps (a Notion page, an Asana task…) are also written to the record with their external ID and link; you open them from the **Apps** (*Uygulamalar*) menu on the record page. --- # Stages and gates > Sales stages are defined per installation; when stage gates are on, an opportunity advances only once it meets their conditions. Each installation's sales pipeline is defined on the **Settings → Stages** (*Ayarlar → Aşamalar*) page: the key, name, and color of each open stage; the names for won, lost, and canceled; whether the Cancel stage is enabled; and the **stage rules**. ## Stage keys The API always uses the **key**; the name is what users see. The closing keys are fixed: | Key | Meaning | |---|---| | `Win` | Won | | `Lost` | Lost | | `Cancel` | Canceled (may be disabled in the installation) | | `Account` | Customer-tracking container (not counted in the sales funnel) | Open stage keys are specific to each installation. For example, the Makro pipeline uses `Qualify`, `Viable`, `Present Solution`, `Negotiation`, `Expect to Close`, while a new installation defaults to `Lead`, `In Progress`. Don't hard-code keys — read them from the [`GET /api/v1/me`](/rest-api/auth/me) response: ```json "constants": { "stages": ["Qualify", "Viable", "Present Solution", "Negotiation", "Expect to Close", "Win", "Lost", "Cancel"], "open_stages": ["Qualify", "Viable", "Present Solution", "Negotiation", "Expect to Close"], "stage_labels": { "Qualify": "Qualify", "Win": "Win", "Lost": "Lost", "…": "…" }, "stage_sla": { "Qualify": 30, "Viable": 60, "Present Solution": 60, "Negotiation": 30, "Expect to Close": 15 }, "stage_rules": true } ``` ## Stage rules (gates) In installations with `stage_rules: true`: - Open stages are passed through **in order**; you can't skip a stage. - To move to the next stage, the current stage's gate must be satisfied. - `Win` can be set only from the last two open stages (`Negotiation` and `Expect to Close` in Makro). - A closed opportunity can't be reopened; [Revive opportunity](/rest-api/deals/revive) creates a new record. Gates in the Makro pipeline: | Stage | To move to the next stage | |---|---| | `Qualify` | BANT 4/4 (budget, authority, need, timing) | | `Viable` | At least one face-to-face (F2F) visit since the project started, and a product selected on the opportunity | | `Present Solution` | The latest trial's outcome is Successful (*Başarılı*) or Partially Positive (*Kısmen Olumlu*) | | `Negotiation` | Quote price (or estimated value) and payment term entered | If the gate isn't satisfied, the [stage endpoint](/rest-api/deals/stage) returns `409 gate`: ```json { "ok": false, "error": "gate", "message": "Viable'a geçmek için: BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)", "gate_problems": ["BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)"] } ``` The single-opportunity endpoint always includes the `gate_ok`, `gate_problems`, and `next_stage` fields; check them before changing the stage so you can show users what's missing. In installations with `stage_rules: false`, there are no gates: an opportunity can move to any open stage, or to won or lost. ## Durations (SLA) Each open stage has a target in days (`stage_sla`), and each opportunity has a total lifetime (`opp_lifetime_days`, 180 days by default). Overruns show up as negative values in the opportunity's `sla_left` and `lifetime_left` fields and lower its health score. The values vary by installation and are set on the **Settings → Stage setup** (*Ayarlar → Aşama kurulumu*) card. ## Winning and losing - `Win`: the company becomes `AC` (active customer), the line items of the accepted quote are written to the company as contracted prices, a draft contract is created if the Contracts module is enabled, and the `opp_won` event is sent to apps. - `Lost` / `Cancel`: the `reason` field is stored as the loss reason (`constants.loss_reasons`; `Diğer` (Other) if the reason isn't in the list). On `Lost`, the `opp_lost` event is sent to apps. - Every stage change also sends an `opp_stage` event (with the old and new stage). --- # Units and currency > Quantities are stored in the product's unit and prices in the record's currency; totals are converted to the installation's base currency. Solk is built for industrial sales: an opportunity's value is often calculated as **monthly quantity × unit price**, and different products are sold in different units (kg, ton, liter, piece, m²…). ## Units - The installation's **base unit** is a company-wide setting (e.g. `kg`); company potential and dashboards are expressed in this unit. `GET /api/v1/me` → `app.unit_default`; enabled units are in `app.units`. - Each product can have its own unit. In opportunity responses, `unit` (e.g. `lt`), `unit_code`, and the price unit `pu` (e.g. `€/lt`) reflect the product's unit. - The `_kg` suffix in field names is historical: fields such as `potential_kg` and `commit_kg` hold quantities **in the product's or the installation's unit**, which isn't always kilograms. ## Currencies - The installation has a **base currency** (`app.currency_base`, e.g. `EUR`) and a list of enabled currencies (`app.currencies`). - Each company can have a default transaction currency (`currency_default`); opportunities and quotes carry their own currency (`currency`, `cur_sym`). - Totals and dashboards are calculated from values converted to the base currency: `value_base` on opportunities, and `value_open` and `base_currency` in lists. ## Exchange rates Exchange rates are fetched from the Central Bank of the Republic of Türkiye every business day (22 currencies). An admin chooses whether to use the **buying** or the **selling** rate, and can manually enter and lock a rate for a given day. Conversion uses the **current** rate, not the rate on the record's date — so historical reports in the base currency can change as exchange rates change. ## Numbers in the API - Quantities and amounts are JSON numbers (`12000`, `2.35`), with no thousands separators or currency symbols. For display, use `value_short` (e.g. `"338K"`, `"2,46M"`) and `cur_sym`. - Send numbers in request bodies as JSON numbers. Strings such as `"2,35"` and `"1.250,50"` are also parsed correctly (Turkish notation: period for thousands, comma for decimals). --- # Archiving and deletion > In Solk, records are rarely deleted; they're closed, deactivated, or marked as former. Historical reports stay intact. Sales history is the foundation of reports, forecasts, and customer relationships. That's why Solk **deactivates** a record instead of deleting it. Permanent deletion is an operation only an admin can perform from the web interface, after an automatic backup is taken. The API has no permanent-delete endpoint. | Record | Instead of deleting | In the API | |---|---|---| | Company | **Deactivate** (*Pasife al*) — the company is removed from lists and reminders; its history is kept. If an admin deletes an inactive company a second time, a backup is taken and the company is permanently deleted. | List inactive companies with `?active=0`; the status can't be changed through the API. | | Person | Mark as **Former** (*Ayrıldı*) (`is_former`) — the person's name stays on past activities. | [Update a person](/rest-api/people/update) `is_former: true` | | Opportunity | Move it to the **Lost** (*Kaybedildi*) or **Canceled** (*İptal*) stage; revive it if needed. | [Change stage](/rest-api/deals/stage), [Revive](/rest-api/deals/revive) | | Task / action | **Complete** (*Tamamla*) — with an outcome note. | [Complete an action](/rest-api/actions/complete) | | Fair lead | **Dropped** (*Vazgeçildi*) status. | [Update a lead](/rest-api/fairs/leads-update) | | Calendar event | Events added in the app can be deleted. | [Delete an event](/rest-api/calendar/delete) | | User | **Close** (*Kapat*) — sign-in and tokens are revoked; the user's records are kept. | [SCIM](/rest-api/scim) `active: false` | ## Sample data For a demo or trial, an admin can load sample companies, opportunities, and actions into the installation with the **Sample data** (*Örnek veri*) card on the **Settings** (*Ayarlar*) page, and remove them all with one click. Sample records don't send events to apps. We recommend testing your integration with sample data in a trial installation before you connect it to a live installation. ## Audit log Creates, edits, stage changes, postponements, deletions, permission denials, and MCP tool calls are written to the audit log along with who performed them; changes from the mobile app and the API carry the note "mobil" (mobile). Admins can view the log in **Settings → Audit log** (*Ayarlar → Denetim günlüğü*). --- # Apps > The 40+ prebuilt connections in Settings → Apps — what they do, how to set them up, and which data goes where. The **Settings → Apps** (*Ayarlar → Uygulamalar*) page is the catalog of prebuilt connections. Each card has a **details page** (overview, setup steps, permissions, link to docs); the **Connect** (*Bağla*) button opens the connection's settings. Connected apps are grouped on the **Connected** (*Bağlı*) tab, which shows the result of the latest delivery. Connections work in four modes: | Mode | What happens | Example | |---|---|---| | **Channel** | CRM events are posted as messages to a channel. | Slack, Teams, Telegram | | **Record from event** | A CRM event creates a record in another app. | Notion page, Asana task, Sheets row | | **Record action** | A one-click action from the **Apps** (*Uygulamalar*) menu on a record page. | Find an email with Hunter, call with Aircall, create a PandaDoc document | | **Inbound data** | An external system sends data to the CRM. | Website form, Stripe, Segment | For connections that send events, you choose which events are sent; the event list and payload format are on the [Webhooks](/guides/webhooks) page. ## Catalog | App | Mode | What it does | |---|---|---| | Slack | Channel · command | Selected events are posted to a channel; the `/crm` command searches companies, people, and opportunities from Slack. | | Microsoft Teams | Channel | Events are posted to a Teams channel as cards. | | Google Chat | Channel | Events are posted to a Google Chat space as messages. | | Discord | Channel | Events are posted to a Discord channel. | | Telegram | Channel | A Telegram bot posts events to a group or channel. | | Webhook | Event | Signed JSON to any URL. | | Zapier · Make · n8n · Pipedream | Event + API | Events trigger your workflow; the workflow writes back to the CRM with an API key. | | Notion | Record from event | Events are added as pages to a Notion database. | | Airtable | Record from event | Events are added as rows to an Airtable table. | | Google Sheets | Record from event | Events are added as rows to a spreadsheet (date · event · title · description · link). | | Asana · ClickUp | Event + action | Creates tasks from events and records. | | Linear | Action | Creates a Linear issue from a support ticket or note. | | Productboard | Action | Sends customer feedback (notes, requests) as insights. | | PandaDoc | Action | Creates a PandaDoc document from a quote and sends it for signature. | | Stripe | Action + inbound | The company's Stripe customer and invoices; payments appear on the timeline. | | Aircall | Action + inbound | One-click calling from a person; calls are logged as activities, and missed calls become callback tasks. | | RingCentral | Action + inbound | Calling with the RingCentral app; call records become activities via Zapier. | | Calendly / Cal.com | Inbound | Bookings land on the calendar, the person, and a note; a cancellation cancels the meeting. | | Meeting notes | Inbound | AI notes go to the company as notes, and action items become tasks. | | Segment | Inbound | People, companies, and notes from `identify` / `group` / `track` events. | | Data sync | Inbound | Bulk-creates and updates companies / people from a data warehouse. | | Website form · Typeform · Tally | Inbound | Submission → prospect company + person + task for the owner. | | Apollo | Action | Enriches a company from its domain and a person from their email. | | Hunter | Action | Finds / verifies emails; fetches the people at a domain. | | Mailchimp | Event + action | Adds people to an audience; optionally adds new people automatically. | | lemlist | Action + inbound | Adds a person to a campaign; reply and interest events become notes and tasks. | | Mixmax | Action | Adds a person to a Mixmax sequence. | | Resend | System | Notification and invitation emails are sent through Resend; people are added to a Resend audience. | | Okta / Entra ID (SCIM) | Inbound | Users are created, updated, and deactivated from the identity provider. [SCIM](/rest-api/scim) | | Add from browser | Action | Bookmarklet: adds the company on the page you're viewing (website, LinkedIn, Gmail) as a prospect. | | Google Drive · OneDrive · Dropbox · Box | Storage | Record files go to a CRM folder in the cloud. [Storage](/guides/storage) | | Cloud backup | Storage | The daily database backup is also sent to the connected cloud account. | | Claude | AI | Connects the CRM to Claude (MCP). [MCP](/mcp/overview) | ## Apps on record pages On company, person, opportunity, and support ticket pages, the **Apps** (*Uygulamalar*) menu shows actions from connected apps: call with Aircall, find an email with Hunter, enrich with Apollo, create an Asana task, create a PandaDoc document… The external record created in the app (task, page, document) is written to the CRM record along with its link, and opens from the same menu. If a calling app is connected, a **Call** (*Ara*) button appears next to person and company phone numbers; after the call, an activity is logged automatically. ## Requesting an app Need an app that isn't in the catalog? The **Request an app** (*Uygulama isteyin*) link at the bottom of the Apps page sends your request to the Solk team; the most-requested apps are prioritized. In the meantime, you can connect almost any tool with [webhooks](/guides/webhooks), [inbound hooks](/guides/inbound), and the [REST API](/rest-api/overview). ## Security - App keys are stored only in the installation's own database and are masked on screen; OAuth tokens for storage accounts and email passwords are stored **encrypted**. Protect your database backups accordingly. - Only admins can set up connections (except storage accounts and Claude — each user connects those for themselves). - Each connection's card shows its latest delivery, error, and counter; the **Test connection** (*Bağlantıyı dene*) button verifies the credentials. --- # Webhooks > Send signed JSON to a URL of your choice when something happens in the CRM (an opportunity is won, a new company is added, a form is submitted…). Zapier, Make, n8n, and Pipedream use the same mechanism. A webhook (outbound hook) is a way to **push** CRM events to your system: instead of constantly polling the API, you receive an HTTP `POST` when an event happens. You set it up on the **Settings → Apps** (*Ayarlar → Uygulamalar*) page; no code required. ## Setup :::steps ### Open the connection In **Settings → Apps** (*Ayarlar → Uygulamalar*), select **Webhook** (*Web kancası*) (or the Zapier, Make, n8n, or Pipedream card) and click **Connect** (*Bağla*). ### Enter the URL Paste the `https://` URL that events should be sent to. You get this URL from "Webhooks by Zapier → Catch Hook" in Zapier, "Webhooks → Custom webhook" in Make, the "Webhook" node in n8n, or the "HTTP / Webhook" trigger in Pipedream. ### Choose events Select the events that should trigger a delivery. The connection card shows a **signing key**; store it so the receiving side can verify that requests really come from the CRM. ### Test it **Test connection** (*Bağlantıyı dene*) sends a test delivery with `event: "test"` to your receiver. The result of the latest delivery (HTTP status code or error) appears on the card. ::: ## Events | Event | When | `data` fields | |---|---|---| | `opp_created` | A new opportunity was created | `id`, `rid`, `name`, `customer`, `customer_id`, `stage`, `stage_label`, `value`, `currency`, `owner` | | `opp_stage` | An opportunity's stage changed (the title shows the old → new stage) | Opportunity fields | | `opp_won` | An opportunity was won (`Win`) | Opportunity fields | | `opp_lost` | An opportunity was lost (`Lost`) | Opportunity fields | | `customer_created` | New company | `id`, `rid`, `name`, `status`, `source`, `city` | | `contact_created` | New person (except those created automatically from email) | `id`, `name`, `email`, `phone`, `title`, `customer`, `customer_id` | | `lead_created` | New fair lead | `id`, `company`, `contact`, `email`, `interest` | | `visit_created` | Activity / visit logged | `id`, `customer`, `type`, `date`, `next_action`, `note` | | `ticket_created` | New support ticket | `id`, `subject`, `priority`, `who` | | `form_submitted` | Website form / Typeform / Tally submission | `form`, `company`, `name`, `email`, `phone`, `customer_id`, `task_id`, `new_customer` | Events are sent for every change, whether it comes from the web interface, the mobile app, the API, MCP, workflows, or inbound hooks. There are two exceptions: - **Sample data** records don't send events. - If a single operation produces more than 25 events (e.g. a bulk import from Excel), no events are sent; a note is added to the audit log instead. ## Payload ```http POST /sizin/adresiniz HTTP/1.1 Content-Type: application/json X-Solk-Event: opp_won X-Solk-Signature: sha256=5d1c0a3e9b… ``` ```json { "event": "opp_won", "title": "Fırsat kazanıldı", "text": "Kuzey Plastik Sanayi · Streç film tedariki · 338.400 € · Sorumlu: Deniz Aksoy", "url": "https://ornek.solk.app/opportunities/53", "data": { "id": 53, "rid": "006Xq3LmT0aZb9K", "name": "Streç film tedariki", "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "stage": "Win", "stage_label": "Win", "value": 338400.0, "currency": "EUR", "owner": "Deniz Aksoy" }, "workspace": "Örnek Kimya", "app": "Solk CRM", "sent_at": "2026-10-02T14:05:11" } ``` | Field | Description | |---|---| | `event` | Event name (see the table above; `test` for test deliveries). | | `title`, `text` | Human-readable title and summary — you can post them directly to notification channels. | | `url` | The record's URL in the CRM. | | `data` | Event-specific structured fields. If you need the full record, fetch it from the [REST API](/rest-api/overview) using `data.id`. | | `workspace`, `app` | The installation's company name and app name. | | `sent_at` | Time the delivery was sent (in the installation's local time). | ## Verifying the signature `X-Solk-Signature` is the HMAC-SHA256 digest of the request body (as raw bytes), computed with the signing key. Verify it **before** you parse the body as JSON: :::code ```python Python import hmac, hashlib def verify(body_bytes: bytes, signature: str, key: str) -> bool: expected = "sha256=" + hmac.new(key.encode(), body_bytes, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or "") # Flask @app.post("/solk") def solk(): if not verify(request.get_data(), request.headers.get("X-Solk-Signature"), SIGNING_KEY): abort(401) event = request.get_json() ... ``` ```javascript Node.js import crypto from "node:crypto"; import express from "express"; const app = express(); app.post("/solk", express.raw({ type: "application/json" }), (req, res) => { const expected = "sha256=" + crypto.createHmac("sha256", process.env.SOLK_SIGNING_KEY) .update(req.body).digest("hex"); const signature = req.get("X-Solk-Signature") || ""; if (signature.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.sendStatus(401); } const event = JSON.parse(req.body); res.sendStatus(204); }); ``` ```php PHP $body = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $body, getenv('SOLK_SIGNING_KEY')); if (!hash_equals($expected, $_SERVER['HTTP_X_SOLK_SIGNATURE'] ?? '')) { http_response_code(401); exit; } $event = json_decode($body, true); ``` ::: ## Delivery - Deliveries are sent in the background, independently of the request; CRM users don't wait. - The timeout is **8 seconds**. Your receiver must return a `2xx`; queue long-running work and respond immediately. - **There are no retries.** A failed delivery shows up as the latest status on the connection card. If you can't afford to miss events, sync periodically from the API (e.g. `GET /api/v1/opportunities?sort=update`). - Ordering isn't guaranteed; sort events for the same record by `sent_at`. ## Channels Slack, Microsoft Teams, Google Chat, Discord, and Telegram connections send the same events in the channel's own message format (Slack text, a Teams Adaptive Card…); the signature header is present only for webhook, Zapier, Make, n8n, and Pipedream connections. Notion, Airtable, Google Sheets, Asana, ClickUp, Linear, and Mailchimp connections instead create a record in that app from the event. Details: [Apps](/guides/apps). ## Writing back to the CRM If your workflow needs to create or update records in the CRM (e.g. "invoice paid → complete task" in Zapier), create an API key in **Settings → Developers** (*Ayarlar → Geliştiriciler*) and call the [REST API](/rest-api/overview) from the HTTP step in Zapier / Make / n8n with the `Authorization: Bearer sk_…` header. The no-code way to push data in from outside is [inbound hooks](/guides/inbound). --- # Inbound hooks > Send data to the CRM through a secret URL from forms, phone systems, payment systems, data warehouses, and booking tools. An inbound hook is a secret URL that lets an external system send data to the CRM **without credentials**. Each connection has its own URL; the random key in the URL carries both identity and authorization. You get the URL when you connect the relevant card in **Settings → Apps** (*Ayarlar → Uygulamalar*). ```text https://ornek.solk.app/in// (Aircall, Stripe, Segment, data sync…) https://ornek.solk.app/in/form/ (website form, Typeform, Tally) ``` :::warning A URL that contains the secret key is like a password. Enter it only in the sending system; if it leaks, get a new one with **Regenerate URL** (*Adresi yenile*) on the connection card — the old URL stops working immediately. ::: ## Common rules - The method is `POST` and the body is JSON (form submissions can also use `application/x-www-form-urlencoded`). A `GET` request to the same URL returns a short JSON response confirming that the connection works. - Records are created on behalf of the connection's **owner** (the user who set up the connection, or the user selected on the card); if the event's own user (e.g. the email of the rep who made the call) matches a user, that user is used instead. - If the same event arrives twice (same external ID), it isn't processed a second time: `{"ok": true, "skipped": "zaten işlendi"}` ("already processed"). - Rate limits are **per hour**, per connection: Segment 3000, data sync / Aircall / RingCentral / Stripe 600, all others 120 requests. If you exceed the limit, you get `429`. - The URL of a disabled or deleted connection returns `404`. ## Website form {#form} Connect the contact / quote form on your own website directly to the CRM. Each submission: 1. Finds an existing company by name, or creates a **prospect company** if none exists (source: Web formu), 2. Links the person to the company (adding the person if the email isn't on file), 3. Creates a **high-priority task due today** for the connection owner and sends a notification, 4. Forwards the `form_submitted` event to apps. Fields are recognized by their names, which can be in Turkish or English: | CRM field | Recognized field names | |---|---| | Company | `firma`, `şirket`, `company`, `kurum`, `organization` | | Full name | `ad soyad`, `adınız`, `isim`, `name`, `full name`, `yetkili` | | Email | `e-posta`, `eposta`, `email`, `mail` (the value must contain `@`) | | Phone | `telefon`, `phone`, `tel`, `gsm`, `cep` | | City | `şehir`, `il`, `city` | | Job title | `unvan`, `görev`, `title`, `pozisyon` | | Message | `mesaj`, `message`, `not`, `açıklama`, `talep`, `konu` | Unrecognized fields aren't lost either: all fields are written to the task details as "label: value" lines. :::code ```html HTML form
``` ```bash cURL (JSON) curl https://ornek.solk.app/in/form/GIZLI_ANAHTAR \ -H "Content-Type: application/json" \ -d '{"company": "Anadolu Gıda", "name": "Murat Er", "email": "murat@anadolugida.com.tr", "message": "Aylık 5 ton streç film"}' ``` ::: The response is `{"ok": true, "customer_id": 42, "task_id": 118}`. If the HTML form sets `_next`, the user is redirected to that page. **Typeform** and **Tally** use the same URL structure; paste the URL into the form's webhook settings. If you enter the signing secret on the card, the `Typeform-Signature` / `Tally-Signature` headers are verified and unsigned requests get `401`. ## Data sync {#sync} Send companies and people from your data warehouse, ERP, or another CRM in bulk to **create or update** them. Works with Census, Hightouch, Airbyte, your own script, or Zapier. ```bash curl "https://ornek.solk.app/in/sync/GIZLI_ANAHTAR?obj=customer" \ -H "Content-Type: application/json" \ -d '{"rows": [ {"external_id": "ERP-1042", "name": "Kuzey Plastik Sanayi", "website": "kuzeyplastik.com.tr", "city": "Bursa", "industry": "Plastik", "status": "AC"}, {"external_id": "ERP-1043", "name": "Anadolu Gıda Ambalaj", "phone": "+90 332 555 10 20"} ]}' ``` ```json { "ok": true, "created": 1, "updated": 1, "errors": [] } ``` - Choose the object with `?obj=customer` / `?obj=contact` or with `"object"` in the body; if you omit it, rows that have an `email` and no `industry` are treated as people. - The body can be an array, a `rows` / `records` / `batch` / `data` field, or a single object. Up to **500 rows** per request. - Matching order — company: `external_id` (if sent before) → name → website domain. Person: `external_id` → email. - Company fields: `name`/`company`, `website`/`domain`, `phone`, `city`, `industry`/`sector`, `address`, `status` (`AC`/`Prospect`). Person fields: `name` (or `first_name` + `last_name`), `email`, `phone`/`mobile`, `title`/`job_title`, `department`, and `company` or `company_external_id` for the company. - Empty values don't erase existing data; only populated fields are written. ## Segment {#segment} In Segment, add a **Webhooks (Actions)** destination and enter the URL. - `identify` → creates or updates the person (and the company, if `traits.company` is present); `userId` is stored as the external ID. - `group` → creates or updates the company (`groupId`, `traits.name`, `website`, `industry`). - `track` → the event names you select on the card (e.g. `Demo Requested, Trial Started`) are added as notes on the person's company. In batch sends (`batch`), up to 100 events are processed per request. ## Phone: Aircall and RingCentral {#calls} In the **Aircall** webhook, select the `call.ended` and `call.voicemail_left` events. For **RingCentral** and other phone systems, send this flat body via Zapier / Make: ```json { "id": "rc-88231", "direction": "outbound", "number": "+90 224 555 01 02", "result": "answered", "duration": 312, "started_at": "2026-10-02T10:41:00+03:00", "user_email": "deniz@ornekkimya.com.tr", "recording_url": "https://…", "notes": "Fiyat görüşüldü" } ``` - If the number matches a person or company, a **phone call** activity is created (duration, direction, recording link, note). - For a missed inbound call, a **"Call back"** (*Geri ara*) task is created for the owner (high priority, with the voicemail link). - If the number is unknown, a "link the record to a company" task is created. ## Stripe {#stripe} In the Stripe Dashboard, add the URL as a webhook endpoint and select the `invoice.paid`, `invoice.payment_failed`, `customer.subscription.deleted`, and `checkout.session.completed` events. If you enter the **signing secret** (`whsec_…`) on the card, `Stripe-Signature` is verified. Payments are added as notes on the company; for failed payments and ended subscriptions, a task is created for the owner. The company is found by a previously linked Stripe customer ID or by the customer's email; on a `checkout.session.completed` event, if no company exists, one is created as a prospect. ## Bookings: Calendly and Cal.com {#booking} When a booking is made, a meeting is added to the calendar, and a person and a note are added to the company; when the booking is canceled, the meeting is canceled too. In Calendly, select the `invitee.created` / `invitee.canceled` events; in Cal.com, select `BOOKING_CREATED`, `BOOKING_RESCHEDULED`, and `BOOKING_CANCELLED`. ## Meeting notes {#meeting-notes} Summaries from AI meeting-note tools (Fireflies, Fathom, tl;dv, Otter… directly or via Zapier) are added as notes on the attendees' companies; action items become tasks due in two days. ```json { "id": "mtg_5521", "title": "Kuzey Plastik · haftalık", "summary": "Numune sonuçları olumlu, fiyat revizyonu istendi.", "attendees": [{ "email": "emre.yildiz@kuzeyplastik.com.tr", "name": "Emre Yıldız" }], "action_items": ["Revize fiyatı gönder", "Hat denemesi tarihini netleştir"], "url": "https://…" } ``` ## lemlist {#lemlist} Campaign events (`emailsReplied`, `linkedinReplied`, `emailsInterested`, `meetingBooked`, `emailsNotInterested`, `emailsBounced`, `emailsUnsubscribed`, `emailsClicked`) are added as notes on the person's company; for reply, interest, and meeting events, a "follow up" task is created for the owner. ## Slack command {#slack} In your Slack app, define a **Slash Command** (e.g. `/crm`) with the connection's URL as the request URL, and enter Slack's **Signing Secret** on the card. A user who types `/crm kuzey` gets the matching companies, people, and opportunities in a reply that only they can see. --- # Storage accounts > Save record files to Google Drive, OneDrive, Dropbox, or Box; link cloud files to records; and send the daily backup to the cloud. Files attached to company, opportunity, quote, contract, and support ticket records are stored on the CRM server. A user who connects a storage account can also save these files to **their own cloud account**, and attach files that already live in the cloud to a record as links. ## Supported providers | Provider | Permission | File location | |---|---|---| | Google Drive | `drive.file` — only files and folders the CRM creates | `CRM//` | | Microsoft OneDrive | `Files.ReadWrite` (personal OneDrive or OneDrive for Business) | `CRM//` | | Dropbox | App folder | `Apps//CRM//` | | Box | Account | `CRM//` | You can change the folder name per installation (`STORAGE_ROOT`, default `CRM`). Files for records that aren't linked to a company go to the `CRM/Genel/` ("General") folder. ## Connecting :::steps ### Choose the account On the **Settings → Storage accounts** (*Ayarlar → Depolama hesapları*) page (or from the provider's card in Apps), select the provider. On the provider's consent screen, sign in with your account and grant access. ### Set options On the connected account card: - **Also save files I add here** (*Eklediğim dosyaları buraya da kaydet*) — when on, every file you upload to records is also sent to the cloud in the background. - **Daily database backup** (*Günlük veritabanı yedeği*) (admins only) — the nightly backup is compressed and also uploaded to this account. ### Try it On any record's **Files** (*Dosyalar*) card, select **Save to cloud** (*Buluta kaydet*) next to a file. The file is created in the cloud folder and shows the provider's icon in the file row. ::: Each user connects their own account; one user's cloud account isn't accessible to other users. A user can connect more than one provider. ## Adding a cloud link If a file is already in Drive, OneDrive, Dropbox, or Box, don't upload it to the record again — use the **Link** (*Bağlantı*) button on the **Files** (*Dosyalar*) card to paste its sharing URL (https://…). The link appears in the file list with its provider's icon and opens in the cloud when clicked; the CRM doesn't download the file's contents. ## Behavior - A file saved to the cloud also **stays** in the CRM; the cloud copy is an additional copy. Deleting the file from the CRM doesn't delete the cloud copy. - The same file isn't saved twice ("already in the cloud", *zaten bulutta*). - If an upload fails (e.g. the permission was revoked), the account card shows the last error; the file stays in the CRM. - Disconnecting the account doesn't delete files in the cloud. ## Self-hosted installations On Solk-managed installations, the provider apps are already set up. On an installation running on your own server, you must add the provider keys to the `.env` file: `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` (Drive API enabled, `drive.file` on the consent screen), `MS_CLIENT_ID` / `MS_CLIENT_SECRET` (Graph `Files.ReadWrite`, `User.Read`), `DROPBOX_APP_KEY` / `DROPBOX_APP_SECRET`, `BOX_CLIENT_ID` / `BOX_CLIENT_SECRET`. The redirect URL is the same callback URL used for the email connection. --- # REST API > The Solk CRM JSON API — the same endpoints the mobile app uses, with the same permission rules and the same business rules. The Solk REST API lets you read and modify the companies, people, opportunities, actions, calendar, fair leads, and email threads in your installation. Solk's iOS / Android app uses this same API, so the endpoints are shaped around a real sales rep's screens: a single request returns everything a screen needs. ## Basics | | | |---|---| | Base URL | `https://.solk.app/api/v1` | | Format | JSON (`Content-Type: application/json`); most write endpoints also accept a form body | | Authentication | `Authorization: Bearer ` — [API key, OAuth, or mobile session](/rest-api/authentication) | | Character set | UTF-8; Turkish characters are returned as is | | Dates | `YYYY-MM-DD`; date-times `YYYY-MM-DDTHH:MM:SS` (the installation's local time) | | Rate limit | 300 requests per minute per token — [Rate limits](/rest-api/rate-limits) | | Specification | [`/openapi.json`](/openapi.json) (OpenAPI 3.1) | ## Response envelope Every response is a JSON object with an `ok` field: ```json { "ok": true, "customer": { "id": 31, "name": "Kuzey Plastik Sanayi", "…": "…" } } ``` Error responses include an HTTP status code, a machine-readable `error` code, and a Turkish `message` you can show to users; some errors carry extra fields (`similar`, `gate_problems`…): ```json { "ok": false, "error": "gate", "message": "Viable'a geçmek için: BANT kriterleri 4/4 sağlanmalı …", "gate_problems": ["…"] } ``` All codes: [Errors](/rest-api/errors). ## Resources :::cards - [Authentication](/rest-api/auth/me) key | Who you are, enabled modules, installation constants. - [Workspace](/rest-api/workspace/today) layout | Today, dashboard, search, notifications. - [Companies](/rest-api/companies/list) building | List, get, create, update, postpone. - [People](/rest-api/people/list) user | Contacts at your companies. - [Opportunities](/rest-api/deals/list) target | Stages, BANT, steps, revival. - [Activities](/rest-api/activities/log) phone | Log calls and visits (Quick Entry). - [Actions and tasks](/rest-api/actions/list) check | Complete, postpone, chain. - [Calendar](/rest-api/calendar/list) calendar | Meetings and derived entries. - [Fairs and leads](/rest-api/fairs/list) flag | Booth records. - [Email](/rest-api/email/threads-list) mail | Threads, linking, sending. - [Ask](/rest-api/ask/ask) spark | Ask questions about your CRM data in natural language. - [SCIM 2.0](/rest-api/scim) shield | User management. ::: ## Versioning The API path `/api/v1` is fixed. New fields and endpoints are **additive**; existing fields are never removed and their meaning never changes. Your client should ignore fields it doesn't recognize. When a breaking change is necessary, a new path (`/api/v2`) is introduced and announced in advance in the [changelog](/changelog). ## Methods for write endpoints Update endpoints accept `PATCH`, `PUT`, and `POST` interchangeably (for older HTTP clients and the mobile app). Updates are **partial**: only the fields you send change (the one exception: for a [fair lead](/rest-api/fairs/leads-update), sending `company` rewrites all form fields). To clear a text field, send an empty string (`""`); a field sent as `null` is ignored (left unchanged). Send boolean fields as JSON `true` / `false`. In a form body (`application/x-www-form-urlencoded`), send `1` for true and leave the field empty for false. --- # Authentication > API keys, OAuth access tokens, and mobile session tokens — when to use each and how to revoke them. Every request carries a **Bearer** token in the `Authorization` header: ```http GET /api/v1/me HTTP/1.1 Host: ornek.solk.app Authorization: Bearer sk_9fQ… ``` If the token is missing, invalid, expired, or revoked, the response is `401 unauthorized`. Never send the token in the URL (query string). ## Token types | Type | Prefix | Issued by | Lifetime | Permissions | When to use | |---|---|---|---|---|---| | **API key** | `sk_` | An admin, in **Settings → Developers** (*Ayarlar → Geliştiriciler*) | Until revoked | All permissions of the admin who created it | Server-to-server integrations, Zapier / Make / n8n, scripts | | **OAuth access token** | `mcp_` | The OAuth flow, with the user's consent | 1 hour (refresh token: 60 days) | The consenting user + scope (`crm.read`, `crm.write`) | Apps that act on behalf of users, MCP clients | | **Mobile session token** | `xk_` | [`POST /api/v1/login`](/rest-api/auth/login) | 90 days (installation setting) | The signed-in user | The Solk mobile app; your own mobile client | ## API key 1. Open **Settings → Developers** (*Ayarlar → Geliştiriciler*). Only admins can see this page. 2. Name the key and click **Create key** (*Anahtar oluştur*). The key is shown only once. 3. Store the key in an environment variable or a secrets vault. The page shows each key's name, creation time, and last-used time; **Revoke** (*İptal et*) invalidates a key immediately. Because a key acts with the identity of the admin who created it, deactivating that admin also invalidates the key — for company-wide integrations, we recommend creating a dedicated "integration" admin user. :::note API keys and OAuth tokens don't prompt for two-step verification, but IP restrictions still apply. Changes made with a key are recorded in the audit log under the key owner's name. ::: ## OAuth If your app runs under multiple users' own accounts (for example, a desktop add-in or a multi-tenant SaaS), get each user's permission through OAuth. The user sees your app and the requested scopes on a consent screen; as soon as they grant access, you receive an access token. Users can remove access at any time in **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*). | Scope | Grants | |---|---| | `crm.read` | Read companies, people, opportunities, actions, email threads, calendar, and dashboards | | `crm.write` | Create companies, people, opportunities, tasks, notes, and activity logs; complete tasks; change stages | Non-`GET` requests made without `crm.write` receive `403 insufficient_scope`. For the full flow, see [OAuth app](/rest-api/oauth). ## Mobile session If you're building your own mobile client, [sign in](/rest-api/auth/login) with a username and password. For users with two-step verification enabled, the first request returns `401 otp_required`; ask the user for the 6-digit code and repeat the same request with the `otp` field. After 5 failed attempts from the same IP, sign-in is locked for 60 seconds (`429 locked`). On sign-out, [`POST /api/v1/logout`](/rest-api/auth/logout) revokes the token. ## Security recommendations - Never put tokens in source code, in JavaScript that runs in the browser, or in public repositories. - Create a separate key for each integration so that revoking one doesn't affect the others. - Send requests only over `https://`; Solk installations redirect HTTP to HTTPS. - If a token leaks, revoke it immediately. When a user is deactivated (or deprovisioned through SCIM), all of their tokens are revoked automatically. --- # OAuth app > Build an app that users authorize with their own accounts — client registration, authorization with PKCE, tokens, and refresh. Every Solk installation runs an **OAuth 2.1 authorization server**. The same server issues tokens to MCP clients (Claude) and to apps that call the REST API on behalf of users. The supported flows are **authorization code + PKCE (S256)** and **refresh token**; the password flow and the client credentials flow are not supported. | | URL | |---|---| | Discovery | `https://ornek.solk.app/.well-known/oauth-authorization-server` | | Client registration | `POST https://ornek.solk.app/oauth/register` | | Authorization | `GET https://ornek.solk.app/oauth/authorize` | | Token | `POST https://ornek.solk.app/oauth/token` | | Revocation | `POST https://ornek.solk.app/oauth/revoke` | Each installation has its own authorization server: get the user's installation address (for example, `ornek.solk.app`) from the user and read the discovery document from that address. :::steps ### Register the client Register your app once ([Dynamic client registration](/rest-api/oauth/register), RFC 7591). Server-side apps that can keep a secret choose `client_secret_post`; desktop and mobile apps use `none` (public client). ```bash curl https://ornek.solk.app/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Örnek Entegrasyon", "redirect_uris": ["https://uygulamaniz.com/oauth/callback"], "token_endpoint_auth_method": "none" }' ``` ```json { "client_id": "crm_Jq8w…", "client_id_issued_at": 1790939471, "client_name": "Örnek Entegrasyon", "redirect_uris": ["https://uygulamaniz.com/oauth/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"] } ``` Redirect URIs must use `https://` or a loopback address (`http://localhost`, `http://127.0.0.1`, `http://[::1]`). For loopback addresses, the port number is ignored during matching. Instead of client registration, you can also use **CIMD**: pass the `https://` URL of your client metadata document as the `client_id`. ### Generate PKCE values For each authorization, generate a random `code_verifier` (43–128 characters) and a `code_challenge`, which is the base64url-encoded SHA-256 hash of the verifier: ```python import secrets, hashlib, base64 verifier = secrets.token_urlsafe(48) challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=") ``` ### Redirect the user ```text https://ornek.solk.app/oauth/authorize ?response_type=code &client_id=crm_Jq8w… &redirect_uri=https%3A%2F%2Fuygulamaniz.com%2Foauth%2Fcallback &scope=crm.read%20crm.write &state=xyz123 &code_challenge=E9Melhoa2Owv… &code_challenge_method=S256 ``` If the user isn't signed in, the sign-in screen appears first. The consent screen shows your app's name, the domain of the redirect URI, and the requested scopes. If the user clicks **Allow** (*İzin ver*), the browser returns to: ```text https://uygulamaniz.com/oauth/callback?code=Zx8…&state=xyz123 ``` If the user declines, you get `?error=access_denied&state=xyz123`. Verify that the `state` value matches the one you sent. ### Exchange the code for tokens The code is valid for 5 minutes and can be used **once**. The token endpoint expects a form body: ```bash curl https://ornek.solk.app/oauth/token \ -d grant_type=authorization_code \ -d code=Zx8… \ -d redirect_uri=https://uygulamaniz.com/oauth/callback \ -d client_id=crm_Jq8w… \ -d code_verifier=$VERIFIER ``` ```json { "access_token": "mcp_uWrPBo…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "mcpr_Nu5ue…", "scope": "crm.read crm.write" } ``` ### Call the API ```bash curl https://ornek.solk.app/api/v1/me -H "Authorization: Bearer mcp_uWrPBo…" ``` The access token is valid for the REST API and the [MCP](/mcp/overview) endpoint. Without the `crm.write` scope, write requests return `403 insufficient_scope`. ### Refresh the token The access token expires after 1 hour. Use the refresh token to get a new pair — every refresh returns a **new** refresh token and invalidates the old one: ```bash curl https://ornek.solk.app/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=mcpr_Nu5ue… \ -d client_id=crm_Jq8w… ``` Retrying with an old refresh token returns `400 invalid_grant`. A refresh token expires if it goes unused for 60 days; the user must then grant access again. ::: ## Grants and revocation - When a user authorizes the same app a second time, the existing grant is updated (its scope is replaced by the scope of the new consent); each app has a single grant per user. - In **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*), users see your app, its last-used time, and its call count; **Remove access** (*Erişimi kaldır*) immediately revokes every token issued under that grant. - If the user is deactivated, their tokens stop working. - Your app can revoke a token itself by calling [`POST /oauth/revoke`](/rest-api/oauth/revoke). ## Errors | Situation | Response | |---|---| | The code expired, was already used, belongs to a different client, or the `redirect_uri` doesn't match | `400 {"error": "invalid_grant"}` | | PKCE verification failed | `400 {"error": "invalid_grant", "error_description": "PKCE doğrulanamadı."}` | | The same code was used a second time | `400 invalid_grant` **and** every token issued under that grant is revoked | | Unsupported grant type | `400 {"error": "unsupported_grant_type"}` | | Too many requests | `429 {"error": "slow_down"}` | --- # Rate limits > The per-minute request limit per token, the 429 response, and the Retry-After header. The REST API, MCP, and SCIM endpoints accept **300 requests per minute per token**. The counter resets at the start of each calendar minute. When you exceed the limit, the response is `429`, and the `Retry-After` header gives the number of seconds until the next minute begins: ```http HTTP/1.1 429 Too Many Requests Retry-After: 9 Content-Type: application/json ``` ```json { "ok": false, "error": "rate_limited", "message": "Dakikalık istek sınırı aşıldı (300 istek / dk). Retry-After saniye sonra yeniden deneyin." } ``` ## Other limits | What | Limit | |---|---| | Mobile sign-in | 60-second lock after 5 failed attempts from the same IP (`429 locked`) | | OAuth client registration | 60 registrations per hour per IP (`429 slow_down`) | | OAuth token endpoint | 600 requests per hour per IP (`429 slow_down`) | | Ask (AI) | 40 questions per hour per user (`429 limit`) | | Inbound webhooks | 120 requests per hour per connection (Segment: 3,000; data sync, Aircall, RingCentral, Stripe: 600) | | List endpoints | Up to 100 rows per page; people, fair lead, and thread lists return at most 300 / 100 rows | | Sending email | Up to 10 recipients per message (to + cc); bulk sending is available only on the web | An installation admin can change the per-minute limit on the server with the `API_RATE_PER_MIN` environment variable. ## Recommended practices - When you receive a `429`, wait for the `Retry-After` interval and then retry; retrying without waiting keeps filling the counter. - Instead of bulk reads, paginate lists with `per=100` and fetch changes with `sort=update`. - Instead of constantly polling for events, receive notifications through [webhooks](/guides/webhooks). - To import a large number of records, use the [data sync webhook](/guides/inbound#sync) (500 rows per request) instead of individual `POST` requests. --- # Pagination and filtering > Page, scope, filter, and sort parameters on list endpoints. ## Pagination Company and opportunity lists are paginated: | Parameter | Default | Description | |---|---|---| | `page` | `1` | Page number. | | `per` | `40` | Rows per page (10–100). | The response includes `rows`, `total` (the filtered total), `page`, and `pages`: ```json { "ok": true, "rows": [ … ], "total": 21, "page": 1, "pages": 3 } ``` To fetch every record, increment `page` until it reaches `pages`: ```python def all_companies(session): page = 1 while True: j = session.get("https://ornek.solk.app/api/v1/customers", params={"scope": "all", "per": 100, "page": page}).json() yield from j["rows"] if page >= j["pages"]: break page += 1 ``` People, action, fair lead, and email thread lists are not paginated; they return at most 300 records, all open actions, 300 records, and 100 records, respectively. Use filters to narrow the results. ## Scope Most lists accept `scope`: | Value | Result | |---|---| | `mine` | Records you own (default for the sales role) | | `all` | All records you're allowed to see (default for the admin role) | | `` | That user's records | Scope does **not** expand visibility: in an installation with team visibility enabled, `scope=all` returns only your own team's records. See [Users and roles](/users-and-roles). ## Filters and sorting | Endpoint | Filters | Sort (`sort`) | |---|---|---| | [Companies](/rest-api/companies/list) | `q`, `status`, `city`, `visit`, `risk`, `active` | `name`, `visit`, `priority` | | [Opportunities](/rest-api/deals/list) | `q`, `stage` (`open`, `closed`, `account`, a stage key) | `update`, `health`, `value`, `sla` | | [People](/rest-api/people/list) | `q`, `former` | name | | [Actions](/rest-api/actions/list) | `state` (`today`, `overdue`, `soon`, `open`, `week`), `customer_id` | due date | | [Calendar](/rest-api/calendar/list) | `from`, `to`, `done` | start time | | [Fair leads](/rest-api/fairs/leads-list) | `q`, `interest`, `owner` | newest first | | [Threads](/rest-api/email/threads-list) | `kind` + `id`, or `filter`, `q` | by latest message | The `q` text search is case-insensitive and matches anywhere in the field. ## Fetching changes There is no separate "changes" endpoint. To sync: - Fetch opportunities with `sort=update` and stop based on the `last_update` field. - To learn about new and changed records immediately, use [webhooks](/guides/webhooks). --- # Errors > HTTP status codes, machine-readable error codes, and the extra fields that accompany errors. When a request fails, the response body has this shape: ```json { "ok": false, "error": "similar", "message": "Benzer adlı firma var — mevcut kaydı seçin ya da confirm_new=1 ile yeni açın.", "similar": [ { "id": 7, "name": "Alize Paketleme San. ve Tic. A.Ş.", "…": "…" } ] } ``` - **`error`** is a stable, machine-readable code — branch on it in your code. - **`message`** is a Turkish description you can show to users; its wording may change, so don't parse it. - Some errors carry extra fields: `similar`, `customer`, `gate_problems`, `module`, `state`, `otp`. ## Error codes | Code | HTTP | Meaning | |---|---|---| | `unauthorized` | 401 | Missing, invalid, expired or revoked token. | | `credentials` | 401 | Wrong username or password. | | `otp_required` | 401 | Two-factor code required (`otp`). | | `otp_invalid` | 401 | Wrong or expired two-factor code. | | `forbidden` | 403 | You may not see / edit this record. | | `insufficient_scope` | 403 | The OAuth token lacks `crm.write`. | | `module` | 403 | The module is off in the licence or for the user. | | `license` | 403 | The licence expired — admins only. | | `ip` | 403 | Access from this network is not allowed (IP restriction). | | `not_found` | 404 | Record not found. | | `invalid` | 400 | Missing or invalid field (`message` explains). | | `send` | 400 | The e-mail could not be sent (no mailbox, bad recipient…). | | `exists` | 409 | A company with this name exists (returned in `customer`). | | `similar` | 409 | Similar names exist (`similar`); send `confirm_new: true` to create anyway. | | `gate` | 409 | Stage gate not met (`gate_problems`). | | `locked` | 429 | Too many failed sign-ins — wait 60 s. | | `rate_limited` | 429 | Per-minute request limit exceeded (`Retry-After`). | | `limit` | 429 | Hourly question limit per user (40) reached. | ## HTTP status codes | Code | Meaning | |---|---| | `200` | Success. | | `201` | Created (OAuth client registration, SCIM user creation). | | `204` | Success with no content (SCIM user deactivation). | | `400` | Missing or invalid field. | | `401` | Authentication failed — refresh the token or sign in again. | | `403` | Not permitted — role, module, scope, license, or IP. | | `404` | The record doesn't exist or is outside what you're allowed to see. | | `405` | Method not supported (for example, `GET` on the MCP endpoint). | | `409` | Business rule conflict — duplicate company, stage gate. | | `429` | Rate limit — wait for the number of seconds in `Retry-After`. | | `500` | Server error. If it keeps happening, send the request and the time to `info@solk.app`. | ## Example: duplicate company ```python r = s.post(f"{BASE}/api/v1/customers", json={"name": "Alize Ambalaj"}) j = r.json() if r.status_code == 409 and j["error"] == "exists": company = j["customer"] # already exists — use it elif r.status_code == 409 and j["error"] == "similar": candidates = j["similar"] # let the user pick, or: j = s.post(f"{BASE}/api/v1/customers", json={"name": "Alize Ambalaj", "confirm_new": True}).json() ``` ## Example: stage gate ```python r = s.post(f"{BASE}/api/v1/opportunities/{oid}/stage", json={"stage": "Viable"}) if r.status_code == 409 and r.json()["error"] == "gate": for problem in r.json()["gate_problems"]: print("Missing:", problem) ``` --- # SCIM 2.0 > Automatically create, update, and deactivate users from Okta, Microsoft Entra ID, OneLogin, or JumpCloud. SCIM (System for Cross-domain Identity Management) automatically carries user changes from your identity provider into the CRM: new hires become CRM users, and people who leave lose access. | | | |---|---| | Base URL | `https://.solk.app/api/scim/v2` | | Authentication | `Authorization: Bearer ` | | Resources | `Users` (groups are not synced) | | Filter | `userName eq "…"`, `id eq "…"` | | Content type | `application/scim+json` | ## Setup :::steps ### Get a SCIM key In the CRM, connect the **Settings → Apps → Okta / Entra ID (SCIM)** (*Ayarlar → Uygulamalar → Okta / Entra ID (SCIM)*) card. The card shows the base URL and the SCIM key. The key is valid only on SCIM endpoints; you can't use it with the REST API. ### Configure your identity provider **Okta:** Applications → your app → Provisioning → Integration → *SCIM connector base URL* = the base URL, *Unique identifier field* = `userName`, *Authentication Mode* = HTTP Header, *Authorization* = the key. In the **To App** section, enable *Create Users*, *Update User Attributes*, and *Deactivate Users*. **Microsoft Entra ID:** Enterprise applications → your app → Provisioning → Automatic → *Tenant URL* = the base URL, *Secret Token* = the key → **Test Connection**. ### Assign users Users you assign to the app in your identity provider are created in the CRM. ::: ## Behavior | In the identity provider | In the CRM | |---|---| | User assigned | The user is created with the **sales** role, and a password setup invitation is sent to their email. | | Name, email, department, or phone changed | The user is updated. | | User suspended / unassigned (`active: false`) | The user is deactivated: they can't sign in, and their mobile sessions and API keys are revoked. Their records remain. | | User deleted (`DELETE`) | The user is deactivated (not deleted). | | User reactivated | The user is reactivated (requires a free seat on the license). | - `userName` is used as the email address in the CRM; a second user with the same email can't be created (`409 uniqueness`). - Roles (admin / sales) and module permissions are assigned in the CRM; SCIM doesn't change them. - The last active admin can't be deactivated through SCIM. - The department comes from the `department` field in the SCIM enterprise extension and is used for [team visibility](/users-and-roles#visibility). ## Example: create a user ```bash curl https://ornek.solk.app/api/scim/v2/Users \ -H "Authorization: Bearer $SCIM_TOKEN" \ -H "Content-Type: application/scim+json" \ -d '{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "ayse.demir@ornekkimya.com.tr", "name": { "givenName": "Ayşe", "familyName": "Demir" }, "emails": [{ "value": "ayse.demir@ornekkimya.com.tr", "primary": true }], "active": true, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Satış" } }' ``` All SCIM endpoints and their real responses are in the reference section: [List users](/rest-api/scim/users-list), [Create a user](/rest-api/scim/users-create), [Update a user](/rest-api/scim/users-update), [Deactivate a user](/rest-api/scim/users-delete). ## Errors SCIM errors use the SCIM format: ```json { "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "401", "detail": "Geçersiz ya da eksik SCIM anahtarı." } ``` --- # Identify `GET /api/v1/me` Returns the user behind the token, their enabled modules, the installation's constants (stage keys and labels, task priorities, loss reasons…), the product list and the licence state. Call it first when your integration starts: stage keys differ per installation, so read `constants.stages` and `constants.stage_labels` from here. ## Response ```json 200 { "ai": false, "app": { "accent": "#0f3a44", "accent_dark": "#0b2b33", "company": "Örnek Kimya", "currencies": [ "EUR" ], "currency_base": "EUR", "currency_symbol": "€", "logo": "", "name": "Solk CRM", "opp_lifetime_days": 180, "site_url": "https://ornek.solk.app", "stage_sla": { "Expect to Close": 15, "Negotiation": 30, "Present Solution": 60, "Qualify": 30, "Viable": 60 }, "ticket_sla_hours": { "Acil": 4, "Düşük": 168, "Normal": 72, "Yüksek": 24 }, "unit_default": "kg", "units": [ "kg" ], "version": "v22" }, "constants": { "action_soon_days": 7, "barriers": [ "Fiyat" ], "customer_statuses": [ "AC" ], "demo_results": [ "Başarılı" ], "demo_statuses": [ "Henüz Deneme Aşamasına Gelmedik" ], "forecast_cats": [ "Pipeline" ], "lead_interest": [ "Sıcak" ], "lead_next_actions": [ "Teklif gönder" ], "lead_statuses": [ "Yeni" ], "lead_timings": [ "Hemen" ], "lead_visitor_types": [ "Potansiyel müşteri" ], "loss_reasons": [ "Fiyat" ], "makro_stages": [ "Takip" ], "next_actions": [ "Numune Gönder" ], "open_stages": [ "Qualify" ], "opp_line_label": "", "opp_status_labels": false, "opp_types": [ "Yeni Müşteri" ], "order_kinds": [ "İlk Sipariş" ], "stage_colors": { "Cancel": "#62767a", "Expect to Close": "#2d9d78", "Lost": "#c23934", "Negotiation": "#e0a228", "Present Solution": "#6b5bd6", "Qualify": "#8a9ba0", "Viable": "#2a78d6", "Win": "#24733f" }, "stage_gate": { "Expect to Close": "kapanış bekleniyor", "Negotiation": "fiyat / vade pazarlığı", "Present Solution": "aktif deneme, sonucu net", "Qualify": "BANT 4/4", "Viable": "f2f ziyaret + doğru ürün" }, "stage_labels": { "Cancel": "Cancel", "Expect to Close": "Expect to Close", "Lost": "Lost", "Negotiation": "Negotiation", "Present Solution": "Present Solution", "Qualify": "Qualify", "Viable": "Viable", "Win": "Win" }, "stage_rules": true, "stage_sla": { "Expect to Close": 15, "Negotiation": 30, "Present Solution": 60, "Qualify": 30, "Viable": 60 }, "stages": [ "Qualify" ], "task_priorities": [ "Düşük" ], "task_statuses": [ "Açık" ], "visit_period": { "AC": 90, "Prospect": 180 }, "visit_topics": [ "Yeni Ürünler" ], "visit_types": [ "F2F" ] }, "license": { "blocked": false, "customer": "Örnek Kimya", "days_left": null, "end": null, "label": "Deneme modu", "modules": [ "calendar" ], "no": null, "state": "trial" }, "mailbox": { "connected": true, "email": "deniz@ornekkimya.com.tr", "error": "", "signature": false, "status": "ok" }, "ok": true, "products": [ { "code": "HS30", "id": 5, "list_price": 6.4, "name": "HS 30 Isıl Yapışma Laki" }, { "code": "SB450", "id": 3, "list_price": 3.8, "name": "PU-SB 450 Solvent Bazlı Yapıştırıcı" } ], "user": { "email": "deniz@ornekkimya.com.tr", "full_name": "Deniz Aksoy", "id": 1, "modules": [ "calendar" ], "role": "admin", "role_label": "Yönetici", "username": "admin" }, "users": [ { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } ], "weak_password": true } ``` --- # Log in with username `POST /api/v1/login` The sign-in endpoint used by the mobile app: exchanges username + password (+ two-factor code) for a session token starting with `xk_`. The token lives `API_TOKEN_DAYS` days (default 90). For server-to-server integrations use an **API key** from the Developers page or **OAuth** instead. ## Body | Field | Type | Description | |---|---|---| | `username` (required) | string | Username. | | `password` (required) | string | Password. | | `otp` | string | 6-digit code when two-factor auth is on (the first request returns `otp_required`). | | `device` | string | Device name shown in the session list. | ## Response ```json 200 { "app": { "accent": "#0f3a44", "accent_dark": "#0b2b33", "company": "Örnek Kimya", "currencies": [ "EUR" ], "currency_base": "EUR", "currency_symbol": "€", "logo": "", "name": "Solk CRM", "opp_lifetime_days": 180, "site_url": "https://ornek.solk.app", "stage_sla": { "Expect to Close": 15, "Negotiation": 30, "Present Solution": 60, "Qualify": 30, "Viable": 60 }, "ticket_sla_hours": { "Acil": 4, "Düşük": 168, "Normal": 72, "Yüksek": 24 }, "unit_default": "kg", "units": [ "kg" ], "version": "v22" }, "license": { "blocked": false, "customer": "Örnek Kimya", "days_left": null, "end": null, "label": "Deneme modu", "modules": [ "calendar" ], "no": null, "state": "trial" }, "ok": true, "token": "xk_1dcb806508e33cee8d728d9baafc6eee23b6e2cb16da8afd", "user": { "email": "deniz@ornekkimya.com.tr", "full_name": "Deniz Aksoy", "id": 1, "modules": [ "calendar" ], "role": "admin", "role_label": "Yönetici", "username": "admin" }, "weak_password": true } ``` --- # Log out `POST /api/v1/logout` Revokes the token used for the request. Meant for mobile session tokens (`xk_`); API keys are revoked on the Developers page. ## Response ```json 200 { "ok": true } ``` --- # Today `GET /api/v1/today` The user's day: today's meetings and actions, overdue items, recently opened records (companies, opportunities, people, leads) and this week's visit / task targets. The mobile home screen is drawn from this response. ## Response ```json 200 { "cards": { "contacts": { "mine": [ { "customer": "Orkide Medikal Ambalaj Ltd. Şti.", "customer_id": 30, "department": "Üretim", "email": "ugur.ozkan@orkidemedikal.example", "id": 57, "is_former": false, "is_main": true, "name": "Uğur Özkan", "note": "", "phone": "0 (262) 000 91 77", "rid": "003LlalbBDATepx", "title": "Üretim Müdürü", "wa": "902620009177" } ], "n": { "mine": 57, "recent": 0, "team": 57 }, "recent": [], "team": [ { "customer": "Orkide Medikal Ambalaj Ltd. Şti.", "customer_id": 30, "department": "Üretim", "email": "ugur.ozkan@orkidemedikal.example", "id": 57, "is_former": false, "is_main": true, "name": "Uğur Özkan", "note": "", "phone": "0 (262) 000 91 77", "rid": "003LlalbBDATepx", "title": "Üretim Müdürü", "wa": "902620009177" } ] }, "customers": { "mine": [ { "active": true, "at_risk": false, "city": "Bursa", "currency": "EUR", "currency_default": "", "id": 25, "initials": "AE", "last_visit": "2026-07-24", "main_contact": { "email": "kaan.ozkan@akasyaesnek.example", "name": "Kaan Özkan", "phone": "0 (224) 000 17 41", "wa": "902240001741" }, "name": "Akasya Esnek Ambalaj San. ve Tic. A.Ş.", "next_visit_due": "2027-01-20", "open_opps": 1, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "soon", "period": 180, "phone": "0 (224) 000 35 36", "potential_kg": 40000.0, "rid": "001Yk1AKJ1EIEQw", "sector": "Esnek ambalaj", "status": "Prospect", "status_label": "Prospect", "unit": "kg", "visit_overdue_days": -110, "visit_state": "ok" } ], "n": { "mine": 30, "recent": 0, "team": 30 }, "recent": [], "team": [ { "active": true, "at_risk": false, "city": "Bursa", "currency": "EUR", "currency_default": "", "id": 25, "initials": "AE", "last_visit": "2026-07-24", "main_contact": { "email": "kaan.ozkan@akasyaesnek.example", "name": "Kaan Özkan", "phone": "0 (224) 000 17 41", "wa": "902240001741" }, "name": "Akasya Esnek Ambalaj San. ve Tic. A.Ş.", "next_visit_due": "2027-01-20", "open_opps": 1, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "soon", "period": 180, "phone": "0 (224) 000 35 36", "potential_kg": 40000.0, "rid": "001Yk1AKJ1EIEQw", "sector": "Esnek ambalaj", "status": "Prospect", "status_label": "Prospect", "unit": "kg", "visit_overdue_days": -110, "visit_state": "ok" } ] }, "leads": { "mine": [ { "can_edit": true, "city": "Bursa", "company": "Işıltı Ambalaj San. Tic. Ltd. Şti.", "contact_name": "Mehmet Arslan", "created_at": "2026-08-28T14:10:00", "customer_id": null, "email": "mehmet@isiltiambalajsanti.example", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 4, "initials": "IA", "interest": "Soğuk", "line": "", "next_action": "Numune gönder", "next_date": "2026-10-07", "note": "Stantta görüşüldü.", "opp_id": null, "opp_type": "Mevcut Müşteri — Değişim", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "0 (224) 000 96 45", "potential_kg": 6000.0, "products": "4", "products_text": "", "project_note": "", "rid": "00QCwrpUC4bpt3E", "sector": "Gıda ambalajı", "status": "Teklif verildi", "status_eff": "Teklif verildi", "supplier": "Mevcut tedarikçi (Asya)", "task_id": null, "timing": "6 ay içinde", "title": "Kalite Kontrol Müdürü", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "902240009645", "website": "" } ], "n": { "mine": 9, "recent": 0, "team": 3 }, "recent": [], "team": [ { "can_edit": true, "city": "Konya", "company": "Alize Ambalaj A.Ş.", "contact_name": "Barış Kaya", "created_at": "2026-08-25T15:10:00", "customer_id": null, "email": "baris@alizeambalajas.example", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 5, "initials": "AA", "interest": "Sıcak", "line": "", "next_action": "Teklif gönder", "next_date": "2026-10-03", "note": "Stantta görüşüldü.", "opp_id": null, "opp_type": "Yeni Müşteri", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "0 (332) 000 44 97", "potential_kg": 4000.0, "products": "5", "products_text": "", "project_note": "", "rid": "00QiUPO55IJhtmL", "sector": "Etiket", "status": "Yeni", "status_eff": "Yeni", "supplier": "Mevcut tedarikçi (Asya)", "task_id": null, "timing": "3 ay içinde", "title": "Genel Müdür", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "903320004497", "website": "" } ] }, "opps": { "mine": [ { "bant": 4, "base_currency": "EUR", "close_date": "2026-11-17", "cur_sym": "€", "currency": "EUR", "customer": "Begonya Esnek Ambalaj A.Ş.", "customer_city": "Kocaeli", "customer_id": 16, "days_in_stage": 21, "detail_status": "", "fc": "Commit", "health": { "grade": "success", "label": "sağlıklı", "score": 100 }, "health_color": "g", "health_manual": false, "health_note": "", "id": 16, "initials": "BE", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 105, "line": "", "name": "Mevcut Müşteri — Değişim · 2026", "next_step": "Vade ve teslim koşullarını görüş", "offer_price": 4.08, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 4000.0, "prob": 0.75, "probability": null, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "product_id": 1, "pu": "€/kg", "rid": "006aJYGE9HUIaM2", "sla_days": 30, "sla_left": 9, "source": "Bayi / distribütör", "stage": "Negotiation", "stage_label": "Negotiation", "start_date": "2026-07-19", "trend": "", "trend_label": "Otomatik", "type": "Mevcut Müşteri — Değişim", "unit": "kg", "unit_code": "kg", "value": 195840.0, "value_base": 195840.0, "value_short": "196K" } ], "n": { "mine": 28, "recent": 0, "team": 28 }, "recent": [], "team": [ { "bant": 4, "base_currency": "EUR", "close_date": "2026-11-17", "cur_sym": "€", "currency": "EUR", "customer": "Begonya Esnek Ambalaj A.Ş.", "customer_city": "Kocaeli", "customer_id": 16, "days_in_stage": 21, "detail_status": "", "fc": "Commit", "health": { "grade": "success", "label": "sağlıklı", "score": 100 }, "health_color": "g", "health_manual": false, "health_note": "", "id": 16, "initials": "BE", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 105, "line": "", "name": "Mevcut Müşteri — Değişim · 2026", "next_step": "Vade ve teslim koşullarını görüş", "offer_price": 4.08, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 4000.0, "prob": 0.75, "probability": null, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "product_id": 1, "pu": "€/kg", "rid": "006aJYGE9HUIaM2", "sla_days": 30, "sla_left": 9, "source": "Bayi / distribütör", "stage": "Negotiation", "stage_label": "Negotiation", "start_date": "2026-07-19", "trend": "", "trend_label": "Otomatik", "type": "Mevcut Müşteri — Değişim", "unit": "kg", "unit_code": "kg", "value": 195840.0, "value_base": 195840.0, "value_short": "196K" } ] } }, "day_label": "Cuma, 2 Ekim 2026", "events": [ { "act": { "addr": "Kayseri Organize Sanayi Bölgesi, 1. Cadde No: 46, Kayseri, Kayseri", "c": 4, "mail": "nazli.aydin@ihlamurmatbaacilik.example", "n": "Ihlamur Matbaacılık A.Ş.", "tel": "0 (352) 000 57 99", "url": "/customers/4", "wa": "903520005799", "who": "Nazlı Aydın" }, "customer_id": 4, "date": "2026-10-02", "kind": "opp", "sub": "Mevcut Müşteri — Ek Satış · 2026 · Qualify", "time": null, "title": "Ihlamur Matbaacılık A.Ş. · sonraki adım", "url": "/opportunities/34" }, { "act": { "addr": "Sakarya Organize Sanayi Bölgesi, 4. Cadde No: 54, Sakarya, Sakarya", "c": 10, "mail": "murat.cetin@reyhanpaketleme.example", "n": "Reyhan Paketleme Ltd. Şti.", "tel": "0 (264) 000 63 67", "url": "/customers/10", "wa": "902640006367", "who": "Murat Çetin" }, "customer_id": 10, "date": "2026-10-02", "kind": "opp", "sub": "Mevcut Müşteri — Ek Satış · 2026 · Expect to Close", "time": null, "title": "Reyhan Paketleme Ltd. Şti. · sonraki adım", "url": "/opportunities/10" } ], "focus": [ { "cls": "bad", "text": "9 gecikmiş aksiyon" }, { "cls": "warn", "text": "3 aksiyon bugün" } ], "greeting": "İyi akşamlar", "ok": true, "today": "2026-10-02", "todos": { "all": [ { "customer": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "customer_id": 3, "date": "2026-09-26", "icon": "📋", "id": 1, "key": "task:1", "kind": "task", "label": "Numune sonucu için ara", "opp": "Yeni Müşteri · 2026", "opp_id": 33, "src": "Görev", "state": "overdue", "url": "/action/task/1" } ], "overdue": [ { "customer": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "customer_id": 3, "date": "2026-09-26", "icon": "📋", "id": 1, "key": "task:1", "kind": "task", "label": "Numune sonucu için ara", "opp": "Yeni Müşteri · 2026", "opp_id": 33, "src": "Görev", "state": "overdue", "url": "/action/task/1" } ], "today": [ { "customer": "Işıltı Film A.Ş.", "customer_id": 8, "date": "2026-10-02", "icon": "📋", "id": 2, "key": "task:2", "kind": "task", "label": "Teklif revizyonunu gönder", "opp": "", "opp_id": null, "src": "Görev", "state": "soon", "url": "/action/task/2" } ] }, "unread": 0, "week": { "demos": 1, "f2f": 3, "f2f_target": 6, "label": "28 Eyl – 4 Eki", "sub_fresh": false, "sub_week": null, "tasks_done": 2, "tasks_target": 12, "visit_target": 10, "visits": 7 } } ``` --- # Sales dashboard `GET /api/v1/dashboard` Opportunity count and value per stage (in base currency), SLA overruns, companies due for a visit, next steps, upcoming items and weekly targets. ## Query parameters | Field | Type | Description | |---|---|---| | `scope` | string | Empty = default for my role, `team` = whole team (admins). | ## Response ```json 200 { "k": { "ac": 9, "at_risk": 0, "demo_all": 28, "demo_ok": 22, "demo_open": 6, "ok_visits": 29, "open_actions": 13, "opp_value": 11243660.0, "opp_weighted": 5169831.0, "opps": 28, "overdue_actions": 9, "overdue_visits": 1, "pot_kg": 331000.0, "prospect": 21, "prospect_pot": 21, "soon_actions": 30, "soon_visits": 0 }, "last_demos": [ { "action_date": "2026-10-06", "action_done": false, "action_state": "soon", "customer_id": 19, "date": "2026-10-01", "id": 19, "line": "", "next_action": "Takip", "opp_id": 19, "product": "HS 30 Isıl Yapışma Laki", "product_id": 5, "result": "Beklemede", "rid": "a0DuuEJElBgUsAw", "status": "Deneme Gönderildi, Yapılması Bekleniyor", "tech_note": "Numune hatta; sonuç bekleniyor.", "user": "Deniz Aksoy" }, { "action_date": "2026-10-03", "action_done": false, "action_state": "soon", "customer_id": 20, "date": "2026-09-23", "id": 20, "line": "", "next_action": "Takip", "opp_id": 20, "product": "PU-SB 450 Solvent Bazlı Yapıştırıcı", "product_id": 3, "result": "Beklemede", "rid": "a0DLsCXfV1D035V", "status": "Deneme Gönderildi, Yapılması Bekleniyor", "tech_note": "Numune hatta; sonuç bekleniyor.", "user": "Deniz Aksoy" } ], "mine": false, "n_actions": 52, "never_visited": 0, "next_steps": [ { "bant": 4, "base_currency": "EUR", "close_date": "2027-01-16", "cur_sym": "€", "currency": "EUR", "customer": "Hanımeli Etiket San. Tic. Ltd. Şti.", "customer_city": "Adana", "customer_id": 23, "days_in_stage": 74, "detail_status": "", "fc": "Best Case", "health": { "grade": "warning", "label": "dikkat", "score": 60 }, "health_color": "g", "health_manual": true, "health_note": "", "id": 23, "initials": "HE", "is_account": false, "is_open": true, "last_update": "2026-09-30", "lifetime_left": 69, "line": "", "name": "Mevcut Müşteri — Yenileme · 2026", "next_step": "Deneme tarihini planla", "offer_price": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": 12000.0, "prob": 0.5, "probability": null, "product": "PU-SL 350 Yüksek Performans (retort)", "product_id": 2, "pu": "€/kg", "rid": "006LsST7gv514nh", "sla_days": 60, "sla_left": -14, "source": "Web formu", "stage": "Present Solution", "stage_label": "Present Solution", "start_date": "2026-06-13", "trend": "up", "trend_label": "Yeşil", "type": "Mevcut Müşteri — Yenileme", "unit": "kg", "unit_code": "kg", "value": 806000.0, "value_base": 806000.0, "value_short": "806K" }, { "bant": 4, "base_currency": "EUR", "close_date": "2026-12-20", "cur_sym": "€", "currency": "EUR", "customer": "Akasya Esnek Ambalaj San. ve Tic. A.Ş.", "customer_city": "Bursa", "customer_id": 25, "days_in_stage": 68, "detail_status": "", "fc": "Pipeline", "health": { "grade": "warning", "label": "dikkat", "score": 66 }, "health_color": "y", "health_manual": true, "health_note": "", "id": 25, "initials": "AE", "is_account": false, "is_open": true, "last_update": "2026-09-22", "lifetime_left": 99, "line": "", "name": "Yeni Müşteri · 2026", "next_step": "Numune gönder", "offer_price": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": 8000.0, "prob": 0.25, "probability": null, "product": "PU-SB 450 Solvent Bazlı Yapıştırıcı", "product_id": 3, "pu": "€/kg", "rid": "006Hyi3NMdZrXvO", "sla_days": 60, "sla_left": -8, "source": "Mevcut müşteri", "stage": "Viable", "stage_label": "Viable", "start_date": "2026-07-13", "trend": "flat", "trend_label": "Otomatik", "type": "Yeni Müşteri", "unit": "kg", "unit_code": "kg", "value": 365000.0, "value_base": 365000.0, "value_short": "365K" } ], "ok": true, "sla_over": 4, "stage_counts": { "Expect to Close": 4, "Negotiation": 5, "Present Solution": 6, "Qualify": 7, "Viable": 6 }, "stage_value": { "Expect to Close": 1416240.0, "Negotiation": 2424420.0, "Present Solution": 2287000.0, "Qualify": 2304000.0, "Viable": 2812000.0 }, "upcoming": [ { "date": "2026-10-02", "head": "Reyhan Paketleme Ltd. Şti.", "kind": "opp", "sub": "Proje #10 · Expect to Close · sonraki adım", "url": "/opportunities/10" }, { "date": "2026-10-02", "head": "Yıldızlı Paketleme San. Tic. Ltd. Şti.", "kind": "opp", "sub": "Proje #19 · Present Solution · sonraki adım", "url": "/opportunities/19" } ], "visit_due": [ { "active": true, "at_risk": false, "city": "Adana", "currency": "EUR", "currency_default": "", "id": 9, "initials": "KP", "last_visit": "2026-07-02", "main_contact": { "email": "ayse.aydin@kumsalplastik.example", "name": "Ayşe Aydın", "phone": "0 (322) 000 80 24", "wa": "903220008024" }, "name": "Kumsal Plastik Ambalaj San. ve Tic. A.Ş.", "next_visit_due": "2026-09-30", "open_opps": 0, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 90, "phone": "0 (322) 000 53 49", "potential_kg": 12000.0, "rid": "001t29x5kWbHOxg", "sector": "Esnek ambalaj", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": 2, "visit_state": "overdue" } ], "week": { "demos": 1, "f2f": 3, "f2f_target": 6, "label": "28 Eyl – 4 Eki", "n_sales": 1, "sub_fresh": false, "tasks_done": 2, "tasks_due": 10, "tasks_target": 12, "visit_target": 10, "visits": 7, "we": "2026-10-04", "ws": "2026-09-28" } } ``` --- # Search `GET /api/v1/search` Searches companies (name, city), people (name, phone, e-mail), opportunities and fair leads; up to 10–15 results per type. Passing a 15-character record ID (`001…`, `006…`) resolves it in the `record` field. ## Query parameters | Field | Type | Description | |---|---|---| | `q` (required) | string | Search text (at least 2 characters). | ## Response ```json 200 { "contacts": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "department": "Satın Alma", "email": "selin.dogan@alizepaketleme.example", "id": 1, "is_former": false, "is_main": true, "name": "Selin Doğan", "note": "", "phone": "0 (212) 000 55 24", "rid": "003MaZR2tfJCp1h", "title": "Satın Alma Müdürü", "wa": "902120005524" } ], "customers": [ { "active": true, "at_risk": false, "city": "İstanbul", "currency": "EUR", "currency_default": "", "id": 1, "initials": "AP", "last_visit": "2026-09-30", "main_contact": { "email": "selin.dogan@alizepaketleme.example", "name": "Selin Doğan", "phone": "0 (212) 000 55 24", "wa": "902120005524" }, "name": "Alize Paketleme San. ve Tic. A.Ş.", "next_visit_due": "2026-12-29", "open_opps": 1, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "overdue", "period": 90, "phone": "0 (212) 000 15 65", "potential_kg": 8000.0, "rid": "001RBoHIo80N8nR", "sector": "Gıda ambalajı", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": -88, "visit_state": "ok" } ], "leads": [ { "can_edit": true, "city": "Konya", "company": "Alize Ambalaj A.Ş.", "contact_name": "Barış Kaya", "created_at": "2026-08-25T15:10:00", "customer_id": null, "email": "baris@alizeambalajas.example", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 5, "initials": "AA", "interest": "Sıcak", "line": "", "next_action": "Teklif gönder", "next_date": "2026-10-03", "note": "Stantta görüşüldü.", "opp_id": null, "opp_type": "Yeni Müşteri", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "0 (332) 000 44 97", "potential_kg": 4000.0, "products": "5", "products_text": "", "project_note": "", "rid": "00QiUPO55IJhtmL", "sector": "Etiket", "status": "Yeni", "status_eff": "Yeni", "supplier": "Mevcut tedarikçi (Asya)", "task_id": null, "timing": "3 ay içinde", "title": "Genel Müdür", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "903320004497", "website": "" } ], "ok": true, "opps": [ { "bant": 0, "base_currency": "EUR", "close_date": "2027-03-31", "cur_sym": "€", "currency": "EUR", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_city": "İstanbul", "customer_id": 1, "days_in_stage": 0, "detail_status": "", "fc": "Omitted", "health": null, "health_color": null, "health_manual": false, "health_note": "", "id": 44, "initials": "AP", "is_account": true, "is_open": false, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "name": "Müşteri Takibi", "next_step": null, "offer_price": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": null, "prob": 0.0, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006w4ETEfW9soDW", "sla_days": null, "sla_left": null, "source": "", "stage": "Account", "stage_label": "Müşteri Takibi", "start_date": "2026-10-02", "trend": "", "trend_label": "Otomatik", "type": "Müşteri Takibi", "unit": "kg", "unit_code": "kg", "value": 0, "value_base": 0, "value_short": "0" }, { "bant": 4, "base_currency": "EUR", "close_date": "2026-10-01", "cur_sym": "€", "currency": "EUR", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_city": "İstanbul", "customer_id": 1, "days_in_stage": 1, "detail_status": "", "fc": "Closed", "health": null, "health_color": null, "health_manual": false, "health_note": "", "id": 1, "initials": "AP", "is_account": false, "is_open": false, "last_update": "2026-10-01", "lifetime_left": 115, "line": "", "name": "Yeni Müşteri · 2026", "next_step": null, "offer_price": 3.92, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 120, "potential_kg": 1500.0, "prob": 1.0, "probability": null, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "product_id": 1, "pu": "€/kg", "rid": "006kmnffbRT8muo", "sla_days": null, "sla_left": null, "source": "Bayi / distribütör", "stage": "Win", "stage_label": "Win", "start_date": "2026-07-29", "trend": "", "trend_label": "Otomatik", "type": "Yeni Müşteri", "unit": "kg", "unit_code": "kg", "value": 70560.0, "value_base": 70560.0, "value_short": "71K" } ], "record": null } ``` --- # List notifications `GET /api/v1/notifications` The user's latest 50 notifications and the unread count. ## Response ```json 200 { "ok": true, "rows": [], "unread": 0 } ``` --- # Mark notifications read `POST /api/v1/notifications/read` Marks the given notifications (or all, when `ids` is omitted) as read. ## Body | Field | Type | Description | |---|---|---| | `ids` | array | Notification ids. | ## Response ```json 200 { "marked": 0, "ok": true } ``` --- # Add to recently viewed `POST /api/v1/recent` Adds a record to the user's recently viewed list (cards on Today). ## Body | Field | Type | Description | |---|---|---| | `kind` (required) | string | Record type. Values: `customer`, `opp`, `contact`, `lead` | | `id` (required) | integer | Record id. | ## Response ```json 200 { "ok": true } ``` --- # List companies `GET /api/v1/customers` Filters, sorts and pages active companies. `counts` gives customer / prospect totals and overdue visits for the filtered list. ## Query parameters | Field | Type | Description | |---|---|---| | `scope` | string | `mine` = records I own (default for sales users), `all` = every record I can see (default for admins), or a user id. | | `status` | string | `AC` = active customer, `Prospect` = prospect. Values: `AC`, `Prospect` | | `q` | string | Name, city or sector contains. | | `city` | string | City (exact match). | | `visit` | string | Visit schedule state. Values: `overdue`, `soon`, `ok`, `none` | | `risk` | string | `1` = only customers at risk. | | `active` | string | `0` = inactive (archived) companies. | | `sort` | string | Sort order. Values: `name`, `visit`, `priority` | | `page` | integer | Page number (starts at 1). | | `per` | integer | Rows per page (10–100). | ## Response ```json 200 { "counts": { "ac": 9, "overdue": 1, "prospect": 21, "soon": 0 }, "ok": true, "page": 1, "pages": 3, "rows": [ { "active": true, "at_risk": false, "city": "Adana", "currency": "EUR", "currency_default": "", "id": 9, "initials": "KP", "last_visit": "2026-07-02", "main_contact": { "email": "ayse.aydin@kumsalplastik.example", "name": "Ayşe Aydın", "phone": "0 (322) 000 80 24", "wa": "903220008024" }, "name": "Kumsal Plastik Ambalaj San. ve Tic. A.Ş.", "next_visit_due": "2026-09-30", "open_opps": 0, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 90, "phone": "0 (322) 000 53 49", "potential_kg": 12000.0, "rid": "001t29x5kWbHOxg", "sector": "Esnek ambalaj", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": 2, "visit_state": "overdue" }, { "active": true, "at_risk": false, "city": "Konya", "currency": "EUR", "currency_default": "", "id": 8, "initials": "IF", "last_visit": "2026-08-25", "main_contact": { "email": "gizem.arslan@isiltifilm.example", "name": "Gizem Arslan", "phone": "0 (332) 000 94 28", "wa": "903320009428" }, "name": "Işıltı Film A.Ş.", "next_visit_due": "2026-11-23", "open_opps": 0, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 90, "phone": "0 (332) 000 99 70", "potential_kg": 40000.0, "rid": "001frrIzBuo8ERp", "sector": "Film üretimi", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": -52, "visit_state": "ok" } ], "total": 30 } ``` --- # Get a company `GET /api/v1/customers/{id}` The full company card: people, opportunities, recent visits and trials, open actions, offers, orders, timeline, competitors, prices and e-mail threads (`threads`). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Company id. | ## Response ```json 200 { "customer": { "act": { "addr": "İstanbul Organize Sanayi Bölgesi, 3. Cadde No: 25, İstanbul, İstanbul", "c": 1, "mail": "selin.dogan@alizepaketleme.example", "n": "Alize Paketleme San. ve Tic. A.Ş.", "tel": "0 (212) 000 55 24", "url": "/customers/1", "wa": "902120005524", "who": "Selin Doğan" }, "active": true, "address": "İstanbul Organize Sanayi Bölgesi, 3. Cadde No: 25, İstanbul", "at_risk": false, "barrier": "", "can_edit": true, "city": "İstanbul", "commit_kg": 1700.0, "competitors": [], "contacts": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "department": "Satın Alma", "email": "selin.dogan@alizepaketleme.example", "id": 1, "is_former": false, "is_main": true, "name": "Selin Doğan", "note": "", "phone": "0 (212) 000 55 24", "rid": "003MaZR2tfJCp1h", "title": "Satın Alma Müdürü", "wa": "902120005524" } ], "created_at": "2026-03-25T09:00:00", "currency": "EUR", "currency_default": "", "demos": [ { "action_date": "2026-09-02", "action_done": true, "action_state": "done", "customer_id": 1, "date": "2026-08-28", "id": 1, "line": "", "next_action": "Fiyat Teklifi", "opp_id": 1, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "product_id": 1, "result": "Başarılı", "rid": "a0DZ6tZKUceyZxW", "status": "Deneme Gerçekleştirilmiştir", "tech_note": "Mürekkep uyumu iyi; şeffaflık beklentiyi karşıladı.", "user": "Deniz Aksoy" } ], "events": [ { "badge": null, "dot": "#94a3b8", "icon": "geo-alt", "kind": "plan", "note": "", "opp_id": null, "planned": true, "state": null, "sub": "periyot 90 gün · son ziyaret 30.09.2026", "title": "Planlanan: ziyaret vadesi", "url": "", "when": "2026-12-29", "who": "" } ], "extra_kg": 800.0, "id": 1, "initials": "AP", "last_visit": "2026-09-30", "main_contact": { "email": "selin.dogan@alizepaketleme.example", "name": "Selin Doğan", "phone": "0 (212) 000 55 24", "wa": "902120005524" }, "mgmt_note": "", "mgmt_support": false, "name": "Alize Paketleme San. ve Tic. A.Ş.", "next_visit_due": "2026-12-29", "note": "", "offers": [ { "currency": "EUR", "date": "2026-09-09", "id": 1, "is_primary": true, "kg": 1500.0, "lines": 1, "locked": true, "monthly_amount": 5880.0, "payment_days": 120, "pdf_url": "/offers/1/pdf", "price": 3.92, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "pu": "€/kg", "quote_no": "TKL-2026-0001", "state": "presented", "status": "Kabul", "unit": "kg", "valid_until": "2026-10-09" } ], "open_actions": [ { "customer": "", "customer_id": null, "date": "2026-09-30", "icon": "⚡", "id": 74, "key": "visit:74", "kind": "visit", "label": "Ziyaret Planla", "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_id": 31, "src": "Ziyaret", "state": "overdue", "url": "/action/visit/74" } ], "open_opps": 1, "opps": [ { "bant": 0, "base_currency": "EUR", "close_date": "2027-03-31", "cur_sym": "€", "currency": "EUR", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_city": "İstanbul", "customer_id": 1, "days_in_stage": 0, "detail_status": "", "fc": "Omitted", "health": null, "health_color": null, "health_manual": false, "health_note": "", "id": 44, "initials": "AP", "is_account": true, "is_open": false, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "name": "Müşteri Takibi", "next_step": null, "offer_price": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": null, "prob": 0.0, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006w4ETEfW9soDW", "sla_days": null, "sla_left": null, "source": "", "stage": "Account", "stage_label": "Müşteri Takibi", "start_date": "2026-10-02", "trend": "", "trend_label": "Otomatik", "type": "Müşteri Takibi", "unit": "kg", "unit_code": "kg", "value": 0, "value_base": 0, "value_short": "0" } ], "orders": [ { "currency": "EUR", "date": "2026-10-02", "id": 1, "kg": 700.0, "kind": "İlk Sipariş", "note": "", "opp_id": 1, "payment_days": 120, "price": 3.92, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "pu": "€/kg", "unit": "kg" } ], "our_kg_current": 1600.0, "our_kg_target": 2400.0, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "overdue", "period": 90, "phone": "0 (212) 000 15 65", "potential_kg": 8000.0, "prices": [ { "currency": "EUR", "id": 1, "payment_days": 120, "price": 3.92, "product": "PU-SL 200 Solventsiz Laminasyon Yapıştırıcısı", "product_id": 1, "pu": "€/kg", "source": "TKL-2026-0001 (Win)", "valid_until": "2027-10-01" } ], "priority_score": 50.0, "rid": "001RBoHIo80N8nR", "sector": "Gıda ambalajı", "share_current": 20.0, "share_start": null, "share_target": 30.0, "status": "AC", "status_label": "Aktif Müşteri", "supplier_note": "Distribütör ürünü", "threads": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "id": 1, "last_at": "2026-10-01T15:06:59", "last_dir": "out", "n": 2, "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_how": "single", "opp_id": 31, "opp_note": "firmanın tek açık projesi", "peer": "selin.dogan@alizepaketleme.example", "subject": "Numune ve fiyat teklifi", "suggested": false, "waiting": false } ], "threads_n": 1, "unit": "kg", "visit_overdue_days": -88, "visit_period_days": null, "visit_postponed_until": null, "visit_state": "ok", "visits": [ { "action_date": "2026-09-30", "action_done": false, "action_state": "overdue", "contact": "Selin Doğan", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "date": "2026-09-30", "id": 74, "line": "", "next_action": "Ziyaret Planla", "note": "Hat 2'de tünelleşme sorunu konuşuldu; solventsiz sisteme geçiş planlıyorlar.", "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_id": 31, "product": "", "result": "Teklif istendi", "rid": "00U2wBTaDRg4JXL", "topic": "Tanışma", "type": "Call", "user": "Deniz Aksoy" } ], "website": "www.alizepaketleme.example" }, "ok": true } ``` --- # Create a company `POST /api/v1/customers` Creates a company (status `Prospect` unless given, owned by the caller). An existing name returns `409 exists`; similar names return `409 similar` with a `similar` list — send `confirm_new: true` to create anyway. Optionally creates the first contact. ## Body | Field | Type | Description | |---|---|---| | `name` (required) | string | Company name (max 200 characters). | | `status` | string | Status. Values: `AC`, `Prospect` | | `city` | string | City. | | `sector` | string | Sector. | | `phone` | string | Phone. | | `website` | string | Website. | | `address` | string | Address. | | `note` | string | Note. | | `owner_id` | integer | Owner user id (admins only). | | `visit_period_days` | integer | Visit every N days. | | `potential_kg` | number | Monthly potential quantity (installation's base unit). | | `contact_name` | string | First contact's name (becomes the main contact). | | `contact_title` | string | First contact's job title. | | `contact_email` | string | First contact's e-mail. | | `contact_phone` | string | First contact's phone. | | `confirm_new` | boolean | Create even if similar names exist. | ## Response ```json 200 { "customer": { "act": { "addr": "Bursa", "c": 31, "mail": "selin.kara@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 224 555 01 02", "url": "/customers/31", "wa": "902245550102", "who": "Selin Kara" }, "active": true, "address": "", "at_risk": false, "barrier": "", "can_edit": true, "city": "Bursa", "commit_kg": null, "competitors": [], "contacts": [ { "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "department": "", "email": "selin.kara@kuzeyplastik.com.tr", "id": 58, "is_former": false, "is_main": true, "name": "Selin Kara", "note": "", "phone": "", "rid": "003URRQi11tfUY7", "title": "Satın Alma Müdürü", "wa": "" } ], "created_at": "2026-10-02T20:07:00", "currency": "EUR", "currency_default": "", "demos": [], "events": [ { "badge": null, "dot": "#94a3b8", "icon": "building", "kind": "firma", "note": "", "opp_id": null, "planned": false, "state": null, "sub": "Firma #31 · Deniz Aksoy", "title": "Firma kaydı açıldı", "url": "", "when": "2026-10-02", "who": "" } ], "extra_kg": null, "id": 31, "initials": "KP", "last_visit": null, "main_contact": { "email": "selin.kara@kuzeyplastik.com.tr", "name": "Selin Kara", "phone": "", "wa": "" }, "mgmt_note": "", "mgmt_support": false, "name": "Kuzey Plastik Sanayi", "next_visit_due": null, "note": "", "offers": [], "open_actions": [], "open_opps": 0, "opps": [], "orders": [], "our_kg_current": null, "our_kg_target": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 180, "phone": "+90 224 555 01 02", "potential_kg": null, "prices": [], "priority_score": 0.0, "rid": "001QJi5Zr1Hk2VH", "sector": "Plastik", "share_current": null, "share_start": null, "share_target": null, "status": "Prospect", "status_label": "Prospect", "supplier_note": "", "threads": [], "threads_n": 0, "unit": "kg", "visit_overdue_days": 0, "visit_period_days": null, "visit_postponed_until": null, "visit_state": "none", "visits": [], "website": "kuzeyplastik.com.tr" }, "ok": true } ``` ```json 409 { "error": "similar", "message": "Benzer adlı firma var — mevcut kaydı seçin ya da confirm_new=1 ile yeni açın.", "ok": false, "similar": [ { "active": true, "at_risk": false, "city": "Kocaeli", "currency": "EUR", "currency_default": "", "id": 2, "initials": "FA", "last_visit": "2026-09-24", "main_contact": { "email": "elif.karaca@filizambalaj.example", "name": "Elif Karaca", "phone": "0 (262) 000 27 12", "wa": "902620002712" }, "name": "Filiz Ambalaj Ltd. Şti.", "next_visit_due": "2026-12-23", "open_opps": 1, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "soon", "period": 90, "phone": "0 (262) 000 83 56", "potential_kg": 40000.0, "rid": "001rutEPiRkEPfD", "sector": "Gıda ambalajı", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": -82, "visit_state": "ok" } ] } ``` --- # Update a company `PATCH /api/v1/customers/{id}` Only the fields you send change; an empty string clears a text field. `PUT` and `POST` are accepted too. Requires edit rights on the company (owner, admin or a user allowed by the visibility rule). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Company id. | ## Body | Field | Type | Description | |---|---|---| | `name` | string | Company name. | | `status` | string | Status. Values: `AC`, `Prospect` | | `owner_id` | integer | Owner (admins only). | | `visit_period_days` | integer | Visit frequency (days). | | `sector / city / phone / website / address / note` | string | Text fields. | | `supplier_note / barrier / mgmt_note` | string | Current supplier, barrier and management note. | | `potential_kg / commit_kg / our_kg_current / our_kg_target` | number | Quantity fields (monthly). | | `share_start / share_current / share_target` | number | Share-of-wallet percentages. | | `mgmt_support` | boolean | Management support. | ## Response ```json 200 { "customer": { "act": { "addr": "Bursa", "c": 31, "mail": "selin.kara@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 224 555 01 02", "url": "/customers/31", "wa": "902245550102", "who": "Selin Kara" }, "active": true, "address": "", "at_risk": true, "barrier": "", "can_edit": true, "city": "Bursa", "commit_kg": null, "competitors": [], "contacts": [ { "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "department": "", "email": "selin.kara@kuzeyplastik.com.tr", "id": 58, "is_former": false, "is_main": true, "name": "Selin Kara", "note": "", "phone": "", "rid": "003URRQi11tfUY7", "title": "Satın Alma Müdürü", "wa": "" } ], "created_at": "2026-10-02T20:07:00", "currency": "EUR", "currency_default": "", "demos": [], "events": [ { "badge": null, "dot": "#94a3b8", "icon": "building", "kind": "firma", "note": "", "opp_id": null, "planned": false, "state": null, "sub": "Firma #31 · Deniz Aksoy", "title": "Firma kaydı açıldı", "url": "", "when": "2026-10-02", "who": "" } ], "extra_kg": null, "id": 31, "initials": "KP", "last_visit": null, "main_contact": { "email": "selin.kara@kuzeyplastik.com.tr", "name": "Selin Kara", "phone": "", "wa": "" }, "mgmt_note": "", "mgmt_support": false, "name": "Kuzey Plastik Sanayi", "next_visit_due": null, "note": "Yıllık sözleşme görüşülüyor.", "offers": [], "open_actions": [], "open_opps": 0, "opps": [], "orders": [], "our_kg_current": null, "our_kg_target": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 30, "phone": "+90 224 555 01 02", "potential_kg": null, "prices": [], "priority_score": 0.0, "rid": "001QJi5Zr1Hk2VH", "sector": "Plastik", "share_current": null, "share_start": null, "share_target": null, "status": "AC", "status_label": "Aktif Müşteri", "supplier_note": "", "threads": [], "threads_n": 0, "unit": "kg", "visit_overdue_days": 0, "visit_period_days": 30, "visit_postponed_until": null, "visit_state": "none", "visits": [], "website": "kuzeyplastik.com.tr" }, "ok": true } ``` --- # Postpone the visit `POST /api/v1/customers/{id}/postpone` Moves the company's visit due date. Uses `date` when given, otherwise `days` from today (default 7). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Company id. | ## Body | Field | Type | Description | |---|---|---| | `date` | string | New date (YYYY-MM-DD). | | `days` | integer | Number of days. | | `note` | string | Reason. | ## Response ```json 200 { "customer": { "active": true, "at_risk": false, "city": "Bursa", "currency": "EUR", "currency_default": "", "id": 31, "initials": "KP", "last_visit": null, "main_contact": { "email": "selin.kara@kuzeyplastik.com.tr", "name": "Selin Kara", "phone": "", "wa": "" }, "name": "Kuzey Plastik Sanayi", "next_visit_due": "2026-10-16", "open_opps": 0, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": null, "period": 30, "phone": "+90 224 555 01 02", "potential_kg": null, "rid": "001QJi5Zr1Hk2VH", "sector": "Plastik", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": -14, "visit_state": "ok" }, "ok": true, "until": "2026-10-16" } ``` --- # Activity form options `GET /api/v1/customers/{id}/lookup` What an activity (quick entry) form needs: the company's open projects, the default project, people, open actions and last stage label. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Company id. | ## Response ```json 200 { "city": "İstanbul", "competitor": "Distribütör ürünü", "contacts": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "department": "Satın Alma", "email": "selin.dogan@alizepaketleme.example", "id": 1, "is_former": false, "is_main": true, "name": "Selin Doğan", "note": "", "phone": "0 (212) 000 55 24", "rid": "003MaZR2tfJCp1h", "title": "Satın Alma Müdürü", "wa": "902120005524" } ], "default_project": 31, "id": 1, "name": "Alize Paketleme San. ve Tic. A.Ş.", "ok": true, "open_actions": [ { "customer": "", "customer_id": null, "date": "2026-09-30", "icon": "⚡", "id": 74, "key": "visit:74", "kind": "visit", "label": "Ziyaret Planla", "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_id": 31, "src": "Ziyaret", "state": "overdue", "url": "/action/visit/74" } ], "opp_type": "Mevcut Müşteri — Yenileme", "potential_kg": 8000.0, "projects": [ { "id": 31, "name": "Mevcut Müşteri — Yenileme · 2026", "stage": "Qualify" }, { "id": 44, "name": "Müşteri Takibi", "stage": "Müşteri Takibi" } ], "stage": "Qualify", "stage_label": "", "status": "AC" } ``` --- # List people `GET /api/v1/contacts` People of active companies (max 300, by name). Former employees are excluded by default. ## Query parameters | Field | Type | Description | |---|---|---| | `scope` | string | `mine` = records I own (default for sales users), `all` = every record I can see (default for admins), or a user id. | | `q` | string | Name, company, phone, e-mail or title contains. | | `former` | string | `1` = include former employees. | ## Response ```json 200 { "ok": true, "rows": [ { "customer": "Ortanca Matbaacılık Ltd. Şti.", "customer_id": 22, "department": "Satın Alma", "email": "ahmet.aydin@ortancamatbaacilik.example", "id": 42, "is_former": false, "is_main": true, "name": "Ahmet Aydın", "note": "", "phone": "0 (332) 000 81 52", "rid": "003qMFfAArgzPDF", "title": "Satın Alma Müdürü", "wa": "903320008152" }, { "customer": "Kamelya Lamine Film Ltd. Şti.", "customer_id": 6, "department": "Teknik", "email": "ahmet.ozturk@kamelyalamine.example", "id": 11, "is_former": false, "is_main": true, "name": "Ahmet Öztürk", "note": "", "phone": "0 (232) 000 89 83", "rid": "003t3MMhbXHOFjR", "title": "Teknik Müdür", "wa": "902320008983" } ], "total": 58 } ``` --- # Create a person `POST /api/v1/customers/{id}/contacts` Adds a person to a company. The company's first person, or one sent with `is_main: true`, becomes the main contact. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Company id. | ## Body | Field | Type | Description | |---|---|---| | `name` (required) | string | Full name. | | `title` | string | Job title. | | `department` | string | Department. | | `email` | string | E-mail. | | `phone` | string | Phone. | | `is_main` | boolean | Make main contact. | | `note` | string | Note (250 characters). | ## Response ```json 200 { "contact": { "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "department": "Üretim", "email": "emre.yildiz@kuzeyplastik.com.tr", "id": 59, "is_former": false, "is_main": false, "name": "Emre Yıldız", "note": "", "phone": "+90 532 555 11 22", "rid": "0037CJrFvjDFvYX", "title": "Üretim Şefi", "wa": "905325551122" }, "ok": true } ``` --- # Update a person `PATCH /api/v1/contacts/{id}` Only the fields you send change. `is_former: true` marks the person as left (not deleted); `is_main: true` makes them the main contact. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Person id. | ## Body | Field | Type | Description | |---|---|---| | `name / title / department / phone / email / note` | string | Text fields. | | `is_main` | boolean | Make main contact. | | `is_former` | boolean | Left the company. | ## Response ```json 200 { "contact": { "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "department": "Üretim", "email": "emre.yildiz@kuzeyplastik.com.tr", "id": 59, "is_former": false, "is_main": true, "name": "Emre Yıldız", "note": "", "phone": "+90 532 555 11 22", "rid": "0037CJrFvjDFvYX", "title": "Fabrika Müdürü", "wa": "905325551122" }, "ok": true } ``` --- # List opportunities `GET /api/v1/opportunities` Filters, sorts and pages opportunities (sales projects). The response also carries open-stage counts, the open total in base currency and the number of SLA overruns. ## Query parameters | Field | Type | Description | |---|---|---| | `scope` | string | `mine` = records I own (default for sales users), `all` = every record I can see (default for admins), or a user id. | | `stage` | string | `open` (default), `closed`, `account` (customer follow-up containers) or a stage key (`/me` → `constants.stages`). | | `q` | string | Opportunity or company name contains. | | `sort` | string | Sort order. Values: `update`, `health`, `value`, `sla` | | `page` | integer | Page number (starts at 1). | | `per` | integer | Rows per page (10–100). | ## Response ```json 200 { "base_currency": "EUR", "ok": true, "page": 1, "pages": 3, "rows": [ { "bant": 4, "base_currency": "EUR", "close_date": "2026-11-12", "cur_sym": "€", "currency": "EUR", "customer": "Sardunya Etiket Ltd. Şti.", "customer_city": "Eskişehir", "customer_id": 14, "days_in_stage": 12, "detail_status": "", "fc": "Best Case", "health": { "grade": "success", "label": "sağlıklı", "score": 100 }, "health_color": "g", "health_manual": true, "health_note": "", "id": 14, "initials": "SE", "is_account": false, "is_open": true, "last_update": "2026-10-01", "lifetime_left": 106, "line": "", "name": "Mevcut Müşteri — Ek Satış · 2026", "next_step": "Vade ve teslim koşullarını görüş", "offer_price": 6.12, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 120, "potential_kg": 12000.0, "prob": 0.75, "probability": null, "product": "HS 30 Isıl Yapışma Laki", "product_id": 5, "pu": "€/kg", "rid": "006riIxDf4YpLLv", "sla_days": 30, "sla_left": 18, "source": "Soğuk arama", "stage": "Negotiation", "stage_label": "Negotiation", "start_date": "2026-07-20", "trend": "up", "trend_label": "Yeşil", "type": "Mevcut Müşteri — Ek Satış", "unit": "kg", "unit_code": "kg", "value": 881280.0, "value_base": 881280.0, "value_short": "881K" }, { "bant": 4, "base_currency": "EUR", "close_date": "2027-01-16", "cur_sym": "€", "currency": "EUR", "customer": "Hanımeli Etiket San. Tic. Ltd. Şti.", "customer_city": "Adana", "customer_id": 23, "days_in_stage": 74, "detail_status": "", "fc": "Best Case", "health": { "grade": "warning", "label": "dikkat", "score": 60 }, "health_color": "g", "health_manual": true, "health_note": "", "id": 23, "initials": "HE", "is_account": false, "is_open": true, "last_update": "2026-09-30", "lifetime_left": 69, "line": "", "name": "Mevcut Müşteri — Yenileme · 2026", "next_step": "Deneme tarihini planla", "offer_price": null, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": 12000.0, "prob": 0.5, "probability": null, "product": "PU-SL 350 Yüksek Performans (retort)", "product_id": 2, "pu": "€/kg", "rid": "006LsST7gv514nh", "sla_days": 60, "sla_left": -14, "source": "Web formu", "stage": "Present Solution", "stage_label": "Present Solution", "start_date": "2026-06-13", "trend": "up", "trend_label": "Yeşil", "type": "Mevcut Müşteri — Yenileme", "unit": "kg", "unit_code": "kg", "value": 806000.0, "value_base": 806000.0, "value_short": "806K" } ], "sla_over": 4, "stage_counts": { "Expect to Close": 4, "Negotiation": 5, "Present Solution": 6, "Qualify": 7, "Viable": 6 }, "total": 28, "value_open": 11243660.0 } ``` --- # Get an opportunity `GET /api/v1/opportunities/{id}` The full opportunity: stage, value, probability, health, checklist steps, BANT, stage-gate problems (`gate_problems`), visits, trials, offers, orders, tasks, stage history, notes and e-mail threads. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Opportunity id. | ## Response ```json 200 { "ok": true, "opp": { "bant": 3, "bant_a": true, "bant_b": true, "bant_n": false, "bant_t": true, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2027-02-06", "close_reason": "", "closed_at": null, "competitor": "Distribütör ürünü", "cur_sym": "€", "currency": "EUR", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_city": "İstanbul", "customer_id": 1, "days_in_stage": 2, "demos": [], "detail_status": "", "fc": "Pipeline", "gate_ok": false, "gate_problems": [ "BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)" ], "guide": "BANT 4/4", "health": { "grade": "success", "label": "sağlıklı", "score": 92 }, "health_color": "g", "health_manual": true, "health_note": "", "health_why": [ [ "−8" ] ], "id": 31, "initials": "AP", "is_account": false, "is_open": true, "last_update": "2026-09-20", "lifetime_left": 178, "line": "", "logs": [ { "date": "2026-09-30T08:00:00", "from": null, "note": "Proje açıldı", "to": "Qualify", "user": "Deniz Aksoy" } ], "name": "Mevcut Müşteri — Yenileme · 2026", "next_stage": "Viable", "next_step": "İhtiyaç, hat ve aylık miktar potansiyelini netleştir (Need)", "note": "", "notes": [], "offer_price": null, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": 8000.0, "prob": 0.1, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006YV7xczE9DnY2", "sla_days": 30, "sla_left": 28, "source": "Bayi / distribütör", "stage": "Qualify", "stage_label": "Qualify", "start_date": "2026-09-30", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": true, "done": true, "due_date": null, "id": 376, "note": "otomatik: BANT kutusu", "stage": "Qualify", "state": "done", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "id": 1, "last_at": "2026-10-01T15:06:59", "last_dir": "out", "n": 2, "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_how": "single", "opp_id": 31, "opp_note": "firmanın tek açık projesi", "peer": "selin.dogan@alizepaketleme.example", "subject": "Numune ve fiyat teklifi", "suggested": false, "waiting": false } ], "threads_n": 1, "trend": "up", "trend_label": "Yeşil", "type": "Mevcut Müşteri — Yenileme", "unit": "kg", "unit_code": "kg", "value": 403000.0, "value_base": 403000.0, "value_short": "403K", "visits": [ { "action_date": "2026-09-30", "action_done": false, "action_state": "overdue", "contact": "Selin Doğan", "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "date": "2026-09-30", "id": 74, "line": "", "next_action": "Ziyaret Planla", "note": "Hat 2'de tünelleşme sorunu konuşuldu; solventsiz sisteme geçiş planlıyorlar.", "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_id": 31, "product": "", "result": "Teklif istendi", "rid": "00U2wBTaDRg4JXL", "topic": "Tanışma", "type": "Call", "user": "Deniz Aksoy" } ], "weighted": 40300.0 } } ``` --- # Create an opportunity `POST /api/v1/opportunities` Opens a sales project for a company in the first open stage and sets up its stage steps. The annual value is quantity × unit price; `value` is used when those are missing. ## Body | Field | Type | Description | |---|---|---| | `customer_id` (required) | integer | Company id. | | `name` | string | Project name. | | `type` | string | Project type (`/me` → `constants.opp_types`). | | `product_id` | integer | Product (`/me` → `products`). | | `potential_kg` | number | Monthly quantity (in the product's unit). | | `offer_price` | number | Unit price. | | `payment_days` | integer | Payment terms (days). | | `value` | number | Annual value (when there is no quantity × price). | | `line` | string | Line / application. | | `competitor` | string | Competitor or current supplier. | | `note` | string | Note. | ## Response ```json 200 { "ok": true, "opp": { "bant": 0, "bant_a": false, "bant_b": false, "bant_n": false, "bant_t": false, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2027-03-31", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Pipeline", "gate_ok": false, "gate_problems": [ "BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)" ], "guide": "BANT 4/4", "health": { "grade": "success", "label": "sağlıklı", "score": 85 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": null, "note": "Proje açıldı (mobil)", "to": "Qualify", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Viable", "next_step": "Karar verici / yetkili kişiyi belirle (Authority)", "note": "", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.1, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 30, "sla_left": 30, "source": "", "stage": "Qualify", "stage_label": "Qualify", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": false, "done": false, "due_date": null, "id": 467, "note": "", "stage": "Qualify", "state": "open", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 33840.0 } } ``` --- # Update an opportunity `POST /api/v1/opportunities/{id}/fields` Frequently changed fields: quantity, unit price (recorded in offer history when it changes), payment terms, close date (logged), next step date, note, product, type, line, name, forecast category and stage label. Use the [stage endpoint](/rest-api/deals/stage) to move stages. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Opportunity id. | ## Body | Field | Type | Description | |---|---|---| | `potential_kg` | number | Monthly quantity. | | `offer_price` | number | Unit price. | | `payment_days` | integer | Payment terms (days). | | `close_date` | string | Close date (YYYY-MM-DD). | | `next_step_date` | string | Next step date. | | `note` | string | Note. | | `product_id` | integer | Product. | | `type` | string | Type. | | `line` | string | Line. | | `name` | string | Name. | | `forecast_cat` | string | Forecast category. Values: `Pipeline`, `Best Case`, `Commit`, `Closed`, `Omitted` | | `detail_status` | string | Detail label matching the stage (Makro pipeline). | ## Response ```json 200 { "ok": true, "opp": { "bant": 0, "bant_a": false, "bant_b": false, "bant_n": false, "bant_t": false, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2026-11-16", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Best Case", "gate_ok": false, "gate_problems": [ "BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)" ], "guide": "BANT 4/4", "health": { "grade": "success", "label": "sağlıklı", "score": 85 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": null, "note": "Proje açıldı (mobil)", "to": "Qualify", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Viable", "next_step": "Karar verici / yetkili kişiyi belirle (Authority)", "note": "Numune onayı bekleniyor.", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.1, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 30, "sla_left": 30, "source": "", "stage": "Qualify", "stage_label": "Qualify", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": false, "done": false, "due_date": null, "id": 467, "note": "", "stage": "Qualify", "state": "open", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 33840.0 } } ``` --- # Change stage `POST /api/v1/opportunities/{id}/stage` Moves the opportunity to another stage. Stage gates apply exactly like in the web app: an unmet rule returns `409 gate` with `gate_problems`. Send `reason` for Lost / Cancel. A won opportunity turns the company into a customer and emits `opp_won` to apps. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Opportunity id. | ## Body | Field | Type | Description | |---|---|---| | `stage` (required) | string | Target stage key (`/me` → `constants.stages`; closing keys are `Win`, `Lost`, `Cancel`). | | `note` | string | Note for the stage log. | | `reason` | string | Loss / cancel reason (`/me` → `constants.loss_reasons`). | ## Response ```json 200 { "messages": [ "Aşama güncellendi: Viable" ], "ok": true, "opp": { "bant": 4, "bant_a": true, "bant_b": true, "bant_n": true, "bant_t": true, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2026-11-16", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Best Case", "gate_ok": false, "gate_problems": [ "En az bir F2F ziyaret kaydı gerekir (Hızlı Giriş → tür F2F)" ], "guide": "f2f ziyaret + doğru ürün", "health": { "grade": "success", "label": "sağlıklı", "score": 80 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": "Qualify", "note": "İhtiyaç ve bütçe doğrulandı.", "to": "Viable", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Present Solution", "next_step": "F2F ziyaret planla ve gerçekleştir", "note": "Numune onayı bekleniyor.", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.25, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 60, "sla_left": 60, "source": "", "stage": "Viable", "stage_label": "Viable", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": true, "done": true, "due_date": null, "id": 467, "note": "otomatik: BANT kutusu", "stage": "Qualify", "state": "done", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 84600.0 } } ``` ```json 409 { "error": "gate", "gate_problems": [ "BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)" ], "message": "Viable'a geçmek için: BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)", "ok": false } ``` --- # Update BANT `POST /api/v1/opportunities/{id}/bant` Sets the Budget, Authority, Need and Timing criteria; the matching checklist steps are updated too. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Opportunity id. | ## Body | Field | Type | Description | |---|---|---| | `bant_b` | boolean | Budget confirmed. | | `bant_a` | boolean | Authority known. | | `bant_n` | boolean | Need confirmed. | | `bant_t` | boolean | Timing known. | ## Response ```json 200 { "message": "BANT güncellendi (4/4).", "ok": true, "opp": { "bant": 4, "bant_a": true, "bant_b": true, "bant_n": true, "bant_t": true, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2026-11-16", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Best Case", "gate_ok": true, "gate_problems": [], "guide": "BANT 4/4", "health": { "grade": "success", "label": "sağlıklı", "score": 90 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": null, "note": "Proje açıldı (mobil)", "to": "Qualify", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Viable", "next_step": "Ön koşullar tamam → Viable aşamasına geçir", "note": "Numune onayı bekleniyor.", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.1, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 30, "sla_left": 30, "source": "", "stage": "Qualify", "stage_label": "Qualify", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": true, "done": true, "due_date": null, "id": 467, "note": "otomatik: BANT kutusu", "stage": "Qualify", "state": "done", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 33840.0 } } ``` --- # Add a step `POST /api/v1/opportunities/{id}/steps` Adds a checklist step to the opportunity's current stage. Dated steps show up in Actions with kind `step`. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Opportunity id. | ## Body | Field | Type | Description | |---|---|---| | `title` (required) | string | Step title. | | `due_date` | string | Due date (YYYY-MM-DD). | | `assigned_to` | integer | Assignee user id. | ## Response ```json 200 { "ok": true, "opp": { "bant": 4, "bant_a": true, "bant_b": true, "bant_n": true, "bant_t": true, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2026-11-16", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Best Case", "gate_ok": false, "gate_problems": [ "En az bir F2F ziyaret kaydı gerekir (Hızlı Giriş → tür F2F)" ], "guide": "f2f ziyaret + doğru ürün", "health": { "grade": "success", "label": "sağlıklı", "score": 95 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−10" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": "Qualify", "note": "İhtiyaç ve bütçe doğrulandı.", "to": "Viable", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Present Solution", "next_step": "F2F ziyaret planla ve gerçekleştir", "note": "Numune onayı bekleniyor.", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.25, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 60, "sla_left": 60, "source": "", "stage": "Viable", "stage_label": "Viable", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": true, "done": true, "due_date": null, "id": 467, "note": "otomatik: BANT kutusu", "stage": "Qualify", "state": "done", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 84600.0 }, "step": { "assignee": null, "auto": false, "done": false, "due_date": "2026-10-09", "id": 474, "note": "", "stage": "Viable", "state": "soon", "title": "Hat denemesi planla" } } ``` --- # Toggle a step `POST /api/v1/steps/{id}/toggle` Marks a step done / open (flips it when `done` is omitted). BANT steps also update the opportunity's BANT flags. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Step id. | ## Body | Field | Type | Description | |---|---|---| | `done` | boolean | Done. | | `note` | string | Note (250 characters). | ## Response ```json 200 { "ok": true, "opp": { "bant": 4, "bant_a": true, "bant_b": true, "bant_n": true, "bant_t": true, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2026-11-16", "close_reason": "", "closed_at": null, "competitor": "Mevcut tedarikçi", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Best Case", "gate_ok": false, "gate_problems": [ "En az bir F2F ziyaret kaydı gerekir (Hızlı Giriş → tür F2F)" ], "guide": "f2f ziyaret + doğru ürün", "health": { "grade": "success", "label": "sağlıklı", "score": 80 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": "Qualify", "note": "İhtiyaç ve bütçe doğrulandı.", "to": "Viable", "user": "Deniz Aksoy" } ], "name": "Streç film tedariki", "next_stage": "Present Solution", "next_step": "F2F ziyaret planla ve gerçekleştir", "note": "Numune onayı bekleniyor.", "notes": [], "offer_price": 2.35, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.25, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 60, "sla_left": 60, "source": "", "stage": "Viable", "stage_label": "Viable", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": true, "done": true, "due_date": null, "id": 467, "note": "otomatik: BANT kutusu", "stage": "Qualify", "state": "done", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K", "visits": [], "weighted": 84600.0 }, "step": { "assignee": null, "auto": false, "done": true, "due_date": "2026-10-09", "id": 474, "note": "Perşembe 10:00", "stage": "Viable", "state": "done", "title": "Hat denemesi planla" } } ``` --- # Revive an opportunity `POST /api/v1/opportunities/{id}/revive` Opens a new opportunity from a lost / cancelled one (product, quantity and line are copied and linked to the original). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Id of the closed opportunity. | ## Response ```json 200 { "ok": true, "opp": { "bant": 0, "bant_a": false, "bant_b": false, "bant_n": false, "bant_t": false, "base_currency": "EUR", "can_edit": true, "can_revive": false, "close_category": "", "close_date": "2027-03-31", "close_reason": "", "closed_at": null, "competitor": "", "cur_sym": "€", "currency": "EUR", "customer": "Işıltı Film A.Ş.", "customer_city": "Konya", "customer_id": 8, "days_in_stage": 0, "demos": [], "detail_status": "", "fc": "Pipeline", "gate_ok": false, "gate_problems": [ "BANT kriterleri 4/4 sağlanmalı (Budget · Authority · Need · Time)" ], "guide": "BANT 4/4", "health": { "grade": "success", "label": "sağlıklı", "score": 85 }, "health_color": "g", "health_manual": false, "health_note": "", "health_why": [ [ "−15" ] ], "id": 54, "initials": "IF", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "logs": [ { "date": "2026-10-02T20:07:00", "from": null, "note": "Yeniden canlandırıldı (mobil) ← #38", "to": "Qualify", "user": "Deniz Aksoy" } ], "name": "Mevcut Müşteri — Ek Satış · 2026 (canlandırıldı)", "next_stage": "Viable", "next_step": "Karar verici / yetkili kişiyi belirle (Authority)", "note": "", "notes": [], "offer_price": null, "offers": [], "orders": [], "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": null, "potential_kg": 12000.0, "prob": 0.1, "probability": null, "product": "PU-SL 350 Yüksek Performans (retort)", "product_id": 2, "pu": "€/kg", "rid": "006CSwEIJ0YiYkC", "sla_days": 30, "sla_left": 30, "source": "", "stage": "Qualify", "stage_label": "Qualify", "start_date": "2026-10-02", "steps": [ { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "auto": false, "done": false, "due_date": null, "id": 475, "note": "", "stage": "Qualify", "state": "open", "title": "Karar verici / yetkili kişiyi belirle (Authority)" } ], "tags": [], "tasks": [], "threads": [], "threads_n": 0, "trend": "", "trend_label": "Otomatik", "type": "Mevcut Müşteri — Ek Satış", "unit": "kg", "unit_code": "kg", "value": 0, "value_base": 0, "value_short": "0", "visits": [], "weighted": 0.0 } } ``` --- # Log an activity `POST /api/v1/quick` Quick Entry: logs a phone call or face-to-face visit in one request. The company is matched by name (created as a prospect when missing; similar names return `409 similar`), a missing person is added, matching open actions are closed, the next action is opened and the project advances under the same rules as the web form. `parts` tells what was recorded; `warnings` and `advice` explain what needs attention. ## Body | Field | Type | Description | |---|---|---| | `customer` (required) | string | Company name (exact or close). | | `visit_type` | string | `F2F` face to face, `Call` phone, `Deneme` trial visit. Values: `F2F`, `Call`, `Deneme` | | `visit_date` | string | Activity date (YYYY-MM-DD, default today). | | `contact` | string | Person met (added when missing). | | `contact_title / contact_phone / contact_email` | string | Details for a new person. | | `topic` | string | Topic. | | `note` | string | What was discussed. | | `result` | string | Outcome. | | `next_action` | string | Next action. | | `action_date` | string | Next action date. | | `opp_id` | integer | Project to log against. | | `product / competitor / offer_price / payment_days / potential_kg` | mixed | Project details (same as the web form). | | `confirm_new` | boolean | Create a new company even if similar names exist. | ## Response ```json 200 { "advice": null, "closed": [], "created": false, "customer": { "active": true, "at_risk": false, "city": "Bursa", "currency": "EUR", "currency_default": "", "id": 31, "initials": "KP", "last_visit": "2026-10-02", "main_contact": { "email": "emre.yildiz@kuzeyplastik.com.tr", "name": "Emre Yıldız", "phone": "+90 532 555 11 22", "wa": "905325551122" }, "name": "Kuzey Plastik Sanayi", "next_visit_due": "2026-11-01", "open_opps": 1, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "pending_state": "soon", "period": 30, "phone": "+90 224 555 01 02", "potential_kg": null, "rid": "001QJi5Zr1Hk2VH", "sector": "Plastik", "status": "AC", "status_label": "Aktif Müşteri", "unit": "kg", "visit_overdue_days": -30, "visit_state": "ok" }, "message": "Kuzey Plastik Sanayi kaydedildi: ziyaret, proje #53 Streç film tedariki.", "next_step": "F2F ziyaret planla ve gerçekleştir", "ok": true, "opp": { "bant": 4, "base_currency": "EUR", "close_date": "2026-11-16", "cur_sym": "€", "currency": "EUR", "customer": "Kuzey Plastik Sanayi", "customer_city": "Bursa", "customer_id": 31, "days_in_stage": 0, "detail_status": "", "fc": "Best Case", "health": { "grade": "success", "label": "sağlıklı", "score": 95 }, "health_color": "g", "health_manual": false, "health_note": "", "id": 53, "initials": "KP", "is_account": false, "is_open": true, "last_update": "2026-10-02", "lifetime_left": 180, "line": "", "name": "Streç film tedariki", "next_step": "F2F ziyaret planla ve gerçekleştir", "offer_price": 2.35, "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "payment_days": 60, "potential_kg": 12000.0, "prob": 0.25, "probability": null, "product": "", "product_id": null, "pu": "€/kg", "rid": "006BMHmVNZ9nFLS", "sla_days": 60, "sla_left": 60, "source": "", "stage": "Viable", "stage_label": "Viable", "start_date": "2026-10-02", "trend": "", "trend_label": "Otomatik", "type": "", "unit": "kg", "unit_code": "kg", "value": 338400.0, "value_base": 338400.0, "value_short": "338K" }, "opp_new": false, "parts": [ "ziyaret", "proje #53 Streç film tedariki" ], "visit": { "action_date": "2026-10-05", "action_done": false, "action_state": "soon", "contact": "Emre Yıldız", "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "date": "2026-10-02", "id": 106, "line": "", "next_action": "Teklif gönder", "note": "Deneme sonuçlarını konuştuk; kalınlık 23 mikron uygun.", "opp": "Streç film tedariki", "opp_id": 53, "product": "", "result": "Olumlu", "rid": "00UEIXukSoU4vCq", "topic": "Görüşme", "type": "Call", "user": "Deniz Aksoy" }, "warnings": [] } ``` --- # List actions `GET /api/v1/actions` Open work in one list: tasks, next actions of visits and trials, dated project steps. Use each row's `kind` + `id` with the complete / postpone endpoints. ## Query parameters | Field | Type | Description | |---|---|---| | `scope` | string | `mine` = records I own (default for sales users), `all` = every record I can see (default for admins), or a user id. | | `state` | string | Due state. Values: `today`, `overdue`, `soon`, `open`, `week` | | `customer_id` | integer | Only this company. | ## Response ```json 200 { "counts": { "overdue": 9, "soon": 0, "today": 0 }, "ok": true, "rows": [ { "customer": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "customer_id": 3, "date": "2026-09-26", "icon": "📋", "id": 1, "key": "task:1", "kind": "task", "label": "Numune sonucu için ara", "opp": "Yeni Müşteri · 2026", "opp_id": 33, "src": "Görev", "state": "overdue", "url": "/action/task/1" }, { "customer": "Hanımeli Etiket San. Tic. Ltd. Şti.", "customer_id": 23, "date": "2026-09-26", "icon": "📋", "id": 17, "key": "task:17", "kind": "task", "label": "Fiyat listesini güncelleyip gönder", "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_id": 23, "src": "Görev", "state": "overdue", "url": "/action/task/17" } ], "total": 9 } ``` --- # Get an action `GET /api/v1/actions/{kind}/{rid}` The action with its context: company, project, the chain before / after it (`chain_before`, `chain_after`), the company's other open work and e-mail threads. ## Path parameters | Field | Type | Description | |---|---|---| | `kind` (required) | string | Action kind: `task`, `visit` (next action of a visit), `demo` (next action of a trial), `step` (opportunity step). Values: `task`, `visit`, `demo`, `step` | | `rid` (required) | integer | Action id (`id` in the list). | ## Response ```json 200 { "action": { "act": { "addr": "Bursa", "c": 31, "mail": "emre.yildiz@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 532 555 11 22", "url": "/customers/31", "wa": "905325551122", "who": "Emre Yıldız" }, "chain_after": [], "chain_before": [], "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "date": "2026-10-04", "detail": "2027 listesi, €/kg", "done": false, "done_at": null, "editable": true, "icon": "📋", "id": 23, "key": "task:23", "kind": "task", "label": "Fiyat listesini gönder", "opp": "", "opp_id": null, "others": [ { "customer": "", "customer_id": null, "date": "2026-10-05", "icon": "⚡", "id": 106, "key": "visit:106", "kind": "visit", "label": "Teklif gönder", "opp": "Streç film tedariki", "opp_id": 53, "src": "Ziyaret", "state": "soon", "url": "/action/visit/106" } ], "result": "", "rid": "00TxuMVTniYx44l", "src": "Görev", "state": "soon", "threads": [], "threads_n": 0, "user": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } }, "ok": true } ``` --- # Complete an action `POST /api/v1/actions/{kind}/{rid}/done` Marks the action done (or open again with `done: false`). `done_on` sets the completion day (default now). ## Path parameters | Field | Type | Description | |---|---|---| | `kind` (required) | string | Action kind: `task`, `visit` (next action of a visit), `demo` (next action of a trial), `step` (opportunity step). Values: `task`, `visit`, `demo`, `step` | | `rid` (required) | integer | Action id (`id` in the list). | ## Body | Field | Type | Description | |---|---|---| | `done` | boolean | Done; flips the state when omitted. | | `done_on` | string | Completion day (YYYY-MM-DD). | | `result` | string | Result note. | ## Response ```json 200 { "action": { "act": { "addr": "Bursa", "c": 31, "mail": "emre.yildiz@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 532 555 11 22", "url": "/customers/31", "wa": "905325551122", "who": "Emre Yıldız" }, "chain_after": [], "chain_before": [], "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "date": "2026-10-05", "detail": "2027 listesi, €/kg\n[Ertelendi 04.10.2026 → 05.10.2026] Liste onayı bekleniyor", "done": true, "done_at": "2026-10-02T20:07:01", "editable": true, "icon": "📋", "id": 23, "key": "task:23", "kind": "task", "label": "Fiyat listesini gönder", "opp": "", "opp_id": null, "others": [ { "customer": "", "customer_id": null, "date": "2026-10-05", "icon": "⚡", "id": 106, "key": "visit:106", "kind": "visit", "label": "Teklif gönder", "opp": "Streç film tedariki", "opp_id": 53, "src": "Ziyaret", "state": "soon", "url": "/action/visit/106" } ], "result": "Liste e-postayla gönderildi", "rid": "00TxuMVTniYx44l", "src": "Görev", "state": "done", "threads": [], "threads_n": 0, "user": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } }, "ok": true } ``` --- # Postpone an action `POST /api/v1/actions/{kind}/{rid}/postpone` Moves the due date; the note is appended to the record with dates. ## Path parameters | Field | Type | Description | |---|---|---| | `kind` (required) | string | Action kind: `task`, `visit` (next action of a visit), `demo` (next action of a trial), `step` (opportunity step). Values: `task`, `visit`, `demo`, `step` | | `rid` (required) | integer | Action id (`id` in the list). | ## Body | Field | Type | Description | |---|---|---| | `date` | string | New date (YYYY-MM-DD). | | `days` | integer | Days (when no date). | | `note` | string | Reason. | ## Response ```json 200 { "action": { "act": { "addr": "Bursa", "c": 31, "mail": "emre.yildiz@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 532 555 11 22", "url": "/customers/31", "wa": "905325551122", "who": "Emre Yıldız" }, "chain_after": [], "chain_before": [], "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "date": "2026-10-05", "detail": "2027 listesi, €/kg\n[Ertelendi 04.10.2026 → 05.10.2026] Liste onayı bekleniyor", "done": false, "done_at": null, "editable": true, "icon": "📋", "id": 23, "key": "task:23", "kind": "task", "label": "Fiyat listesini gönder", "opp": "", "opp_id": null, "others": [ { "customer": "", "customer_id": null, "date": "2026-10-05", "icon": "⚡", "id": 106, "key": "visit:106", "kind": "visit", "label": "Teklif gönder", "opp": "Streç film tedariki", "opp_id": 53, "src": "Ziyaret", "state": "soon", "url": "/action/visit/106" } ], "result": "", "rid": "00TxuMVTniYx44l", "src": "Görev", "state": "soon", "threads": [], "threads_n": 0, "user": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } }, "ok": true, "until": "2026-10-05" } ``` --- # Close and open the next `POST /api/v1/actions/{kind}/{rid}/close` Closes the action with its result and optionally opens the next task in the chain (same company and project). `mode: keep` leaves the action open and only adds the next task. ## Path parameters | Field | Type | Description | |---|---|---| | `kind` (required) | string | Action kind: `task`, `visit` (next action of a visit), `demo` (next action of a trial), `step` (opportunity step). Values: `task`, `visit`, `demo`, `step` | | `rid` (required) | integer | Action id (`id` in the list). | ## Body | Field | Type | Description | |---|---|---| | `result` | string | Result. | | `next_title` | string | Next task's title. | | `next_date` | string | Next task's due date. | | `next_assigned` | integer | Next task's assignee. | | `next_priority` | string | Priority. Values: `Düşük`, `Normal`, `Yüksek`, `Acil` | | `mode` | string | `close` closes, `keep` leaves it open. Values: `close`, `keep` | ## Response ```json 200 { "action": { "act": { "addr": "Ankara Organize Sanayi Bölgesi, 8. Cadde No: 35, Ankara, Ankara", "c": 3, "mail": "cem.polat@lotusmedikal.example", "n": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "tel": "0 (312) 000 46 49", "url": "/customers/3", "wa": "903120004649", "who": "Cem Polat" }, "chain_after": [ [ { "date": "2026-10-04", "done": false, "id": 24, "key": "task:24", "kind": "task", "label": "Teklif hazırla", "src": "Görev", "state": "soon" } ] ], "chain_before": [], "customer": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "customer_id": 3, "date": "2026-09-26", "detail": "", "done": true, "done_at": "2026-10-02T20:07:01", "editable": true, "icon": "📋", "id": 1, "key": "task:1", "kind": "task", "label": "Numune sonucu için ara", "opp": "Yeni Müşteri · 2026", "opp_id": 33, "others": [ { "customer": "", "customer_id": null, "date": "2026-10-03", "icon": "📋", "id": 19, "key": "task:19", "kind": "task", "label": "Kargo takip numarasını ilet", "opp": "Yeni Müşteri · 2026", "opp_id": 33, "src": "Görev", "state": "soon", "url": "/action/task/19" } ], "result": "Müşteri aradı, teklif istendi", "rid": "00Tc4zPI4VpXeWp", "src": "Görev", "state": "done", "threads": [], "threads_n": 0, "user": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } }, "next_task": { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "completed_at": null, "customer": "Lotus Medikal Ambalaj San. Tic. Ltd. Şti.", "customer_id": 3, "detail": "↳ Görev #1 (Numune sonucu için ara) sonrası: Müşteri aradı, teklif istendi", "due_date": "2026-10-04", "due_time": "", "id": 24, "opp": "Yeni Müşteri · 2026", "opp_id": 33, "priority": "Normal", "result": "", "rid": "00TfYtMol2EnOoG", "source_key": "task:1", "state": "soon", "status": "Açık", "title": "Teklif hazırla" }, "ok": true } ``` --- # Create a task `POST /api/v1/tasks` Creates a task, assigned to the caller unless `assigned_to` is given. Linking a company is optional (by id or name); no project is opened automatically — use `opp_id` for an existing project or `opp_name` for a new one. ## Body | Field | Type | Description | |---|---|---| | `title` (required) | string | Task title. | | `due_date` | string | Due date (YYYY-MM-DD). | | `due_time` | string | Time (HH:MM). | | `priority` | string | Priority. Values: `Düşük`, `Normal`, `Yüksek`, `Acil` | | `detail` | string | Details. | | `assigned_to` | integer | Assignee user id. | | `customer_id` | integer | Company id. | | `customer` | string | Company name (if the id is unknown). | | `opp_id` | string | Project id or `__none__` (no project). | | `opp_name` | string | New project name. | ## Response ```json 200 { "action": { "act": { "addr": "Bursa", "c": 31, "mail": "emre.yildiz@kuzeyplastik.com.tr", "n": "Kuzey Plastik Sanayi", "tel": "+90 532 555 11 22", "url": "/customers/31", "wa": "905325551122", "who": "Emre Yıldız" }, "chain_after": [], "chain_before": [], "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "date": "2026-10-04", "detail": "2027 listesi, €/kg", "done": false, "done_at": null, "editable": true, "icon": "📋", "id": 23, "key": "task:23", "kind": "task", "label": "Fiyat listesini gönder", "opp": "", "opp_id": null, "others": [ { "customer": "", "customer_id": null, "date": "2026-10-05", "icon": "⚡", "id": 106, "key": "visit:106", "kind": "visit", "label": "Teklif gönder", "opp": "Streç film tedariki", "opp_id": 53, "src": "Ziyaret", "state": "soon", "url": "/action/visit/106" } ], "result": "", "rid": "00TxuMVTniYx44l", "src": "Görev", "state": "soon", "threads": [], "threads_n": 0, "user": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" } }, "ok": true, "task": { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "completed_at": null, "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "detail": "2027 listesi, €/kg", "due_date": "2026-10-04", "due_time": "10:30", "id": 23, "opp": "", "opp_id": null, "priority": "Yüksek", "result": "", "rid": "00TxuMVTniYx44l", "source_key": "", "state": "soon", "status": "Açık", "title": "Fiyat listesini gönder" } } ``` --- # Update a task `PATCH /api/v1/tasks/{id}` Only the fields you send change. `status: Tamamlandı` completes the task (`done_on` picks the day). `PUT` and `POST` are accepted too. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Task id. | ## Body | Field | Type | Description | |---|---|---| | `title` | string | Title. | | `detail` | string | Details. | | `due_date` | string | Due date. | | `due_time` | string | Time (HH:MM). | | `priority` | string | Priority. Values: `Düşük`, `Normal`, `Yüksek`, `Acil` | | `status` | string | Status. Values: `Açık`, `Devam Ediyor`, `Tamamlandı` | | `done_on` | string | Completion day. | | `assigned_to` | integer | Assignee. | ## Response ```json 200 { "ok": true, "task": { "assignee": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "completed_at": null, "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "detail": "2027 listesi, €/kg", "due_date": "2026-10-04", "due_time": "09:00", "id": 23, "opp": "", "opp_id": null, "priority": "Acil", "result": "", "rid": "00TxuMVTniYx44l", "source_key": "", "state": "soon", "status": "Açık", "title": "Fiyat listesini gönder" } } ``` --- # List calendar `GET /api/v1/events` Meetings (`events`) and entries derived from actions (`derived`: due actions, visit dates) in a date range. Internal meetings are visible to their owner only. `mode` tells whether a meeting is online or in person; `join_url` is the meeting link. ## Query parameters | Field | Type | Description | |---|---|---| | `from` | string | Start (YYYY-MM-DD, default today). | | `to` | string | End (default +30 days). | | `done` | string | `1` = include completed actions. | ## Response ```json 200 { "derived": [ { "date": "2026-10-05", "done": false, "firm": "Kuzey Plastik Sanayi", "icon": "⚡", "kind": "visit", "location": "", "note": "Deneme sonuçlarını konuştuk; kalınlık 23 mikron uygun.", "state": "soon", "title": "⚡ Kuzey Plastik Sanayi · Teklif gönder", "uid": "visit-106@ornek", "url": "/action/visit/106", "what": "Teklif gönder" } ], "events": [], "from": "2026-10-02", "ok": true, "to": "2026-10-16" } ``` --- # Create an event `POST /api/v1/events` Adds a meeting to the calendar; it syncs to phone calendars when CalDAV is on. ## Body | Field | Type | Description | |---|---|---| | `title` (required) | string | Title. | | `start` (required) | string | Start (YYYY-MM-DDTHH:MM). | | `end` | string | End. | | `all_day` | boolean | All day. | | `customer_id` | integer | Company. | | `customer` | string | Company name. | | `location` | string | Location. | | `note` | string | Note. | ## Response ```json 200 { "event": { "all_day": false, "customer": "Kuzey Plastik Sanayi", "customer_id": 31, "editable": true, "end": "2026-10-07T11:30:00", "id": 1, "join_url": "", "location": "Bursa OSB", "mode": "onsite", "note": "", "source": "manual", "start": "2026-10-07T10:00:00", "title": "Kuzey Plastik · hat denemesi" }, "ok": true } ``` --- # Delete an event `DELETE /api/v1/events/{id}` Deletes an event added in the app (events from e-mail or CalDAV can't be deleted). `POST` is accepted too. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Event id. | ## Response ```json 200 { "ok": true } ``` --- # List fairs `GET /api/v1/fairs` Trade fairs (active first) with lead counts. ## Response ```json 200 { "ok": true, "rows": [ { "active": true, "city": "İstanbul", "date_label": "25.08.2026 – 28.08.2026", "end_date": "2026-08-28", "id": 1, "is_current": false, "lead_count": 10, "name": "Ambalaj Fuarı 2026 (örnek)", "start_date": "2026-08-25", "venue": "Fuar merkezi · Salon 4, Stand B12" } ] } ``` --- # List fair leads `GET /api/v1/fairs/{id}/leads` Leads collected at a fair (max 300, newest first). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Fair id. | ## Query parameters | Field | Type | Description | |---|---|---| | `owner` | string | `me` (default for sales users), empty = everyone, or a user id. | | `interest` | string | Interest level. Values: `Sıcak`, `Ilık`, `Soğuk` | | `q` | string | Company, person or phone contains. | ## Response ```json 200 { "fair": { "id": 1, "name": "Ambalaj Fuarı 2026 (örnek)" }, "ok": true, "rows": [ { "can_edit": true, "city": "Bursa", "company": "Işıltı Ambalaj San. Tic. Ltd. Şti.", "contact_name": "Mehmet Arslan", "created_at": "2026-08-28T14:10:00", "customer_id": null, "email": "mehmet@isiltiambalajsanti.example", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 4, "initials": "IA", "interest": "Soğuk", "line": "", "next_action": "Numune gönder", "next_date": "2026-10-07", "note": "Stantta görüşüldü.", "opp_id": null, "opp_type": "Mevcut Müşteri — Değişim", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "0 (224) 000 96 45", "potential_kg": 6000.0, "products": "4", "products_text": "", "project_note": "", "rid": "00QCwrpUC4bpt3E", "sector": "Gıda ambalajı", "status": "Teklif verildi", "status_eff": "Teklif verildi", "supplier": "Mevcut tedarikçi (Asya)", "task_id": null, "timing": "6 ay içinde", "title": "Kalite Kontrol Müdürü", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "902240009645", "website": "" }, { "can_edit": true, "city": "Tekirdağ", "company": "Nehirli Ambalaj San. Tic. Ltd. Şti.", "contact_name": "İpek Karaca", "created_at": "2026-08-28T12:10:00", "customer_id": null, "email": "ipek@nehirliambalajsant.example", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 8, "initials": "NA", "interest": "Soğuk", "line": "", "next_action": "Teklif gönder", "next_date": "2026-10-13", "note": "Stantta görüşüldü.", "opp_id": null, "opp_type": "Mevcut Müşteri — Değişim", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "0 (282) 000 76 98", "potential_kg": 2000.0, "products": "3", "products_text": "", "project_note": "", "rid": "00QBIhQqejYyYc3", "sector": "Film üretimi", "status": "Teklif verildi", "status_eff": "Teklif verildi", "supplier": "Mevcut tedarikçi (Asya)", "task_id": null, "timing": "Hemen", "title": "Satın Alma Müdürü", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "902820007698", "website": "" } ], "total": 10 } ``` --- # Create a lead `POST /api/v1/fairs/{id}/leads` Records a lead at the fair stand; a next action opens a task for the owner. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Fair id. | ## Body | Field | Type | Description | |---|---|---| | `company` (required) | string | Company name. | | `contact_name / title / phone / email` | string | Contact details. | | `city / sector / website` | string | Company details. | | `visitor_type` | string | Visitor type. Values: `Potansiyel müşteri`, `Mevcut müşteri`, `Tedarikçi`, `Rakip`, `Diğer` | | `interest` | string | Interest. Values: `Sıcak`, `Ilık`, `Soğuk` | | `products` | array | Products of interest. | | `products_text` | string | Products (free text). | | `potential_kg` | number | Monthly potential. | | `timing` | string | Timing. Values: `Hemen`, `3 ay içinde`, `6 ay içinde`, `1 yıl+`, `Belirsiz` | | `next_action` | string | Next action. Values: `Teklif gönder`, `Ziyaret planla`, `Numune gönder`, `Deneme planla`, `Bilgi gönder`, `Ara`, `Aksiyon yok` | | `next_date` | string | Action date. | | `owner_id` | integer | Owner. | | `note / project_note / supplier / line / opp_type` | string | Other fields. | ## Response ```json 200 { "lead": { "can_edit": true, "city": "Konya", "company": "Anadolu Gıda Ambalaj", "contact_name": "Murat Er", "created_at": "2026-10-02T20:07:01", "customer_id": null, "email": "", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 11, "initials": "AG", "interest": "Sıcak", "line": "", "next_action": "Numune gönder", "next_date": "2026-10-07", "note": "", "opp_id": null, "opp_type": "", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "+90 533 555 33 44", "potential_kg": null, "products": "", "products_text": "", "project_note": "", "rid": "00QpWFogpnPsXu7", "sector": "", "status": "Yeni", "status_eff": "Yeni", "supplier": "", "task_id": 25, "timing": "", "title": "Genel Müdür", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "905335553344", "website": "" }, "ok": true } ``` --- # Get a lead `GET /api/v1/leads/{id}` The lead and the product list (for form options). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Lead id. | ## Response ```json 200 { "lead": { "can_edit": true, "city": "Konya", "company": "Anadolu Gıda Ambalaj", "contact_name": "Murat Er", "created_at": "2026-10-02T20:07:01", "customer_id": null, "email": "", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 11, "initials": "AG", "interest": "Sıcak", "line": "", "next_action": "Numune gönder", "next_date": "2026-10-07", "note": "", "opp_id": null, "opp_type": "", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "+90 533 555 33 44", "potential_kg": null, "products": "", "products_text": "", "project_note": "", "rid": "00QpWFogpnPsXu7", "sector": "", "status": "Yeni", "status_eff": "Yeni", "supplier": "", "task_id": 25, "timing": "", "title": "Genel Müdür", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "905335553344", "website": "" }, "ok": true, "products": [ { "id": 5, "name": "HS 30 Isıl Yapışma Laki" }, { "id": 3, "name": "PU-SB 450 Solvent Bazlı Yapıştırıcı" } ] } ``` --- # Update a lead `PATCH /api/v1/leads/{id}` Changes the status; sending `company` rewrites all form fields (fields you omit are cleared). ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Lead id. | ## Body | Field | Type | Description | |---|---|---| | `status` | string | Status. Values: `Yeni`, `İletişime geçildi`, `Teklif verildi`, `Aktarıldı`, `Vazgeçildi` | | `company …` | string | Fields of the create endpoint. | ## Response ```json 200 { "lead": { "can_edit": true, "city": "Konya", "company": "Anadolu Gıda Ambalaj", "contact_name": "Murat Er", "created_at": "2026-10-02T20:07:01", "customer_id": null, "email": "", "fair": "Ambalaj Fuarı 2026 (örnek)", "fair_id": 1, "id": 11, "initials": "AG", "interest": "Sıcak", "line": "", "next_action": "Numune gönder", "next_date": "2026-10-07", "note": "", "opp_id": null, "opp_type": "", "owner": { "full_name": "Deniz Aksoy", "id": 1, "role": "admin", "role_label": "Yönetici", "username": "admin" }, "phone": "+90 533 555 33 44", "potential_kg": null, "products": "", "products_text": "", "project_note": "", "rid": "00QpWFogpnPsXu7", "sector": "", "status": "İletişime geçildi", "status_eff": "İletişime geçildi", "supplier": "", "task_id": 25, "timing": "", "title": "Genel Müdür", "unit": "kg", "visitor_type": "Potansiyel müşteri", "wa": "905335553344", "website": "" }, "ok": true } ``` --- # List threads `GET /api/v1/threads` E-mail threads either for one record (`kind` + `id`) or the inbox (`filter`, `q`). Content follows the user's permission to see it. ## Query parameters | Field | Type | Description | |---|---|---| | `kind` | string | Record type. Values: `customer`, `opp`, `visit`, `demo`, `task`, `offer`, `ticket`, `contract` | | `id` | integer | Record id. | | `filter` | string | `waiting` awaiting reply, `replied`, `mine`. Values: `all`, `waiting`, `replied`, `mine` | | `q` | string | Subject or address contains. | ## Response ```json 200 { "items": [ { "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "id": 1, "last_at": "2026-10-01T15:06:59", "last_dir": "out", "n": 2, "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_how": "single", "opp_id": 31, "opp_note": "firmanın tek açık projesi", "peer": "selin.dogan@alizepaketleme.example", "subject": "Numune ve fiyat teklifi", "suggested": false, "waiting": false } ], "mailbox": { "connected": true, "email": "deniz@ornekkimya.com.tr", "error": "", "signature": false, "status": "ok" }, "ok": true, "total": 1 } ``` --- # Get a thread `GET /api/v1/threads/{id}` Messages (`body: null`, `hidden: true` when the user may not see them), linked records, companies and people, a reply draft (`reply`) and link suggestions. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Thread id. | ## Response ```json 200 { "mailbox": { "connected": true, "email": "deniz@ornekkimya.com.tr", "error": "", "signature": false, "status": "ok" }, "ok": true, "thread": { "companies": [ { "contacts": [ { "email": "selin.dogan@alizepaketleme.example", "id": 1, "name": "Selin Doğan" } ], "id": 1, "name": "Alize Paketleme San. ve Tic. A.Ş.", "role": "main" } ], "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "id": 1, "last_at": "2026-10-01T15:06:59", "last_dir": "out", "linkables": [ { "items": [ { "id": 74, "label": "30.09.2026 · Call · Tanışma" } ], "kind": "visit", "klabel": "Ziyaret / görüşme" } ], "links": [], "may_edit": true, "messages": [ { "at": "2026-09-30T17:06:59", "body": "Merhaba Deniz Bey,\n\nNumuneler bize ulaştı, hat denemesini perşembe yapacağız. Aylık 12 ton için güncel fiyatınızı paylaşabilir misiniz?\n\nİyi çalışmalar", "cc": "", "dir": "in", "from_addr": "selin.dogan@alizepaketleme.example", "from_name": "Selin Doğan", "hidden": false, "id": 1, "subject": "Numune ve fiyat teklifi", "to": "deniz@ornekkimya.com.tr" } ], "n": 2, "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_how": "single", "opp_id": 31, "opp_note": "firmanın tek açık projesi", "opps": [ { "id": 44, "name": "Müşteri Takibi", "open": true, "stage_label": "Müşteri Takibi" } ], "peer": "selin.dogan@alizepaketleme.example", "reply": { "contact_id": 1, "customer_id": 1, "in_reply_to": "", "opp_id": 31, "subject": "Re: Numune ve fiyat teklifi", "to": "selin.dogan@alizepaketleme.example" }, "subject": "Numune ve fiyat teklifi", "suggested": false, "suggestions": [ { "date": "2026-09-30", "how": "", "id": 74, "kind": "visit", "klabel": "Ziyaret / görüşme", "label": "Call 30.09.2026", "nav": { "id": 74, "kind": "visit", "screen": "action" }, "url": "/action/visit/74", "why": "yazışmayla aynı günlerde görüşme" } ], "waiting": false } } ``` --- # Link a thread `POST /api/v1/threads/{id}/{action}` `opp` changes the project (`opp_id`), `link` links a record (`kind`, `obj_id`), `unlink` removes a link, `dismiss` ignores a suggestion. Only the thread's owner, the company owner or an admin may change it. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | integer | Thread id. | | `action` (required) | string | Action. Values: `opp`, `link`, `unlink`, `dismiss` | ## Body | Field | Type | Description | |---|---|---| | `opp_id` | integer | Project (`opp`). | | `kind` | string | Record type (`link`/`unlink`). | | `obj_id` | integer | Record id. | ## Response ```json 200 { "msg": "Yazışma projesi: Mevcut Müşteri — Yenileme · 2026.", "ok": true, "thread": { "companies": [ { "contacts": [ { "email": "selin.dogan@alizepaketleme.example", "id": 1, "name": "Selin Doğan" } ], "id": 1, "name": "Alize Paketleme San. ve Tic. A.Ş.", "role": "main" } ], "customer": "Alize Paketleme San. ve Tic. A.Ş.", "customer_id": 1, "id": 1, "last_at": "2026-10-01T15:06:59", "last_dir": "out", "linkables": [ { "items": [ { "id": 74, "label": "30.09.2026 · Call · Tanışma" } ], "kind": "visit", "klabel": "Ziyaret / görüşme" } ], "links": [], "may_edit": true, "messages": [ { "at": "2026-09-30T17:06:59", "body": "Merhaba Deniz Bey,\n\nNumuneler bize ulaştı, hat denemesini perşembe yapacağız. Aylık 12 ton için güncel fiyatınızı paylaşabilir misiniz?\n\nİyi çalışmalar", "cc": "", "dir": "in", "from_addr": "selin.dogan@alizepaketleme.example", "from_name": "Selin Doğan", "hidden": false, "id": 1, "subject": "Numune ve fiyat teklifi", "to": "deniz@ornekkimya.com.tr" } ], "n": 2, "opp": "Mevcut Müşteri — Yenileme · 2026", "opp_how": "manual", "opp_id": 31, "opp_note": "elle seçildi", "opps": [ { "id": 44, "name": "Müşteri Takibi", "open": true, "stage_label": "Müşteri Takibi" } ], "peer": "selin.dogan@alizepaketleme.example", "reply": { "contact_id": 1, "customer_id": 1, "in_reply_to": "", "opp_id": 31, "subject": "Re: Numune ve fiyat teklifi", "to": "selin.dogan@alizepaketleme.example" }, "subject": "Numune ve fiyat teklifi", "suggested": false, "suggestions": [ { "date": "2026-09-30", "how": "", "id": 74, "kind": "visit", "klabel": "Ziyaret / görüşme", "label": "Call 30.09.2026", "nav": { "id": 74, "kind": "visit", "screen": "action" }, "url": "/action/visit/74", "why": "yazışmayla aynı günlerde görüşme" } ], "waiting": false } } ``` --- # Compose options `GET /api/v1/emails/compose` For a new e-mail: whether the mailbox is connected, the company's people with e-mail and its open projects. ## Query parameters | Field | Type | Description | |---|---|---| | `customer_id` | integer | Company. | | `opp_id` | integer | Project. | ## Response ```json 200 { "contacts": [ { "email": "selin.dogan@alizepaketleme.example", "id": 1, "main": true, "name": "Selin Doğan", "title": "Satın Alma Müdürü" } ], "customer": { "id": 1, "name": "Alize Paketleme San. ve Tic. A.Ş." }, "mailbox": { "connected": true, "email": "deniz@ornekkimya.com.tr", "error": "", "signature": false, "status": "ok" }, "ok": true, "opp": null, "opps": [ { "id": 44, "name": "Müşteri Takibi", "stage_label": "Müşteri Takibi" }, { "id": 31, "name": "Mevcut Müşteri — Yenileme · 2026", "stage_label": "Qualify" } ] } ``` --- # Send an e-mail `POST /api/v1/emails/send` Sends one e-mail (including replies) from the user's connected mailbox; it joins the thread and project under the web rules. Returns `400 send` when no mailbox is connected. JSON body only. ## Body | Field | Type | Description | |---|---|---| | `to` (required) | string | Recipient(s), comma separated. | | `subject` (required) | string | Subject. | | `body` (required) | string | Body (plain text). | | `cc` | string | Cc. | | `customer_id / contact_id / opp_id` | integer | Records to link. | | `in_reply_to` | string | Message-ID being replied to (`reply.in_reply_to`). | ## Response ```json 400 { "error": "invalid", "message": "E-posta metni boş.", "ok": false } ``` --- # Ask a question `POST /api/v1/ask` A natural-language question about CRM data: with the AI module on, the model answers (`mode: ai`); otherwise a rule-based answer (`mode: kural`). `blocks` is a renderable structure, `refs` the records mentioned. JSON body only. ## Body | Field | Type | Description | |---|---|---| | `q` (required) | string | Question. | | `history` | array | Previous turns (chat context). | ## Response ```json 200 { "ai": false, "blocks": [ { "muted": false, "spans": [ { "b": true, "text": "Gecikmiş 2 görev" } ], "t": "p" }, { "muted": false, "spans": [ { "b": false, "nav": { "id": 17, "kind": "task", "screen": "action" }, "text": "Fiyat listesini güncelleyip gönder", "url": "/action/task/17" } ], "t": "li" } ], "mode": "kural", "ok": true, "q": "Bu hafta geciken aksiyonlarım neler?", "refs": [ { "kind": "task", "label": "Fiyat listesini güncelleyip gönder", "nav": { "id": 17, "kind": "task", "screen": "action" }, "url": "/action/task/17" }, { "kind": "task", "label": "Yeni hat devreye alma toplantısı", "nav": { "id": 8, "kind": "task", "screen": "action" }, "url": "/action/task/8" } ], "text": "**Gecikmiş 2 görev** (sizin)\n- [Fiyat listesini güncelleyip gönder](/action/task/17) · 26.09.2026 · Hanımeli Etiket San. Tic. Ltd. Şti.\n- [Yeni hat devreye alma toplantısı](/action/task/8) · 30.09.2026 · Işıltı Film A.Ş." } ``` --- # Question history `GET /api/v1/ask/history` The user's last 12 questions and suggested questions. ## Response ```json 200 { "ai": false, "items": [ { "at": "2026-10-02T20:07:01", "mode": "kural", "q": "Bu hafta geciken aksiyonlarım neler?" } ], "ok": true, "suggest": [ "Bu ay kapanması beklenen fırsatlar", "Gecikmiş görevlerim" ] } ``` --- # Service provider config `GET /api/scim/v2/ServiceProviderConfig` Supported SCIM features (PATCH yes; bulk, sort, ETag no). ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" ], "patch": { "supported": true }, "bulk": { "supported": false }, "filter": { "supported": true, "maxResults": 200 }, "changePassword": { "supported": false }, "sort": { "supported": false }, "etag": { "supported": false }, "authenticationSchemes": [ { "type": "oauthbearertoken", "name": "Bearer", "description": "Ayarlar → Uygulamalar → SCIM anahtarı" } ] } ``` --- # Resource types `GET /api/scim/v2/ResourceTypes` Only the `User` resource. ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:ListResponse" ], "totalResults": 1, "Resources": [ { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:ResourceType" ], "id": "User", "name": "User", "endpoint": "/Users", "schema": "urn:ietf:params:scim:schemas:core:2.0:User", "schemaExtensions": [ { "schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "required": false } ] } ] } ``` --- # Schemas `GET /api/scim/v2/Schemas` User and enterprise extension schemas. ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:ListResponse" ], "totalResults": 1, "Resources": [ { "id": "urn:ietf:params:scim:schemas:core:2.0:User", "name": "User", "attributes": [ { "name": "userName", "type": "string", "required": true, "uniqueness": "server" } ] } ] } ``` --- # List users `GET /api/scim/v2/Users` Users; only the `userName eq "…"` (or `id eq`) filter is supported. Paging via `startIndex` + `count` (max 200). ## Query parameters | Field | Type | Description | |---|---|---| | `filter` | string | `userName eq "name@company.com"` | | `startIndex` | integer | Starts at 1. | | `count` | integer | Page size. | ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:ListResponse" ], "totalResults": 1, "startIndex": 1, "itemsPerPage": 1, "Resources": [ { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "id": "2", "userName": "ayse.demir@ornekkimya.com.tr", "displayName": "Ayşe Demir", "name": { "formatted": "Ayşe Demir", "givenName": "Ayşe", "familyName": "Demir" }, "active": true, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Satış" }, "meta": { "resourceType": "User", "location": "https://ornek.solk.app/api/scim/v2/Users/2" }, "emails": [ { "value": "ayse.demir@ornekkimya.com.tr", "type": "work", "primary": true } ] } ] } ``` --- # Create a user `POST /api/scim/v2/Users` Creates a user and sends a set-password invitation (role: sales). An existing e-mail returns `409 uniqueness`. ## Response ```json 201 { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "id": "2", "userName": "ayse.demir@ornekkimya.com.tr", "displayName": "Ayşe Demir", "name": { "formatted": "Ayşe Demir", "givenName": "Ayşe", "familyName": "Demir" }, "active": true, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Satış" }, "meta": { "resourceType": "User", "location": "https://ornek.solk.app/api/scim/v2/Users/2" }, "emails": [ { "value": "ayse.demir@ornekkimya.com.tr", "type": "work", "primary": true } ] } ``` --- # Get a user `GET /api/scim/v2/Users/{id}` One user. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | string | User id. | ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "id": "2", "userName": "ayse.demir@ornekkimya.com.tr", "displayName": "Ayşe Demir", "name": { "formatted": "Ayşe Demir", "givenName": "Ayşe", "familyName": "Demir" }, "active": true, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Satış" }, "meta": { "resourceType": "User", "location": "https://ornek.solk.app/api/scim/v2/Users/2" }, "emails": [ { "value": "ayse.demir@ornekkimya.com.tr", "type": "work", "primary": true } ] } ``` --- # Replace a user `PUT /api/scim/v2/Users/{id}` Rewrites name, e-mail, department, phone and `active`. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | string | User id. | ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "id": "2", "userName": "ayse.demir@ornekkimya.com.tr", "displayName": "Ayşe Demir Kaya", "name": { "formatted": "Ayşe Demir Kaya", "givenName": "Ayşe Demir", "familyName": "Kaya" }, "active": true, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "" }, "meta": { "resourceType": "User", "location": "https://ornek.solk.app/api/scim/v2/Users/2" }, "emails": [ { "value": "ayse.demir@ornekkimya.com.tr", "type": "work", "primary": true } ] } ``` --- # Update a user `PATCH /api/scim/v2/Users/{id}` `replace`, `add`, `remove` operations; `active: false` deactivates the user (mobile sessions and API keys are revoked, records stay; the last active admin can't be deactivated). `active: true` needs a free seat. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | string | User id. | ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "id": "2", "userName": "ayse.demir@ornekkimya.com.tr", "displayName": "Ayşe Demir", "name": { "formatted": "Ayşe Demir", "givenName": "Ayşe", "familyName": "Demir" }, "active": false, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "department": "Satış" }, "meta": { "resourceType": "User", "location": "https://ornek.solk.app/api/scim/v2/Users/2" }, "emails": [ { "value": "ayse.demir@ornekkimya.com.tr", "type": "work", "primary": true } ] } ``` --- # Deactivate a user `DELETE /api/scim/v2/Users/{id}` Deactivates the user instead of deleting; records and history are kept. Returns `204`. ## Path parameters | Field | Type | Description | |---|---|---| | `id` (required) | string | User id. | ## Response ```json 204 HTTP/1.1 204 No Content ``` --- # Groups `GET /api/scim/v2/Groups` No group sync; returns an empty list (roles are set in the CRM). ## Response ```json 200 { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:ListResponse" ], "totalResults": 0, "startIndex": 1, "itemsPerPage": 0, "Resources": [] } ``` --- # Protected resource metadata `GET /.well-known/oauth-protected-resource/mcp` RFC 9728 document: the MCP resource, its authorization server and scopes. Clients arrive here from `resource_metadata` in a `401`. ## Response ```json 200 { "authorization_servers": [ "https://ornek.solk.app" ], "bearer_methods_supported": [ "header" ], "resource": "https://ornek.solk.app/mcp", "resource_documentation": "https://docs.solk.app/mcp/overview", "resource_name": "Solk CRM (Örnek Kimya)", "scopes_supported": [ "crm.read", "crm.write" ] } ``` --- # Authorization server metadata `GET /.well-known/oauth-authorization-server` RFC 8414 document: endpoint URLs, supported flows (`authorization_code` with PKCE S256 and `refresh_token` only), client auth methods. ## Response ```json 200 { "authorization_endpoint": "https://ornek.solk.app/oauth/authorize", "client_id_metadata_document_supported": true, "code_challenge_methods_supported": [ "S256" ], "grant_types_supported": [ "authorization_code", "refresh_token" ], "issuer": "https://ornek.solk.app", "registration_endpoint": "https://ornek.solk.app/oauth/register", "response_modes_supported": [ "query" ], "response_types_supported": [ "code" ], "revocation_endpoint": "https://ornek.solk.app/oauth/revoke", "revocation_endpoint_auth_methods_supported": [ "none", "client_secret_post", "client_secret_basic" ], "scopes_supported": [ "crm.read", "crm.write", "offline_access" ], "service_documentation": "https://ornek.solk.app/settings/connections", "token_endpoint": "https://ornek.solk.app/oauth/token", "token_endpoint_auth_methods_supported": [ "none", "client_secret_post", "client_secret_basic" ] } ``` --- # Register a client `POST /oauth/register` Dynamic client registration (RFC 7591). `redirect_uris` must be https or loopback (`http://localhost`, `127.0.0.1`, `[::1]`); max 10. `token_endpoint_auth_method: none` registers a public client; `client_secret_post` / `client_secret_basic` a confidential one. ## Body | Field | Type | Description | |---|---|---| | `redirect_uris` (required) | array | Redirect URIs. | | `client_name` | string | Name shown on the consent screen. | | `client_uri` | string | App website (https). | | `token_endpoint_auth_method` | string | Client authentication. Values: `none`, `client_secret_post`, `client_secret_basic` | | `grant_types` | array | `authorization_code`, `refresh_token`. | ## Response ```json 201 { "client_id": "crm_sN-deD6QaASdSwwlE2np8BWe", "client_id_issued_at": 1790960821, "client_name": "Örnek Entegrasyon", "grant_types": [ "authorization_code", "refresh_token" ], "redirect_uris": [ "https://uygulamaniz.com/oauth/callback" ], "response_types": [ "code" ], "token_endpoint_auth_method": "none" } ``` --- # Authorize `GET /oauth/authorize` Sends the user to the consent screen (signing in first if needed). On approval it redirects to `redirect_uri?code=…&state=…`; on denial `error=access_denied`. PKCE (S256) is mandatory. ## Query parameters | Field | Type | Description | |---|---|---| | `response_type` (required) | string | Only `code`. Values: `code` | | `client_id` (required) | string | Client id from registration or a CIMD URL. | | `redirect_uri` (required) | string | A registered redirect URI. | | `scope` | string | `crm.read crm.write` (and optional `offline_access`). Both when empty. | | `state` | string | Client CSRF value (echoed back). | | `code_challenge` (required) | string | BASE64URL(SHA256(code_verifier)). | | `code_challenge_method` (required) | string | Only `S256`. Values: `S256` | | `resource` | string | The MCP resource (`https:///mcp`) — leave empty for REST. | ## Response ```json 302 HTTP/1.1 302 Found Location: https://uygulamaniz.com/oauth/callback?code=Zx8…&state=xyz123 ``` --- # Get a token `POST /oauth/token` Form body (`application/x-www-form-urlencoded`). `authorization_code`: code + `code_verifier` → a 1-hour access token (`mcp_…`) + a 60-day refresh token (`mcpr_…`). `refresh_token`: every refresh returns a new pair and revokes the old one. A code works once; reusing it revokes the tokens issued from that grant. ## Body | Field | Type | Description | |---|---|---| | `grant_type` (required) | string | Grant type. Values: `authorization_code`, `refresh_token` | | `code` | string | Authorization code. | | `redirect_uri` | string | Same as in authorize. | | `code_verifier` | string | PKCE verifier. | | `refresh_token` | string | Refresh token. | | `client_id` (required) | string | Client id. | | `client_secret` | string | For confidential clients. | ## Response ```json 200 { "access_token": "mcp_uBprY6…", "expires_in": 3600, "refresh_token": "mcpr_AXksm…", "scope": "crm.read crm.write", "token_type": "Bearer" } ``` --- # Revoke a token `POST /oauth/revoke` Revokes an access or refresh token (RFC 7009); always returns `200`. Users remove the whole grant in Settings → App connections. ## Body | Field | Type | Description | |---|---|---| | `token` (required) | string | Token. | ## Response ```json 200 HTTP/1.1 200 OK ``` --- # Solk MCP > Connect AI assistants like Claude to your CRM — ask about companies and opportunities, and create tasks and activity logs right from the chat. Solk MCP is a **Model Context Protocol** server that runs in every installation. When an AI assistant that supports MCP (Claude, Claude Code, ChatGPT developer mode, Cursor, VS Code…) connects to this server, it can query your CRM in natural language and create records with your approval. ```text https://.solk.app/mcp ``` The **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*) page in the CRM shows your connector URL and copies it with one click. ## What you can do :::cards - [Find companies and people](/mcp/prompts#lookup) search | "Who handles purchasing at Alize Paketleme, and what did we last talk about?" - [Daily to-do list](/mcp/prompts#today) check | "What are my overdue actions for today and this week?" - [Pipeline](/mcp/prompts#pipeline) target | "Sort the opportunities in Negotiation by value and flag the ones past their SLA." - [Create records](/mcp/prompts#write) plus | "Log the phone call I just had with Kuzey Plastik and create a quote action for Thursday." ::: ## Get started **Requirements:** A user account in your Solk installation and a client that supports MCP. The connection runs under your account; no additional license is required. ### Claude (claude.ai, desktop, and mobile) 1. In Claude, open **Customize → Connectors → + Add → Add custom connector**. 2. Enter `Solk` as the name, paste `https://.solk.app/mcp` as the URL, and click **Add**. 3. Click **Connect**. The Solk sign-in page opens (or the consent screen directly, if you're already signed in); approve the permissions. On Team and Enterprise plans, the organization owner adds the custom connector once (**Organization settings → Connectors → Add → Custom → Web**); each member then clicks **Connect** with their own Solk account. On the Free plan, you can add one custom connector. ### Claude Code ```bash claude mcp add --transport http solk https://ornek.solk.app/mcp ``` In Claude Code, type `/mcp`, select the **solk** server, and grant access on the consent screen that opens in your browser. ### Other clients Enter the URL in your client as a remote MCP server (Streamable HTTP); the client handles OAuth discovery itself. For tools that don't support OAuth and require a static header, an admin can use an API key created in **Settings → Developers** (*Ayarlar → Geliştiriciler*): ```json { "mcpServers": { "solk": { "url": "https://ornek.solk.app/mcp", "headers": { "Authorization": "Bearer sk_…" } } } } ``` Detailed steps: [Connect](/mcp/connect). ## Tools The server provides 14 read tools and 9 write tools: search; reading companies, people, opportunities, actions, calendar, and email; dashboards and "Today"; creating companies, people, opportunities, tasks, and notes; changing stages; completing actions; and logging activities. Full list and parameters: [Tools](/mcp/tools). ## Security - **OAuth-based authentication** — your password is never given to the client; you see the requested permissions on the consent screen. - **Your account's permissions** — the assistant sees only the records you can see and changes only the records you can change; your role, module permissions, and record visibility all apply as is. - **Separate read and write** — the `crm.read` and `crm.write` scopes are approved separately; read tools are reported to the client as "read-only". - **Revocable** — in **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*), you see connected apps, their last use, and their operation count, and you can cut off access instantly with **Remove access** (*Erişimi kaldır*). - **Auditable** — every tool call is recorded in the audit log. Details: [Security](/mcp/security). ## Limits 300 requests per minute per token. A tool response carries at most ~90,000 characters; longer responses are truncated, with a note at the end saying that paging or a filter is needed. --- # Connect > Connect Claude, Claude Code, and other MCP clients to your Solk installation step by step, and see how OAuth discovery works. ## Claude :::steps ### Add the connector In Claude (web, desktop, or mobile), choose **Customize → Connectors → + Add → Add custom connector**. Name: `Solk`, URL: `https://.solk.app/mcp`. Leave the client ID fields under the advanced settings empty — Claude registers itself automatically. ### Connect Click **Connect** next to the connector. Your Solk installation's sign-in page opens; after you sign in, the consent screen shows the permissions Claude is requesting: - **Read records** — companies, people, opportunities, actions, email threads, calendar, and dashboards - **Create and update records** — companies, people, opportunities, tasks, notes, and activity logs; completing tasks; changing stages Click **Allow** (*İzin ver*) to return to Claude. ### Use it Keep Solk enabled in the chat and type your question. Claude asks for permission the first time it uses a tool; for read tools, you can choose "always allow". ::: **Team / Enterprise:** The organization owner adds the connector under **Organization settings → Connectors → Add → Custom → Web**. Members see the connector in their own list and click **Connect** to connect with their own Solk accounts — each member's permissions are limited to their own Solk role. ## Claude Code ```bash claude mcp add --transport http solk https://ornek.solk.app/mcp ``` Then, in Claude Code, run `/mcp` → **solk** → **Authenticate**. Claude Code uses a local redirect URI (`http://localhost:/callback`); for loopback addresses, Solk ignores the port number during matching. To make the server available to everyone on the project, you can set the scope to project (`--scope project`, written to `.mcp.json`); each developer still grants access with their own account. ## ChatGPT, Cursor, VS Code, and others In clients that support remote MCP servers (Streamable HTTP), enter the same URL. Clients that support OAuth handle discovery themselves. For clients that don't, an admin can create an [API key](/rest-api/authentication) and provide it in an `Authorization: Bearer sk_…` header — in that case, tools run with the permissions of the admin who created the key and with both scopes. ## How OAuth discovery works Clients need no extra configuration; the flow follows the standards (MCP Authorization, OAuth 2.1): 1. The client sends `POST /mcp` without a token → `401` and `WWW-Authenticate: Bearer … resource_metadata="https://ornek.solk.app/.well-known/oauth-protected-resource/mcp"`. 2. The client reads the [protected resource metadata](/rest-api/oauth/protected-resource) (RFC 9728) → authorization server `https://ornek.solk.app`. 3. It reads the [authorization server metadata](/rest-api/oauth/authorization-server) (RFC 8414). 4. It [registers itself](/rest-api/oauth/register) (RFC 7591) or uses the URL of its client metadata document (CIMD) as the `client_id`. 5. It redirects the user to the [consent screen](/rest-api/oauth/authorize) with PKCE (S256) and exchanges the code for a [token](/rest-api/oauth/token). 6. It sends MCP requests with `Authorization: Bearer mcp_…` and refreshes the token when it expires. ## Protocol details | | | |---|---| | Transport | Streamable HTTP, JSON responses (no SSE streaming) | | Protocol versions | `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` | | Methods | `initialize`, `tools/list`, `tools/call`, `ping`, and notifications | | `GET /mcp` | `405` (no server-to-client streaming) | | Session | Stateless; `Mcp-Session-Id` is not required | | Insufficient scope | If a write tool is called without `crm.write`: `403` + `WWW-Authenticate: … error="insufficient_scope"` | ```bash curl https://ornek.solk.app/mcp \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` --- # Tools > The Solk MCP server's read and write tools, their parameters, and example prompts. Tools run on top of the REST API: each tool call invokes the corresponding API endpoint under your account, and the same rules apply (permissions, visibility, stage gates, duplicate protection). The client sees only the tools you can use: - Without the `crm.read` scope, read tools don't appear in the list; without `crm.write`, write tools don't. - Tools for modules that are disabled on your license or for your user aren't listed (for example, if the Fair leads module is disabled, its tools are hidden). Tool names and descriptions are in English so that models understand them better; you can still talk to the assistant in Turkish. ## Read tools Read tools are reported to the client with `readOnlyHint: true` — Claude offers an "always allow" option for them. | Tool | Description | Parameters | Example prompt | |---|---|---|---| | `search` | Find companies, contacts, opportunities and fair leads by name, city, phone or e-mail (min 2 characters). Also resolves a 15-character record ID. | `query`* | *Find Kumsal Plastik* | | `list_companies` | List companies (customers) with filters. Returns rows with id, status (AC/Prospect), owner, city, visit state, plus counts. | `page`, `query`, `scope` (mine · all), `sort` (name · visit · priority), `status` (AC · Prospect), `visit` (overdue · soon · ok · none) | *List my active customers with overdue visits* | | `get_company` | Full company card: contacts, opportunities, recent visits/demos, open actions, offers, notes, email threads and key fields. | `company_id`* | *Summarize the Alize Paketleme company card* | | `list_contacts` | List contacts (people) across companies. | `query`, `scope` (mine · all) | *Show contacts whose title is purchasing manager* | | `list_opportunities` | List opportunities (sales projects). stage: open (default), closed, account (customer follow-up containers) or a stage key Qualify, Viable, Present Solution, Negotiation, Expect to Close, Win, Lost, Cancel. | `page`, `query`, `scope` (mine · all), `sort` (update · health · value · sla), `stage` | *Sort opportunities in Negotiation by value* | | `get_opportunity` | Full opportunity: stage, value, probability, health, checklist steps, BANT, gate problems, visits, demos, offers, stage history, notes. | `opportunity_id`* | *What is the stretch film opportunity waiting on to move to the next stage?* | | `list_actions` | Open to-dos: tasks, next actions from visits and demos, and dated project steps. Each row has kind + id (use with complete_action). | `company_id`, `scope` (mine · all), `state` (today · overdue · soon · open · week) | *Which of my actions are overdue this week?* | | `get_action` | One action / task with its context (company, opportunity, history). | `id`*, `kind`* (visit · demo · task · step) | *Open the details of the price list task* | | `list_calendar` | Calendar: meetings and events in a date range, plus derived entries (due actions, visit due dates). | `from`, `to` | *What's on my calendar next week?* | | `list_email_threads` | E-mail conversations synced from the users' mailboxes. Either for a record (record_type + record_id) or the inbox (filter, query). | `filter` (all · waiting · replied · mine), `query`, `record_id`, `record_type` (customer · opp · visit · demo · task · offer · ticket · contract) | *Show customer emails waiting for a reply* | | `get_email_thread` | Messages of one e-mail conversation (bodies may be hidden if the user may not see them) and its linked records. | `thread_id`* | *Summarize the latest thread with Kuzey Plastik* | | `get_dashboard` | KPIs: pipeline by stage (count and value in base currency), weekly visit targets, SLA overruns, visits due, next steps, upcoming items. | `scope` | *Give me a pipeline summary* | | `get_today` | My day: today's meetings and to-dos, overdue items, recently viewed records and this week's activity. | — | *What do I need to do today?* | | `list_team` | Active users (id, name, role, department) — use ids for assigned_to / owner_id. | — | *Who is on the team?* | `*` required parameter. ## Write tools Write tools create or modify records; none of them delete records (`destructiveHint: false`). Claude asks for approval before every write call. | Tool | Description | Parameters | Example prompt | |---|---|---|---| | `create_company` | Create a company (status Prospect unless given). Fails if the name exists; similar names need confirm_new=true. Optional first contact. | `address`, `city`, `confirm_new`, `contact_email`, `contact_name`, `contact_phone`, `contact_title`, `name`*, `note`, `owner_id`, `phone`, `sector`, `status` (AC · Prospect), `website` | *Create a new prospect called Anadolu Gıda Ambalaj in Konya* | | `update_company` | Update company fields (only the given ones change). | `address`, `city`, `company_id`*, `name`, `note`, `owner_id`, `phone`, `sector`, `status` (AC · Prospect), `visit_period_days`, `website` | *Mark Kuzey Plastik as an active customer and set a 30-day visit cycle* | | `create_contact` | Add a person to a company. | `company_id`*, `department`, `email`, `is_main`, `name`*, `note`, `phone`, `title` | *Add Emre Yıldız to Kuzey Plastik as production lead* | | `create_opportunity` | Open a new sales project for a company (starts in the first stage). | `company_id`*, `competitor`, `name`, `note`, `type`, `value` | *Open a 'Stretch film supply' opportunity for Kuzey Plastik* | | `change_opportunity_stage` | Move an opportunity to another stage. Keys: Qualify, Viable, Present Solution, Negotiation, Expect to Close, Win, Lost, Cancel. Pipeline gates apply; refused changes return the reasons. | `note`, `opportunity_id`*, `reason`, `stage`* (Qualify · Viable · Present Solution · Negotiation · Expect to Close · Win · Lost · Cancel) | *Move the stretch film opportunity to Viable* | | `create_task` | Create a task (to-do). Assigned to me unless assigned_to is given. Linking a company is optional; no project is opened automatically. | `assigned_to`, `company_id`, `company_name`, `detail`, `due_date`, `due_time`, `opportunity_id`, `priority` (Düşük · Normal · Yüksek · Acil), `title`* | *Create a task to call Kuzey Plastik tomorrow at 10:30* | | `complete_action` | Mark an action done (task, visit/demo next action, project step). done_on = completion day YYYY-MM-DD (default today). | `done_on`, `id`*, `kind`* (visit · demo · task · step), `result` | *Mark the price list task as done* | | `log_activity` | Record a call or face-to-face visit with a company (Quick Entry): note, result and the next action + date. Closes matching open actions and advances the project like the web form. Unknown company names create a new lead unless similar names exist (then confirm_new=true). | `action_date`, `company`*, `confirm_new`, `contact`, `next_action`, `note`, `opportunity_id`, `result`, `topic`, `visit_date`, `visit_type`* (F2F · Call) | *Log my call with Kuzey Plastik just now and add a follow-up to send a quote on Thursday* | | `add_note` | Add a note to a company, opportunity, support ticket or contract. | `body`*, `record_id`*, `record_type`* (customer · opp · ticket · contract) | *Add a note 'asked for a price revision' to Alize Paketleme* | `*` required parameter. ## Response format Tools return results both as text (`content[0].text`, JSON) and as structured content (`structuredContent`). On business rule errors (for example, a stage gate or a similar company name), the result has `isError: true` and text explaining the reason; the assistant passes this on to the user or corrects the request and retries. ```json { "jsonrpc": "2.0", "id": 3, "result": { "content": [{ "type": "text", "text": "{\"counts\": {\"overdue\": 8, \"soon\": 0, \"today\": 0}, \"rows\": [ … ]}" }], "structuredContent": { "counts": { "overdue": 8, "soon": 0, "today": 0 }, "rows": [ "…" ] }, "isError": false } } ``` --- # Security > Authentication, permissions, approval, auditing, and revoking access for the MCP connection. ## Authentication Solk MCP works only with **OAuth 2.1** (or with an API key created by an admin). Your password is never given to the assistant or the client: the client redirects you to your Solk installation's own sign-in and consent pages, and once you grant access, it receives a short-lived access token. | | | |---|---| | Access token | 1 hour | | Refresh token | 60 days; rotated on every refresh | | PKCE | Required (S256) | | Redirect URIs | `https://` and loopback only | | Token storage | The server stores only a SHA-256 hash | ## Permissions The assistant works **under your account**: - Your role (admin / sales), your module permissions, and record visibility all apply as is. If you connected as a sales rep, the assistant sees only what you can see. - Email content is returned according to the mailbox owner's sharing settings; the text of messages you aren't allowed to see is never given to the assistant. - Scopes are separate: if you grant only `crm.read`, the assistant can't change any records. - No tool deletes records. ## Approval Claude asks for permission the first time it uses a tool. Because read tools are marked "read-only", you can grant them permanent permission; for write tools, we recommend having Claude ask for approval on every call. Organization owners can restrict which tools are available in Claude's admin settings. ## Auditing - Every tool call is recorded in the Solk audit log with the user, the tool name, and the connected app's name. - Records created by write tools appear with their owner and timestamp, just as if they had been created in the web app; the same events are sent to integrations (Slack, webhooks…). - **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*) shows each app's connection date, last use, and operation count. ## Revoking access | Who | How | |---|---| | User | **Settings → App connections → Remove access** (*Ayarlar → Uygulama bağlantıları → Erişimi kaldır*) — all of that app's tokens are revoked immediately. | | Admin | Deactivating the user cuts off all of their connections. | | On the Claude side | Remove the connector from the Connectors list, or click **Disconnect**. | ## Data processing The MCP server runs inside your installation; there's no additional intermediary service. Data the assistant reads is sent to the AI service you use (for example, Anthropic) under that service's terms. Decide which users can connect based on your organization's AI usage policy. --- # Example prompts > Real-world examples you can ask an assistant connected to Solk — lookups, daily planning, pipeline, and creating records. The assistant first uses `search` to turn names into IDs, then calls the detail tools. Approximate company and person names are enough. ## Find companies and people {#lookup} - "Who's the main contact at Alize Paketleme, and what's their phone number?" - "What did we discuss in our last three calls or visits with Kumsal Plastik?" - "Which active customers in Bursa are overdue for a visit?" - "Which company has this record ID starting with 001: 001aB3xY7kLmN2q" ## My day and actions {#today} - "What do I need to do today? List my meetings and overdue work separately." - "Which of my actions are due this week? Group them by company." - "Show the open items at Ilgaz Plastik and tell me which one is the oldest." ## Pipeline {#pipeline} - "Sort the opportunities in the Negotiation and Expect to Close stages by value." - "Which of my opportunities have red health, and why?" - "What's the total value of opportunities expected to close this quarter (in the base currency)?" - "What is the stretch film supply opportunity waiting on before it can move to the next stage?" ## Email {#email} - "Show customer emails that are waiting for a reply." - "Summarize the latest thread with Kuzey Plastik. Which opportunity is it linked to?" ## Create and update records {#write} - "I just got off the phone with Kuzey Plastik: the trial went well, and I'll send a quote on Thursday. Log the call and create the action." - "Create a new prospect company called Anadolu Gıda Ambalaj, city Konya, and add Murat Er as general manager." - "Mark the 'Send the price list' task as complete, with the outcome: sent by email." - "Create a task 'Ask about the sample results' for Kuzey Plastik tomorrow at 10:30." - "Move the stretch film supply opportunity to Viable, with the note: need and budget confirmed." :::tip For write requests, the assistant first tells you what it's going to do and asks for your approval. If a company with a similar name exists, it asks you which one you mean before creating a new company; if a stage gate isn't met, it lists what's missing. ::: ## Bulk operations - "For every active customer more than 30 days overdue for a visit, create a 'Schedule a visit' task for its owner." — the assistant shows you the list first and, if you approve, creates the tasks one by one. - "Count the loss reasons for lost opportunities and list the three most common." --- # Troubleshooting > Causes of and fixes for connection, permission, missing tool, and rate limit problems. | Symptom | Cause | Fix | |---|---|---| | Claude says "couldn't connect" | The URL is wrong, or the installation is running an old version. | Check that the URL is `https://.solk.app/mcp` and that the `version` value at `https://.solk.app/health` is `v22` or later. | | The consent screen says "Uygulama tanınmadı" (app not recognized) | The client registration was deleted or belongs to another installation. | Remove the connector in Claude and add it again. | | "Dönüş adresi bu uygulamaya kayıtlı değil" (redirect URI not registered for this app) | The client is using a redirect URI that differs from the one in its registration. | Re-register the client (remove the connector in Claude and add it again). | | The connection drops after a while | The refresh token went unused for 60 days, or access was removed. | Grant access again with **Connect**. | | Some tools are missing | A scope or module permission is missing. | Remove the connection and reconnect, granting both permissions; ask an admin for the module permission. | | "Bu işlem için kayıt oluşturma / güncelleme izni gerekli" (create/update permission required for this action) | Only `crm.read` was granted. | When you reconnect, approve the write permission too. | | The assistant can't find a record | The record is outside your visibility, or the company is inactive. | Check that you can see the record in the web app with the same user. | | "Dakikalık istek sınırı aşıldı" (per-minute request limit exceeded) | Too many calls in a bulk operation. | Wait a minute; ask the assistant to work with a narrower filter. | | License error | The installation's license has expired. | The connection works only for admins; renew the license. | ## Manual testing To test the token and the server directly: ```bash # 1) discovery (without a token, this should return 401 and resource_metadata) curl -i -X POST https://ornek.solk.app/mcp -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' # 2) list tools with an API key curl https://ornek.solk.app/mcp -H "Authorization: Bearer sk_…" \ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` You can also use the official **MCP Inspector** (`npx @modelcontextprotocol/inspector`): enter the URL and step through the OAuth flow and the tools. --- # Changelog > Changes to Solk CRM, the REST API, and MCP — newest first. All installations are updated from the same codebase. `https://.solk.app/health` shows your installation's version. Changes that affect the API are marked with the **API** label. ## v22 · October 2, 2026 {#v22} **Claude and MCP** - MCP server on every installation: `https://.solk.app/mcp` — 14 read tools and 9 write tools. [MCP](/mcp/overview) - OAuth 2.1 authorization server: discovery (RFC 9728 / 8414), dynamic client registration and CIMD, mandatory PKCE, refresh token rotation. [OAuth implementation](/rest-api/oauth) - **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*): connector URL, connected apps, last used, remove access. **Apps** - App detail pages (overview, setup steps, permissions, documentation). - **Call** (*Ara*) button on records (Aircall, RingCentral) and a "Call now" (*Şimdi ara*) window. - **App requests** (*uygulama isteği*) for apps that aren't in the catalog. **API** - The REST API accepts OAuth access tokens (`mcp_…`); writes without `crm.write` return `403 insufficient_scope`. - Per-token rate limit per minute (default 300) — `429 rate_limited` and `Retry-After`. [Rate limits](/rest-api/rate-limits) - In JSON bodies, `false` now explicitly means false: `done: false` leaves an action open (previously it toggled the status), and fields such as `is_former: false` and `bant_b: false` revert the value. - This documentation site, `llms.txt`, and the OpenAPI 3.1 definition are live. ## v21 · October 2, 2026 {#v21} - **Apps** (*Uygulamalar*) catalog with 41 cards: Slack (+ `/crm` command), Teams, Google Chat, Discord, Telegram, Zapier, Make, n8n, Pipedream, webhook; Notion, Asana, ClickUp, Airtable, Google Sheets, Linear, Mailchimp; Hunter, Apollo, lemlist, Mixmax, Productboard, PandaDoc, Stripe, Aircall, RingCentral; website form, Typeform, Tally, Segment, data sync, Calendly / Cal.com, meeting notes; Resend; add from browser. [Apps](/guides/apps) - **Storage accounts** (*Depolama hesapları*): Google Drive, OneDrive, Dropbox, Box — save to the cloud, attach share links, auto-save, daily cloud backup. [Storage](/guides/storage) - **SCIM 2.0** (Okta, Entra ID). [SCIM](/rest-api/scim) — **API** - **Developers** (*Geliştiriciler*) page: non-expiring API keys (`sk_…`). — **API** - "Send to app" (*Uygulamaya gönder*) step in workflows. ## v20 · October 2, 2026 {#v20} - Collapsible, animated left menu (Ctrl + .). - Sales stages configured in settings: name, order, color, Cancel stage, stage rules; new installations start with two open stages. [Stages](/concepts/pipeline) — **API**: `constants.stages`, `stage_labels`, `stage_rules` - Opportunity, task, and company templates. - Notification preferences (event × email / in-app) and daily digest. - Slack, Teams, and Telegram notifications, webhook, form → lead. ## v19 · October 2, 2026 {#v19} - Simplified interface with a left menu. - Colored dot for opportunity health; users can pick the color manually and add a note. — **API**: `health_color`, `health_manual`, `health_note` - Pick the day when completing (Today / Yesterday / date). — **API**: `done_on` ## v18 · October 1–2, 2026 {#v18} - **Email Automation** (*E-posta Otomasyonu*) module (bulk sending, sequences); email threads and Ask in the mobile app. — **API**: `threads`, `emails/send`, `ask` - Automatic customer provisioning and removal (panel → server). - Email sync: full history, Gmail archive, Turkish folder names; company mail connection; correct company / person in bulk threads; company name from the domain. - Redesigned Actions page; overdue items email. - Sample data (for demos, removable with one click). - Meeting invites in email go to the calendar (online / in-person distinction). — **API**: `mode`, `join_url` - Project selection and time on tasks; time-based calendar; shared + personal calendar for iPhone. — **API**: `due_time`, `opp_name` ## v17 · October 1, 2026 {#v17} - Email threads link to records (project, visit, trial, task, quote, ticket, contract); link suggestions. - Automatic companies and people from email; domain analysis; noise filter. - Record visibility: everyone / team. [Users and roles](/users-and-roles) ## v16 · September 30 – October 1, 2026 {#v16} - Email window: Bcc, attachments, variables, scheduled and bulk sending, drafts. - Email sequences. - Mailbox connection with Google / Microsoft (centralized redirect). - Report builder and **Ask** (*Sor*) (AI module). ## v15 · September 30, 2026 {#v15} - Command palette (Ctrl/⌘ + K), quick task and note, @ mentions. - Getting-started checklist and team invites. - Workflows (automation). - Email account connection (IMAP/SMTP). ## v14 · September 2026 {#v14} - Per-product units, multi-currency, Central Bank of Türkiye (TCMB) exchange rates; totals in the base currency. [Units and currency](/concepts/units-currency) — **API**: `unit`, `pu`, `value_base` - SLA durations per company (stage days, project lifetime, support ticket target). - Product list import from Excel. ## v13 · September 2026 {#v13} - Support tickets, contracts, Kanban, Opportunity Board, assistant, two-factor authentication, IP restrictions. ## v12 · September 2026 {#v12} - Today page; iOS / Android app and JSON API (`/api/v1`). — **API**