Skip to content
AISARAISAR
REST API

Deals

The CRM layer of AISAR: deals attached to funnels and stages, deal tasks (calls, meetings, follow-ups), and per-company reference lists of deal types and lost reasons. Ownership of a deal or task is always checked by company_id — touching another company's record returns 404, not 403. Every endpoint requires an active subscription and the plan's deals feature.

Listing deals: filters and the absence of pagination

GET /deals accepts the optional query parameters contact_id, funnel_id, responsible_user_id (integers ≥ 1), status (exactly one of open, won, lost) and search (a string of up to 100 characters, matched case-insensitively as a substring of the deal title or of its contact's display name). Filters are combined with AND; an empty string in any of them means "no filter". Passing an array instead of a scalar (status[]=open) or a value outside the enum returns 422 — parameters are never silently ignored. Every filter is applied strictly inside the caller's own company: a contact_id belonging to another company is not a 403 and not a leak, it simply yields an empty list.

GET /deals is not paginated: it returns every matching deal of the company as one flat array, {"data": [...]} (ordered by updated_at descending, ties broken by id descending), with no pagination block. For the deals of a single contact use the paginated alternative GET /v1/contacts/{contact}/deals (a perPage parameter, data.items + data.pagination response shape) — it walks the same deals.contact_id link, so the two endpoints agree by construction.

Since 2026-09-29 GET /deals takes two more optional parameters. perPage (integer 1–100) is a cap, not pagination: the response stays the same flat array with no pagination block, it just holds at most perPage of the most recently updated deals; without the parameter the list is complete, as before. include_unsorted (boolean) includes leads waiting in "Unsorted" (see below): by default GET /deals leaves them out. The exception is a request filtered by contact_id: one contact's deals come back together with that contact's unsorted lead even without the flag, exactly as in GET /v1/contacts/{contact}/deals.

Funnel/stage consistency

Updating a deal (POST /deals/{deal}) obeys funnel/stage consistency rules: changing funnel_id requires an explicit funnel_stage_id in the same request (otherwise 422 "A funnel_stage_id is required when changing funnel_id." — the old silent jump to the first stage was removed), and the funnel_stage_id itself must belong to the chosen funnel (otherwise 422 "Selected stage does not belong to the selected funnel."). If you change the stage without an explicit status, the status is derived from the stage type: success→won, failed→lost, anything else→open.

Closing a deal: won and lost

Moving status to lost (including implicitly, by moving into a failed-typed stage) requires a lost_reason_id from the lost-reasons reference or a free-form lost_reason (otherwise 422 "A lost reason is required when marking a deal as lost."); a transition to won/lost snapshots closed_amount/closed_currency at close time, and moving back to open clears closed_at, lost_reason, lost_reason_id and the amount snapshot.

Deletion and restore

Deleting a deal is soft (deleted_at, the id and dialog links are kept), and POST /deals/{deal}/restore (permission deal.delete) restores it idempotently.

Auto-leads and "Unsorted"

Since 2026-09-29 a deal is created from a conversation automatically by a single "auto-lead" decision point. The only trigger is a live inbound appeal from the client: an inbound message, an inbound call (missed ones included), or a visitor's first message in the website widget. A conversation the company opened itself (an operator, the API, a broadcast, the first outbound message from the phone) never produces a lead: the client's answer in such a conversation is a reply, not an appeal. No lead is created either for group chats, for contacts hidden from the inbox or blocked, for contacts carrying the auto_lead_excluded_at exclusion, for history imports, for inbound events older than a day (a reconnected channel redelivering what piled up), or for dealless conversations that started before auto-leads were turned on. How the deal is created is set by auto_lead_mode in the company settings (see "Reference lists and settings").

In unsorted mode an auto-lead lands in "Unsorted", the virtual first column of its funnel. Such a deal has unsorted_at set (not null ⇔ the lead is waiting to be sorted); lead_conversation_id on any auto-lead (in both modes) is the id of the conversation it was born in, and null on deals created by a person, the API or an automation. Both fields are present in the full deal object (the /deals, /deals/{deal} and /contacts/{contact}/deals responses), and unsorted_at also in the deal embedded in a conversation and a thread. A lead in "Unsorted" is not part of the board columns, their deals_count/total_amount, or stage paging: only GET /v1/funnels/{funnel}/unsorted lists it, and the board shows just the unsorted.count counter (see the "Funnels" group). Per contact the lead is visible: GET /v1/contacts/{contact}/deals and GET /deals?contact_id= include it.

Until a lead is accepted it cannot be closed or moved by an ordinary edit: POST /deals/{deal} carrying any of status, funnel_id, funnel_stage_id answers 409 {"data": {"message": "The deal is in Unsorted: accept it first."}}. The other fields (title, amount, responsible user, etc.) change as usual. The update_deal/move_deal_stage MCP tools return a tool error on such a deal, and the "Move deal to stage" automation node ends as skipped with reason deal_unsorted.

Three routes sort "Unsorted", all needing deal.update (an agent who sees the column through deal.view gets 403) and limited to 30 requests per minute. Another company's deal answers 404; a lead someone else has already sorted (or that is no longer in "Unsorted", or is closed) answers 409 "The deal is not in Unsorted.". `POST /deals/{deal}/accept` accepts: optional funnel_stage_id (an open stage of the lead's funnel — not success/failed, otherwise 422 "The stage must be an open stage of the lead's funnel."; defaults to the first open stage) and responsible_user_id (a human member of the company — a service account is refused with 422; defaults to the caller). unsorted_at is cleared, a lead_accepted row goes to the timeline, and the response is the deal object. `POST /deals/{deal}/decline` declines: the lead is soft-deleted (a lead restored with restore goes back to "Unsorted"); with exclude_contact: true (which also needs contact.update, otherwise 403) the contact is excluded from auto-leads with auto_lead_excluded_via: "decline". A lead_declined row goes to the timeline and the response is {"data": {"declined": true}}. `POST /deals/{deal}/link` links: the required target_deal_id must be an open, non-deleted, already sorted deal of your own company (otherwise 422); every conversation of the lead moves to it, the lead itself is soft-deleted, the target deal's timeline gets lead_linked, and the response is the target deal object. A lead cannot be linked to itself (422 "A lead cannot be linked to itself.").

A lead in "Unsorted" is not sent anywhere until it is accepted: no deal.created webhook, no deal.updated on edits, no deal-created automation trigger, no Bitrix24 sync. "Accept" is the deal's birth for integrations: the deal.created webhook and the deal-created trigger go out, the deal and its activity are sent to Bitrix24, and the assigned responsible user (unless that is the person who accepted) gets a deal_assigned notification. "Decline" and "Link" send nothing out — deal.deleted included. In the message.created and conversation.* webhooks conversation.deal_id stays null while the conversation's deal is an unsorted or declined lead.

An auto-lead's responsible user is the contact's responsible manager, otherwise the user assigned to the conversation; if there is none either, the first teammate who joins the conversation becomes the responsible user of its open deal (the timeline shows responsible_changed without an actor). Holders of deal.update get a lead_unsorted in-app notification, grouped per funnel, about new leads in "Unsorted".

Activity timeline

GET /deals/{deal}/activities returns an append-only timeline whose type is one of created|stage_changed|field_changed|note|task_created|task_completed|won|lost|responsible_changed|system; on system entries (automations, AI tools, inbound Bitrix24) actor is null.

Since 2026-09-29 the set has four auto-lead types: inbound_repeat — a live appeal was attached to an already open deal instead of a new one (metadata: conversation_id, channel_type, trigger — message, call or widget); lead_accepted — a lead was accepted out of "Unsorted" (metadata: funnel_stage_id, responsible_user_id); lead_declined — a lead was declined, written right before the soft delete (metadata.exclude_contact); lead_linked — written on the target deal a lead was linked to (metadata: source_deal_id, conversation_ids). Deal edits made by an AI agent are marked in the metadata of the stage_changed/field_changed/responsible_changed rows with actor: "ai_agent" and ai_agent_instance_id.

A deal's source says where it came from: manual, api, whatsapp, telegram, instagram, bitrix24, voice (the voice AI agent during a call), and since 2026-09-29 also messenger, live_chat, phone and other. An auto-lead's source is derived from the channel type: whatsapp/whatsapp_business → whatsapp, telegram/telegram_bot → telegram, instagram/instagram_business → instagram, facebook_messenger → messenger, live_chat → live_chat, sip_telephony → phone, anything else → other.

Deal tasks

Deal tasks are updated with PATCH /deal-tasks/{task} (the legacy /deals/{deal} deliberately stays on POST): overdue is a derived field (due_at < now() while status=open; "now" and day boundaries are computed in the company's timezone), closing (done/cancelled) records completed_at, and assigning a task to someone else sends that person a task_assigned in-app notification (the former deal_task_assigned type is no longer sent).

GET /deal-tasks (since 2026-09-26) accepts: scope — visible (the default: everything the caller may see), mine (assigned to me), unassigned (nobody's) or all (needs the deal_task.view_all permission, otherwise 403); bucket — overdue, today, tomorrow, week, later, no_date (open tasks in that due-date window) or done (tasks closed as done in the last 7 days); status (open|done|cancelled), type (call|meeting|follow_up|todo), assigned_user_id, contact_id, deal_id, search (up to 100 characters, a case-insensitive substring of the task title, the contact name or the deal title), sort (due_at|created_at), plus the older overdue, due_before, due_after and mine=1 (same as scope=mine). Pagination is page/perPage (5–100, default 20). Every filter only narrows the visible area, never widens it. A request with a non-empty search is limited to 30 per minute per user.

Task visibility: a holder of deal_task.view_all (admin, manager) sees every task of the company; without it (agent) — tasks assigned to them, created by them, and nobody's tasks. When channel access rules (team channel_access) hide some channels from the user, other people's and nobody's tasks about a contact are visible only if the contact has no conversations or at least one of its threads sits on an accessible channel; the user's own tasks stay visible. Tasks of a deal in the recycle bin are invisible to everyone. A task outside the visible area answers 404 on both read and write — the existence of someone else's task is not revealed.

Per-row rights of an agent (without deal_task.view_all): on create and update assigned_user_id may only be the agent themself or null — otherwise 403 "You may only assign a task to yourself or leave it unassigned."; only their own tasks (assigned to or created by them) can be changed and deleted; a nobody's task created by someone else can only be taken — a PATCH whose body is exactly {"assigned_user_id": <my id>}; any other body on such a task returns 404. A holder of deal_task.view_all can assign to any company member. For the client's convenience every task carries ready-made can_update and can_delete flags.

Fields of POST/PATCH /deal-tasks: besides the existing ones (deal_id, contact_id — own company only, type, title up to 255 characters, description — now up to 10,000 characters, due_at, remind_at, assigned_user_id, status on PATCH) there are new is_all_day (boolean), reminder_disabled (boolean) and remind_offset_minutes (0, 15, 60 or 1440, or null). A remind_at later than the deadline (due_at from the body, or the task's current deadline on a PATCH without it) is rejected with 422 "The reminder may not be later than the deadline.". A successful create answers 201.

The task object (DealTaskResource) keeps every previous key and additionally returns: is_all_day, reminder_disabled, remind_offset_minutes, duration_minutes (length in minutes, 5–720, or null; settable in POST/PATCH, filled by an AI appointment booking), reminded_at, snoozed_until, is_overdue (same as overdue), created_by and created_by_user ({id, name} or null), source (human — created by a person through the API, automation — by an automation node, ai — by an AI agent, null — unknown), conversation_id (the contact's latest active conversation, but only if the caller can open it, otherwise null), can_update, can_delete, and the nested deal ({id, title, deleted}), contact ({id, display_name}) and assigned_user ({id, name}).

GET /deal-tasks/counters (and the tasks_counters part of GET /bootstrap) additionally returns done_today — the number of tasks of the visible area completed today (in the company time zone) — and mine.done_today — the same for tasks assigned to you. Unlike the other counters these count closed tasks, not open ones.

New routes: GET /deal-tasks/{task} — a single task (outside the visible area — 404); GET /deal-tasks/counters — no parameters, returns {overdue, today, upcoming_7d, unassigned, mine: {overdue, today}} over the open tasks of the visible area (60 requests per minute per user); GET /deal-tasks/calendar?from&to — compact items of tasks due within [from, to) (a window of at most 42 days, otherwise 422; filters scope, type, status, assigned_user_id, contact_id), at most 1,000 of them, with truncated: true beyond that; POST /deal-tasks/{task}/snooze — "remind me later": body {"minutes": 15|60} or {"until": "<ISO time>"} (in the future, at most 31 days ahead); it moves only snoozed_until and leaves the deadline and remind_at alone; rights as for PATCH (someone else's task — 404), a closed task — 409 with error: task_not_open.

Notifications and realtime: assignment sends task_assigned, a due reminder sends task_reminder to one recipient — the first eligible one along the chain "assignee → deal's responsible → contact's responsible → task author" (eligible = a company member holding deal_task.view and deal_task.update who can see the task); when nobody is eligible, every holder of deal_task.view_all gets a single task_reminder_unassigned row grouped per day. Every task change (by any writer) is published as a deal_task.changed event on private-company.{companyId} with a deliberately thin payload {task_id, action, assigned_user_id} (action is created, updated or deleted) — the client re-reads its own queries with its own rights (see "Realtime events"). The "Create task" automation node (create-task) now creates a real task (it used to leave a private note in the conversation): task_type (default follow_up), assignee — a user id of your own company (otherwise the task stays unassigned), due_in_hours, linked to the conversation's open deal; the priority field is no longer used.

GET /deals/{deal}/tasks still returns every task of the deal to anyone who sees the deal, with no "mine/nobody's" narrowing, but a user whom channel access rules hide some channels from does not get tasks about contacts that have conversations none of whose threads sits on a channel accessible to them.

Reference lists and settings

Lost reasons (/lost-reasons; editing needs the lost_reason.manage permission) are unique by name within a company. The auto-creation settings (/company/deal-settings) return an already-resolved default_currency (defaulting to KZT when it is null in the database); auto_create_deal_enabled cannot be turned on without at least one funnel in the company (422).

Since 2026-09-29 the auto-lead settings (GET /company/deal-settings needs deal.view, POST needs deal_settings.manage) carry extra fields, always returned as resolved values: auto_lead_mode — off (deals only manually, by an automation or an AI agent), unsorted (auto-leads go to "Unsorted") or direct (straight into the funnel's first stage); repeat_contact_policy — what an appeal from a client who already has an open deal does: attach_open (the default — attach to the contact's most recently updated open deal, an unsorted lead included) or always_new (a new conversation means a new deal; only the conversation's own open deal blocks it); repeat_cooldown_hours — a per-contact pause after the contact's deal is closed or their lead is declined, an integer from 0 to 720, default 24, 0 meaning no pause. Read-only: crm_lead_integrations — the active Bitrix24 and amoCRM integrations as {type, instance_id, creates_leads}, where creates_leads: true means the integration creates a lead for a new appeal on its own (a double-lead risk). The auto-lead's funnel is the channel's funnel from its CRM settings, otherwise default_funnel_id, otherwise the company's first funnel.

auto_lead_mode is the source of truth and auto_create_deal_enabled stays its mirror (false ⇔ off) for older clients: a request carrying only auto_create_deal_enabled: false turns auto-leads off, while true keeps the current mode if it is already on and otherwise switches to direct. Switching from off to any other mode records the moment it was turned on: dealless conversations that started earlier never get a lead. New companies start in unsorted mode; companies that existed before 2026-09-29 follow their earlier toggle: direct if the auto-deal was on, otherwise off. A mode other than off cannot be turned on without at least one funnel — 422 {"data": {"message": "Cannot enable auto-create deal without at least one funnel."}}.

Bulk actions on deals

POST /deals/bulk applies one action to a list of deals. Body: action — move_stage, assign or delete; deal_ids — 1 to 100 distinct ids; move_stage requires funnel_stage_id (a stage of your company), plus lost_reason_id or lost_reason when the target stage is a "lost" one; assign requires the responsible_user_id key (a member of the company; null clears the owner). Permissions: deal.update for move_stage and assign, deal.delete for delete (without it the whole request is refused with 403); an embed token gets 403. Limits: 20 requests per minute per user and 30 per minute per company (429).

Up to and including 25 deals are processed inside the request: a 200 response with data.operation_id, data.status (completed), data.results[] ({deal_id, result}; validation_error also carries a message), data.summary ({ok, failed}; unchanged counts as success) and data.undo_available. More than 25 — 202 with {operation_id, status: "queued"}; the outcome arrives as the deal.bulk.completed realtime event on the author's user channel (see the realtime events guide). Per-deal result codes: ok, unchanged (already in the target state), not_found (no such deal in the company), unsorted_locked (a lead in "Unsorted" — accept it first), stage_mismatch (the stage does not fit the deal), validation_error, error. Each deal goes through the regular update/delete logic, so it fires the deal.updated / deal.deleted webhooks carrying data.meta.bulk_operation_id, and automations.

POST /deals/bulk/{operation}/undo rolls back a finished bulk move_stage or assign (there is no undo for delete — use POST /deals/{deal}/restore). The window is 10 minutes from the operation, only its author can undo it, and only once; the deal.update permission is required. An unknown, expired, foreign or already undone operation is 404. Response: data.operation_id (the id of the undo itself), data.undo_of, data.status, data.results[] with the codes ok, changed_since (the deal was changed after the operation — left untouched), not_found, validation_error, error, and data.summary. The undo webhooks carry meta.bulk_operation_id too — the id of the undo operation.

Endpoints

MethodPath
GET/v1/deals

List deals, not paginated (filters: contact_id, funnel_id, responsible_user_id, status, search; include_unsorted — together with "Unsorted"; perPage 1–100 — a cap on the number of rows)

POST/v1/deals

Create a deal

POST/v1/deals/bulk

Bulk action on deals: move_stage | assign | delete (up to 100 deals; up to 25 run at once, more are queued)

POST/v1/deals/bulk/{operation}/undo

Undo a bulk move_stage/assign within 10 minutes (the operation author only)

GET/v1/deals/{deal}

Get a deal

POST/v1/deals/{deal}

Update a deal

DELETE/v1/deals/{deal}

Delete a deal (soft delete)

POST/v1/deals/{deal}/restore

Restore a soft-deleted deal

GET/v1/deals/{deal}/activities

Deal activity feed (timeline)

POST/v1/deals/{deal}/notes

Add a note to the deal timeline

GET/v1/deals/{deal}/tasks

Tasks attached to a deal

POST/v1/deals/{deal}/accept

Accept a lead out of "Unsorted" (funnel_stage_id?, responsible_user_id?) — the deal is born for integrations at this point

POST/v1/deals/{deal}/decline

Decline a lead in "Unsorted" (exclude_contact? — stop creating leads for the contact)

POST/v1/deals/{deal}/link

Link a lead in "Unsorted" to an open deal (target_deal_id) — its conversations move there and the lead is removed

GET/v1/company/deal-settings

Company deal auto-creation settings (incl. auto_lead_mode, repeat_contact_policy, repeat_cooldown_hours, crm_lead_integrations)

POST/v1/company/deal-settings

Save the deal auto-creation and auto-lead settings

GET/v1/deal-tasks

List visible tasks (scope: visible|mine|unassigned|all; filters: bucket, status, type, assigned_user_id, contact_id, deal_id, search, sort, overdue, mine)

POST/v1/deal-tasks

Create a task

PATCH/v1/deal-tasks/{task}

Update or close a task

DELETE/v1/deal-tasks/{task}

Delete a task

GET/v1/deal-tasks/counters

Open-task counters: overdue, today, next 7 days, unassigned and mine, plus tasks done today

GET/v1/deal-tasks/calendar

Tasks due within the from–to window (up to 42 days, up to 1,000 items)

GET/v1/deal-tasks/{task}

Get a single task

POST/v1/deal-tasks/{task}/snooze

Snooze the reminder (minutes: 15|60 or until)

GET/v1/deal-types

List deal types

POST/v1/deal-types

Create a deal type

PATCH/v1/deal-types/{dealType}

Update a deal type

DELETE/v1/deal-types/{dealType}

Delete a deal type

GET/v1/lost-reasons

List lost reasons

POST/v1/lost-reasons

Create a lost reason

PATCH/v1/lost-reasons/{lostReason}

Update a lost reason

DELETE/v1/lost-reasons/{lostReason}

Delete a lost reason

Examples

Bulk move of deals to another stage

Request

bash
curl -X POST https://api.aisar.app/v1/deals/bulk \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "move_stage",
    "deal_ids": [501, 502, 503],
    "funnel_stage_id": 12
  }'

Response

json
{
  "data": {
    "operation_id": "0b6f5e1c-3a52-4d0e-9d0a-8f3f2c6a1b77",
    "status": "completed",
    "results": [
      { "deal_id": 501, "result": "ok" },
      { "deal_id": 502, "result": "unchanged" },
      { "deal_id": 503, "result": "not_found" }
    ],
    "summary": { "ok": 2, "failed": 1 },
    "undo_available": true
  }
}

List a contact's deals (contact_id filter)

Request

bash
curl -X GET "https://api.aisar.app/v1/deals?contact_id=123&status=open" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 501,
      "company_id": 1,
      "contact_id": 123,
      "funnel_id": 1,
      "funnel_stage_id": 3,
      "title": "Office renovation",
      "amount": "5000.00",
      "currency": "KZT",
      "status": "open",
      "type_id": null,
      "source": "api",
      "utm_source": null,
      "utm_medium": null,
      "utm_campaign": null,
      "probability": null,
      "comments": null,
      "begin_date": null,
      "responsible_user_id": 5,
      "expected_close_date": null,
      "closed_at": null,
      "lost_reason": null,
      "lost_reason_id": null,
      "closed_amount": null,
      "closed_currency": null,
      "stage_changed_at": "2026-03-15T10:00:00.000000Z",
      "unsorted_at": null,
      "lead_conversation_id": null,
      "channel": null,
      "custom_fields": null,
      "created_at": "2026-03-15T10:00:00.000000Z",
      "updated_at": "2026-03-16T12:00:00.000000Z",
      "contact": { "id": 123, "display_name": "Aigerim K.", "avatar": null },
      "responsible_user": { "id": 5, "name": "Ivan" },
      "conversations_count": 2
    }
  ]
}

Search deals by title or contact

Request

bash
curl -X GET "https://api.aisar.app/v1/deals?search=renovation&responsible_user_id=5" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 501,
      "company_id": 1,
      "contact_id": 123,
      "funnel_id": 1,
      "funnel_stage_id": 3,
      "title": "Office renovation",
      "amount": "5000.00",
      "currency": "KZT",
      "status": "open",
      "responsible_user_id": 5,
      "channel": null,
      "created_at": "2026-03-15T10:00:00.000000Z",
      "updated_at": "2026-03-16T12:00:00.000000Z",
      "contact": { "id": 123, "display_name": "Aigerim K.", "avatar": null },
      "responsible_user": { "id": 5, "name": "Ivan" },
      "conversations_count": 2
    }
  ]
}

Create a deal

Request

bash
curl -X POST https://api.aisar.app/v1/deals \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": 123,
    "funnel_id": 1,
    "funnel_stage_id": 3,
    "title": "Office renovation",
    "amount": "5000.00",
    "currency": "KZT",
    "source": "api",
    "responsible_user_id": 5
  }'

Response

json
{
  "data": {
    "id": 501,
    "company_id": 1,
    "contact_id": 123,
    "funnel_id": 1,
    "funnel_stage_id": 3,
    "title": "Office renovation",
    "amount": "5000.00",
    "currency": "KZT",
    "status": "open",
    "type_id": null,
    "source": "api",
    "utm_source": null,
    "utm_medium": null,
    "utm_campaign": null,
    "probability": null,
    "comments": null,
    "begin_date": null,
    "responsible_user_id": 5,
    "expected_close_date": null,
    "closed_at": null,
    "lost_reason": null,
    "lost_reason_id": null,
    "closed_amount": null,
    "closed_currency": null,
    "stage_changed_at": "2026-03-15T10:00:00.000000Z",
    "unsorted_at": null,
    "lead_conversation_id": null,
    "channel": null,
    "custom_fields": null,
    "created_at": "2026-03-15T10:00:00.000000Z",
    "updated_at": "2026-03-15T10:00:00.000000Z"
  }
}

Mark a deal as lost

Request

bash
curl -X POST https://api.aisar.app/v1/deals/501 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "lost",
    "lost_reason_id": 3
  }'

Response

json
{
  "data": {
    "id": 501,
    "status": "lost",
    "lost_reason_id": 3,
    "lost_reason": null,
    "closed_amount": "5000.00",
    "closed_currency": "KZT",
    "closed_at": "2026-03-17T09:00:00.000000Z",
    "stage_changed_at": "2026-03-17T09:00:00.000000Z",
    "updated_at": "2026-03-17T09:00:00.000000Z"
  }
}

Mark a deal as lost (free-text reason)

Request

bash
curl -X POST https://api.aisar.app/v1/deals/501 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "lost",
    "lost_reason": "Ушли к конкуренту"
  }'

Response

json
{
  "data": {
    "id": 501,
    "status": "lost",
    "lost_reason_id": null,
    "lost_reason": "Ушли к конкуренту",
    "closed_amount": "5000.00",
    "closed_currency": "KZT",
    "closed_at": "2026-03-17T09:00:00.000000Z",
    "stage_changed_at": "2026-03-15T10:00:00.000000Z",
    "updated_at": "2026-03-17T09:00:00.000000Z"
  }
}

Move a deal to another funnel (with a stage)

Request

bash
curl -X POST https://api.aisar.app/v1/deals/501 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "funnel_id": 2,
    "funnel_stage_id": 8,
    "responsible_user_id": 5
  }'

Response

json
{
  "data": {
    "id": 501,
    "funnel_id": 2,
    "funnel_stage_id": 8,
    "status": "open",
    "responsible_user_id": 5,
    "stage_changed_at": "2026-03-16T12:00:00.000000Z",
    "updated_at": "2026-03-16T12:00:00.000000Z"
  }
}

Deal activity feed

Request

bash
curl -X GET "https://api.aisar.app/v1/deals/501/activities?perPage=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "items": [
      {
        "id": 1,
        "deal_id": 501,
        "type": "stage_changed",
        "actor_user_id": 5,
        "body": null,
        "metadata": { "from": 1, "to": 3 },
        "created_at": "2026-03-15T10:05:00.000000Z",
        "actor": { "id": 5, "name": "Ivan", "lastname": "Petrov" }
      }
    ],
    "pagination": { "page": 1, "perPage": 20, "total": 4, "lastPage": 1 }
  }
}

Note on the deal timeline

Request

bash
curl -X POST https://api.aisar.app/v1/deals/501/notes \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Клиент просит счёт на юрлицо"
  }'

Response

json
{
  "data": {
    "message": "Note added.",
    "item": {
      "id": 902,
      "deal_id": 501,
      "type": "note",
      "actor_user_id": 5,
      "body": "Клиент просит счёт на юрлицо",
      "metadata": null,
      "created_at": "2026-03-16T09:00:00.000000Z"
    }
  }
}

Deal auto-creation settings

Request

bash
curl -X GET https://api.aisar.app/v1/company/deal-settings \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "auto_create_deal_enabled": true,
    "default_funnel_id": 1,
    "default_deal_title_template": "{contact_displayName}",
    "default_currency": "KZT",
    "default_stale_after_days": null,
    "auto_lead_mode": "unsorted",
    "repeat_contact_policy": "attach_open",
    "repeat_cooldown_hours": 24,
    "crm_lead_integrations": [
      { "type": "bitrix24", "instance_id": 12, "creates_leads": true }
    ],
    "has_funnels": true
  }
}

Save deal and auto-lead settings

Request

bash
curl -X POST https://api.aisar.app/v1/company/deal-settings \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_lead_mode": "unsorted",
    "repeat_contact_policy": "attach_open",
    "repeat_cooldown_hours": 48,
    "default_funnel_id": 1,
    "default_deal_title_template": "{contact_displayName}",
    "default_currency": "KZT"
  }'

Response

json
{
  "data": {
    "auto_create_deal_enabled": true,
    "default_funnel_id": 1,
    "default_deal_title_template": "{contact_displayName}",
    "default_currency": "KZT",
    "default_stale_after_days": null,
    "auto_lead_mode": "unsorted",
    "repeat_contact_policy": "attach_open",
    "repeat_cooldown_hours": 48,
    "crm_lead_integrations": [],
    "has_funnels": true
  }
}

Accept a lead out of "Unsorted"

Request

bash
curl -X POST https://api.aisar.app/v1/deals/640/accept \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "funnel_stage_id": 2,
    "responsible_user_id": 5
  }'

Response

json
{
  "data": {
    "id": 640,
    "company_id": 1,
    "contact_id": 318,
    "funnel_id": 1,
    "funnel_stage_id": 2,
    "title": "Dana S.",
    "amount": null,
    "currency": "KZT",
    "status": "open",
    "source": "whatsapp",
    "responsible_user_id": 5,
    "stage_changed_at": "2026-09-29T16:05:00.000000Z",
    "unsorted_at": null,
    "lead_conversation_id": 9120,
    "channel": "whatsapp",
    "created_at": "2026-09-29T15:40:00.000000Z",
    "updated_at": "2026-09-29T16:05:00.000000Z",
    "contact": { "id": 318, "display_name": "Dana S.", "avatar": null },
    "responsible_user": { "id": 5, "name": "Ivan" },
    "conversations_count": null
  }
}

Decline a lead and exclude the contact from auto-leads

Request

bash
curl -X POST https://api.aisar.app/v1/deals/641/decline \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "exclude_contact": true
  }'

Response

json
{
  "data": {
    "declined": true
  }
}

Link a lead to the client's open deal

Request

bash
curl -X POST https://api.aisar.app/v1/deals/642/link \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "target_deal_id": 501
  }'

Response

json
{
  "data": {
    "id": 501,
    "company_id": 1,
    "contact_id": 123,
    "funnel_id": 1,
    "funnel_stage_id": 3,
    "title": "Office renovation",
    "status": "open",
    "responsible_user_id": 5,
    "unsorted_at": null,
    "lead_conversation_id": null,
    "updated_at": "2026-09-29T16:10:00.000000Z",
    "contact": { "id": 123, "display_name": "Aigerim K.", "avatar": null },
    "responsible_user": { "id": 5, "name": "Ivan" }
  }
}

Changing the stage of an unsorted lead — 409

Request

bash
curl -X POST https://api.aisar.app/v1/deals/640 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "funnel_stage_id": 3
  }'

Response

json
{
  "data": {
    "message": "The deal is in Unsorted: accept it first."
  }
}

List tasks (mine, overdue)

Request

bash
curl -X GET "https://api.aisar.app/v1/deal-tasks?scope=mine&bucket=overdue&perPage=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "items": [
      {
        "id": 1,
        "company_id": 1,
        "deal_id": 501,
        "contact_id": null,
        "type": "call",
        "title": "Перезвонить клиенту",
        "description": null,
        "due_at": "2026-07-08T09:00:00.000000Z",
        "remind_at": "2026-07-08T08:45:00.000000Z",
        "status": "open",
        "assigned_user_id": 5,
        "completed_at": null,
        "overdue": true,
        "created_at": "2026-07-07T10:00:00.000000Z",
        "updated_at": "2026-07-07T10:00:00.000000Z",
        "is_all_day": false,
        "reminder_disabled": false,
        "remind_offset_minutes": 15,
        "reminded_at": "2026-07-08T08:45:05.000000Z",
        "snoozed_until": null,
        "is_overdue": true,
        "created_by": 5,
        "source": "human",
        "conversation_id": null,
        "can_update": true,
        "can_delete": true,
        "deal": { "id": 501, "title": "Поставка оборудования", "deleted": false },
        "contact": null,
        "assigned_user": { "id": 5, "name": "Айгерим" },
        "created_by_user": { "id": 5, "name": "Айгерим" }
      }
    ],
    "pagination": { "page": 1, "perPage": 20, "total": 7, "lastPage": 1 }
  }
}

Create a deal task

Request

bash
curl -X POST https://api.aisar.app/v1/deal-tasks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deal_id": 501,
    "type": "call",
    "title": "Перезвонить клиенту",
    "due_at": "2026-07-08T09:00:00Z",
    "remind_at": "2026-07-08T08:45:00Z",
    "remind_offset_minutes": 15,
    "assigned_user_id": 5
  }'

Response

json
{
  "data": {
    "id": 1,
    "company_id": 1,
    "deal_id": 501,
    "contact_id": null,
    "type": "call",
    "title": "Перезвонить клиенту",
    "description": null,
    "due_at": "2026-07-08T09:00:00.000000Z",
    "remind_at": "2026-07-08T08:45:00.000000Z",
    "status": "open",
    "assigned_user_id": 5,
    "completed_at": null,
    "overdue": false,
    "created_at": "2026-07-07T10:00:00.000000Z",
    "updated_at": "2026-07-07T10:00:00.000000Z",
    "is_all_day": false,
    "reminder_disabled": false,
    "remind_offset_minutes": 15,
    "reminded_at": null,
    "snoozed_until": null,
    "is_overdue": false,
    "created_by": 3,
    "source": "human",
    "conversation_id": null,
    "can_update": true,
    "can_delete": true,
    "deal": { "id": 501, "title": "Поставка оборудования", "deleted": false },
    "contact": null,
    "assigned_user": { "id": 5, "name": "Айгерим" },
    "created_by_user": { "id": 3, "name": "Данияр" }
  }
}

Task counters

Request

bash
curl -X GET https://api.aisar.app/v1/deal-tasks/counters \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "overdue": 4,
    "today": 6,
    "upcoming_7d": 11,
    "unassigned": 2,
    "done_today": 9,
    "mine": { "overdue": 1, "today": 3, "done_today": 4 }
  }
}

Task calendar for a week

Request

bash
curl -X GET "https://api.aisar.app/v1/deal-tasks/calendar?from=2026-07-06T00:00:00Z&to=2026-07-13T00:00:00Z&scope=mine" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "items": [
      {
        "id": 1,
        "title": "Перезвонить клиенту",
        "type": "call",
        "due_at": "2026-07-08T09:00:00.000000Z",
        "is_all_day": false,
        "status": "open",
        "assigned_user_id": 5,
        "deal_id": 501,
        "contact_id": null,
        "can_update": true
      }
    ],
    "truncated": false
  }
}

Snooze the reminder for an hour

Request

bash
curl -X POST https://api.aisar.app/v1/deal-tasks/1/snooze \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "minutes": 60
  }'

Response

json
{
  "data": {
    "id": 1,
    "deal_id": 501,
    "type": "call",
    "status": "open",
    "due_at": "2026-07-08T09:00:00.000000Z",
    "remind_at": "2026-07-08T08:45:00.000000Z",
    "snoozed_until": "2026-07-08T09:45:00.000000Z",
    "assigned_user_id": 5,
    "can_update": true
  }
}

Close a task (PATCH)

Request

bash
curl -X PATCH https://api.aisar.app/v1/deal-tasks/1 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "done"
  }'

Response

json
{
  "data": {
    "id": 1,
    "deal_id": 501,
    "type": "call",
    "status": "done",
    "assigned_user_id": 5,
    "completed_at": "2026-07-08T09:30:00.000000Z",
    "updated_at": "2026-07-08T09:30:00.000000Z"
  }
}

Create a lost reason

Request

bash
curl -X POST https://api.aisar.app/v1/lost-reasons \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Дорого",
    "is_active": true,
    "order": 0
  }'

Response

json
{
  "data": {
    "id": 1,
    "company_id": 1,
    "name": "Дорого",
    "is_active": true,
    "order": 0,
    "created_at": "2026-07-07T10:00:00.000000Z",
    "updated_at": "2026-07-07T10:00:00.000000Z"
  }
}