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
| Method | Path | Summary |
|---|---|---|
| GET | /v1/dealsList 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) | 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/dealsCreate a deal | Create a deal |
| POST | /v1/deals/bulkBulk action on deals: move_stage | assign | delete (up to 100 deals; up to 25 run at once, more are queued) | 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}/undoUndo a bulk move_stage/assign within 10 minutes (the operation author only) | Undo a bulk move_stage/assign within 10 minutes (the operation author only) |
| GET | /v1/deals/{deal}Get a deal | Get a deal |
| POST | /v1/deals/{deal}Update a deal | Update a deal |
| DELETE | /v1/deals/{deal}Delete a deal (soft delete) | Delete a deal (soft delete) |
| POST | /v1/deals/{deal}/restoreRestore a soft-deleted deal | Restore a soft-deleted deal |
| GET | /v1/deals/{deal}/activitiesDeal activity feed (timeline) | Deal activity feed (timeline) |
| POST | /v1/deals/{deal}/notesAdd a note to the deal timeline | Add a note to the deal timeline |
| GET | /v1/deals/{deal}/tasksTasks attached to a deal | Tasks attached to a deal |
| POST | /v1/deals/{deal}/acceptAccept a lead out of "Unsorted" (funnel_stage_id?, responsible_user_id?) — the deal is born for integrations at this point | 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}/declineDecline a lead in "Unsorted" (exclude_contact? — stop creating leads for the contact) | Decline a lead in "Unsorted" (exclude_contact? — stop creating leads for the contact) |
| POST | /v1/deals/{deal}/linkLink a lead in "Unsorted" to an open deal (target_deal_id) — its conversations move there and the lead is removed | 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-settingsCompany deal auto-creation settings (incl. auto_lead_mode, repeat_contact_policy, repeat_cooldown_hours, crm_lead_integrations) | Company deal auto-creation settings (incl. auto_lead_mode, repeat_contact_policy, repeat_cooldown_hours, crm_lead_integrations) |
| POST | /v1/company/deal-settingsSave the deal auto-creation and auto-lead settings | Save the deal auto-creation and auto-lead settings |
| GET | /v1/deal-tasksList visible tasks (scope: visible|mine|unassigned|all; filters: bucket, status, type, assigned_user_id, contact_id, deal_id, search, sort, overdue, mine) | 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-tasksCreate a task | Create a task |
| PATCH | /v1/deal-tasks/{task}Update or close a task | Update or close a task |
| DELETE | /v1/deal-tasks/{task}Delete a task | Delete a task |
| GET | /v1/deal-tasks/countersOpen-task counters: overdue, today, next 7 days, unassigned and mine, plus tasks done today | Open-task counters: overdue, today, next 7 days, unassigned and mine, plus tasks done today |
| GET | /v1/deal-tasks/calendarTasks due within the from–to window (up to 42 days, up to 1,000 items) | 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 | Get a single task |
| POST | /v1/deal-tasks/{task}/snoozeSnooze the reminder (minutes: 15|60 or until) | Snooze the reminder (minutes: 15|60 or until) |
| GET | /v1/deal-typesList deal types | List deal types |
| POST | /v1/deal-typesCreate a deal type | Create a deal type |
| PATCH | /v1/deal-types/{dealType}Update a deal type | Update a deal type |
| DELETE | /v1/deal-types/{dealType}Delete a deal type | Delete a deal type |
| GET | /v1/lost-reasonsList lost reasons | List lost reasons |
| POST | /v1/lost-reasonsCreate a lost reason | Create a lost reason |
| PATCH | /v1/lost-reasons/{lostReason}Update a lost reason | Update a lost reason |
| DELETE | /v1/lost-reasons/{lostReason}Delete a lost reason | Delete a lost reason |
Examples
Bulk move of deals to another stage
Request
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
{
"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
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
{
"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
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
{
"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
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
{
"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
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
{
"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
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
{
"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
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
{
"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
curl -X GET "https://api.aisar.app/v1/deals/501/activities?perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"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
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
{
"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
curl -X GET https://api.aisar.app/v1/company/deal-settings \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"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
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
{
"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
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
{
"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
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
{
"data": {
"declined": true
}
}Link a lead to the client's open deal
Request
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
{
"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
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
{
"data": {
"message": "The deal is in Unsorted: accept it first."
}
}List tasks (mine, overdue)
Request
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
{
"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
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
{
"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
curl -X GET https://api.aisar.app/v1/deal-tasks/counters \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"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
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
{
"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
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
{
"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
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
{
"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
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
{
"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"
}
}