Notifications
In-app notifications (the "bell") are a per-user feed of events addressed to a specific cabinet user (for example, being assigned to a deal or to a deal task). This is the REST side of the same stream that arrives in real time as the notification.created event on the user's personal WebSocket channel: use the socket for the live push and badge, and these endpoints to backfill history on app load, read the unread count, and clear the badge.
Feed scope
The feed is scoped to the current user and their active company (current_company_id): the list, the counter, and "mark all" return only notifications for the active company; marking a single notification read/unread also resolves it within the active company (someone else's, another company's or unknown — 404).
Notification object
A notification object has these fields:
id— a UUID string.type— a snake_case string identifying the notification kind. The current catalog:conversation_assigned,call_missed,channel_disconnected,channel_banned,export_completed,export_failed,template_approved,template_rejected,broadcast_finished,broadcast_failed,history_sync_completed,ai_action_pending,deal_assigned,recording_retention_changed,recording_switched_off,recording_switched_on,task_assigned,task_reminder,task_reminder_unassigned(since 2026-09-26 the threetask_*types replace the formerdeal_task_assigned, which is no longer sent),lead_unsortedandlead_cap_reached(since 2026-09-29: new auto-leads in a funnel's "Unsorted" — one row per funnel carrying the current number of leads, sent to holders ofdeal.updateand marked read once the column is sorted; the company's hourly auto-lead limit was reached — to administrators, once an hour) and three AI-employee types (since 2026-10-10):ai_agent_budget_warning(the agent has spentbudget_warning_pctof its monthly limit, once per agent per month, to administrators),ai_handoff_waiting(the AI handed a dialog to people and nobody answered withinhandoff.notify_after_minutes, to the dialog participants),ai_model_retired(the provider stopped serving the agent's model; for a company-key agent an administrator has to pick a new one, to administrators); since 2026-10-15 two more types of the nightly AI-employee review:ai_review_enabled(the platform switched the review on for the company's busy agents, to administrators, before the first run;data:instance_idsanddisable={method, path, body}for the "switch off" button,PUT /v1/ai-agents/review-settingswith{enabled: false}) andai_review_proposed(a manager suggested a decision on a review issue, to administrators;data:issue_id,instance_id, no title or dialog fragments). The morning review digest (ai_review_digest, counters and a link, no issue texts) does not go to the feed: it is sent by e-mail and WhatsApp to administrators unless switched off inreview-settings; the set will grow, so render an unknowntypefrom itstitle/bodyrather than dropping it.category— the section the notification belongs to:inbox,calls,deals,tasks,channels,broadcasts,automations,data,team,billing,system. Always present — handy for grouping and filters in your own UI.severity— importance:info,success,warning,critical. Always present too.title— a short heading.body— a string ornull.action_url— the in-cabinet path the notification points at, ornull(a purely informational row with nothing to open). The path is relative, carries no domain, and may differ between recipients of the same event: for a missed call (call_missed) with a linked conversation it is always that conversation (/chat?conversation_id=...), while without one it is the console's callback queue (/telephony?tab=console&callback=...) for those who can manage telephony (thecompany_telephony.managepermission) and the call journal (/chat?filter=calls&call_id=...) for everyone else.group_key— the key used to collapse notifications of the same kind (e.g. several missed calls from one contact update a single row instead of piling up new ones), ornull.data— an object whose fields depend ontype(e.g.deal_id/deal_titlefordeal_assigned,call_id/contact_id/conversation_id/missed_last_hourforcall_missed; thetask_*types carrydeal_task_id,deal_id,company_id,assigned_user_id,unassigned, plustask_title/assigned_byontask_assignedandcount/last_deal_task_idontask_reminder_unassigned;lead_unsortedcarriescount,funnel_id,last_deal_id, andlead_cap_reachedcarrieslimit).read_at— the ISO timestamp when read, ornullwhile unread.expires_at— the ISO timestamp after which the notification is stale, ornull.created_at— when it was created.
List response shape and query parameters
Important about the list response shape: GET /v1/notifications returns the standard Laravel paginator envelope — the array under the data key, with the pagination meta fields (current_page, per_page, total, last_page, next_page_url, etc.) sitting flat at the top level. This differs from most REST groups, where a list comes back as data:{ items, pagination }.
Query parameters: filter=unread|all (defaults to all; any other value is rejected with 422), category (one of the category values above; an unknown one returns 422), categories[] (several category values at once, e.g. categories[]=deals&categories[]=tasks; combined with category by AND), q (case-insensitive search in title and body, 2-100 characters; % and _ match literally) and per_page (page size, 1–100, default 20). Note the snake_case per_page (not perPage). Notifications whose expires_at has passed appear in neither the list nor the counter.
Platform notifications (`GET /platform-notifications`)
GET /platform-notifications (2026-08-24) — not to be confused with the personal "bell" feed above: this is a company-scoped (not per-user) flat log of technical notices from the AISAR platform itself (currently only "channel disconnected", from NotifyChannelDisconnectedJob), replacing the old service-conversation mechanic in chat. It requires the conversation.view permission (the same one the /conversations family uses), the feed window is the last 90 days, newest first, page-based pagination (page/limit, limit 1-50, default 20; a pagination block {page, perPage, total, lastPage}). Rate limit: 120 requests per minute.
A list item: id, kind (the notification type, e.g. channel_disconnected), reason (a human-readable cause, or null), created_at, channel ({id, name, type} or null — null once the channel has since been deleted; the event itself survives), conversation_id and message_id (kept for backward compatibility with notifications inherited from the old mechanic; null on new rows). The feed has no per-item "read" mark — instead, PATCH /me/ui-preferences with aisar_notifications.seen_until (an ISO datetime or null) advances the unseen-cursor consumed by the counters.aisar_notifications key in the Conversations group (GET /conversations/counters).
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/notificationsThe user's notification feed in the active company (Laravel paginator envelope). Params: `filter=unread|all`, `category`, `categories[]`, `q` (2-100 chars), `per_page` (1-100, default 20). | The user's notification feed in the active company (Laravel paginator envelope). Params: `filter=unread|all`, `category`, `categories[]`, `q` (2-100 chars), `per_page` (1-100, default 20). |
| GET | /v1/notifications/unread-countCount of the user's unread notifications in the active company. Response: `{ "count": N, "critical": M, "by_category": { "<category>": n } }` — `critical` counts only unread rows with `severity: "critical"`; `by_category` is the unread count per section, only for n>0 (`{}` when nothing is unread). | Count of the user's unread notifications in the active company. Response: `{ "count": N, "critical": M, "by_category": { "<category>": n } }` — `critical` counts only unread rows with `severity: "critical"`; `by_category` is the unread count per section, only for n>0 (`{}` when nothing is unread). |
| POST | /v1/notifications/{notification}/readMark a single notification read (by its UUID). Response: `{ "status": "ok" }`; 404 if the id is not a UUID, or the notification is missing or owned by another user or company. | Mark a single notification read (by its UUID). Response: `{ "status": "ok" }`; 404 if the id is not a UUID, or the notification is missing or owned by another user or company. |
| POST | /v1/notifications/{notification}/unreadMark a notification unread again (by UUID). Response: `{ "status": "ok" }`; 404 for a foreign or unknown id. | Mark a notification unread again (by UUID). Response: `{ "status": "ok" }`; 404 for a foreign or unknown id. |
| POST | /v1/notifications/bulkBulk-mark notifications: body `{ "ids": [uuid, ...] (1-100), "action": "read"|"unread" }`. Response: `{ "updated": n }`; ids of other users and companies are silently ignored. | Bulk-mark notifications: body `{ "ids": [uuid, ...] (1-100), "action": "read"|"unread" }`. Response: `{ "updated": n }`; ids of other users and companies are silently ignored. |
| POST | /v1/notifications/read-allMark all of the user's unread notifications in the active company as read. Response: `{ "status": "ok" }`. | Mark all of the user's unread notifications in the active company as read. Response: `{ "status": "ok" }`. |
| GET | /v1/platform-notificationsCompany-scoped log of platform notifications (e.g. "channel disconnected") over the last 90 days. | Company-scoped log of platform notifications (e.g. "channel disconnected") over the last 90 days. |
Examples
List unread notifications
Request
curl -X GET "https://api.aisar.app/v1/notifications?filter=unread&per_page=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"current_page": 1,
"data": [
{
"id": "3f1e6a20-0c7b-4a9e-b2d1-8a5c1e000001",
"type": "deal_assigned",
"category": "deals",
"severity": "info",
"title": "You were assigned to deal \"Website redesign\"",
"body": "Assigned by Aigerim",
"action_url": "/deals/42",
"group_key": null,
"data": {
"deal_id": 42,
"deal_title": "Website redesign",
"assigned_by": 3
},
"read_at": null,
"expires_at": null,
"created_at": "2026-02-10T09:15:00.000000Z"
}
],
"first_page_url": "https://api.aisar.app/v1/notifications?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "https://api.aisar.app/v1/notifications?page=1",
"next_page_url": null,
"path": "https://api.aisar.app/v1/notifications",
"per_page": 20,
"prev_page_url": null,
"to": 1,
"total": 1
}Unread count
Request
curl -X GET "https://api.aisar.app/v1/notifications/unread-count" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"count": 3,
"critical": 1,
"by_category": { "deals": 2, "tasks": 1 }
}Mark one notification read
Request
curl -X POST "https://api.aisar.app/v1/notifications/3f1e6a20-0c7b-4a9e-b2d1-8a5c1e000001/read" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"status": "ok"
}Mark a notification unread
Request
curl -X POST "https://api.aisar.app/v1/notifications/3f1e6a20-0c7b-4a9e-b2d1-8a5c1e000001/unread" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"status": "ok"
}Bulk mark notifications
Request
curl -X POST "https://api.aisar.app/v1/notifications/bulk" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["3f1e6a20-0c7b-4a9e-b2d1-8a5c1e000001", "3f1e6a20-0c7b-4a9e-b2d1-8a5c1e000002"], "action": "read"}'Response
{
"updated": 2
}Mark all read
Request
curl -X POST "https://api.aisar.app/v1/notifications/read-all" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"status": "ok"
}Platform notification log
Request
curl -X GET "https://api.aisar.app/v1/platform-notifications?limit=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 340,
"kind": "channel_disconnected",
"reason": "logged_out",
"created_at": "2026-08-24T09:12:00.000000Z",
"channel": { "id": 3, "name": "Основной WhatsApp", "type": "whatsapp" },
"conversation_id": null,
"message_id": null
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1 }
}
}