ClouisleClouisle

Notifications API

Read the in-app notification inbox and let admins publish or delete notifications

The Notifications API has two groups: the in-app inbox for every signed-in user (/api/v1/notifications — list visible notifications, unread count, mark as read) and the /api/v1/admin/notifications routes used by administrators to publish, browse, and delete notifications. A notification's scope (global / team / user) decides who can see it, and read state is per user.

Endpoints

MethodPathPurposePermission
GET/api/v1/notificationsList notifications visible to the callerSigned-in user
GET/api/v1/notifications/unread-countCount the caller's unread notificationsSigned-in user
POST/api/v1/notifications/readMark specific notifications, or all visible ones, as readSigned-in user
GET/api/v1/admin/notificationsBrowse notifications across scopesGlobal admin, or team admin (must name a team)
POST/api/v1/admin/notificationsCreate and dispatch a notificationadmin:notification:create
DELETE/api/v1/admin/notifications/{notification_id}Delete a notificationadmin:notification:delete

Every endpoint requires Authorization: Bearer <token>.

Prerequisites

The inbox endpoints only require a session (get_current_active_user); there is no permission code.

Among the admin endpoints, GET performs no permission-code check but authorizes by scope: the caller must have global admin access (is_superuser, or any role granting admin:dashboard:access / *), otherwise they must pass a team_id they administer as OWNER/ADMIN. POST requires admin:notification:create and DELETE requires admin:notification:delete.

External delivery channels (email, DingTalk, WeCom, Feishu, webhook, Slack) must be enabled and configured in the Settings API first, otherwise the create request is rejected.

Visibility

The caller can see a notification if and only if at least one of these holds:

  • scope is global;
  • scope is user and user_id is the caller;
  • scope is team and team_id is one of the caller's teams.

Notifications whose expires_at is in the past are excluded from the list and the unread count; only include_expired=true (admin route) brings them back, for troubleshooting.

Notification object

FieldTypeDescription
idstring (UUID)Notification ID
scopestringglobal, team, or user
team_idstring (UUID) | nullTarget team for team scope
user_idstring (UUID) | nullTarget user for user scope
typestringNotification type key, max 100 characters (automatic notifications use names such as team.member_added, workflow.run_failed, security.account_locked)
sourcestringsystem, user, or biz
titlestringTitle, max 255 characters
contentstringBody text
levelstringlow, medium, or high
dataobject | nullArbitrary payload
link_urlstring | nullIn-app link to open, max 500 characters
statusstringAlways active
expires_atstring (ISO 8601) | nullExpiry timestamp
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp
is_readbooleanWhether the caller has read it
read_atstring (ISO 8601) | nullWhen the caller read it
deliveriesarrayPer-channel delivery records

Delivery record (deliveries[])

FieldTypeDescription
channelstringemail, dingtalk, wechat, feishu, webhook, or slack
statusstringpending, sending, success, or failed
error_messagestring | nullFailure reason (localized)
retry_countintegerRetry count, default 0
sent_atstring (ISO 8601) | nullSuccessful send time
created_at / updated_atstring (ISO 8601)Record creation and update timestamps

The user-facing list does not populate delivery records — deliveries is always [] there; only the admin list and the create response include deliveries. is_read / read_at are the opposite: they appear only on the user side, and the admin list always reports false / null.


List notifications

GET /api/v1/notifications

Returns the notifications visible to the caller, ordered by created_at descending.

Query parameters

ParameterTypeRequiredDefaultDescription
scopestringNo-Filter by scope: global, team, user
typestringNo-Filter by notification type key
levelstringNo-Filter by level: low, medium, high
searchstringNo-Case-insensitive contains match on title, content, type
unread_onlybooleanNofalseReturn only unread notifications
created_fromstringNo-Lower bound on created_at
created_tostringNo-Upper bound on created_at
pageintegerNo1Page number, minimum 1
page_sizeintegerNo20Items per page, 1–100
curl -X GET "https://your-domain.com/api/v1/notifications?unread_only=true&page=1&page_size=20" \
  -H "Authorization: Bearer YOUR_TOKEN"

200 OK:

{
  "code": 0,
  "data": {
    "items": [
      {
        "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "scope": "team",
        "team_id": "550e8400-e29b-41d4-a716-446655440000",
        "user_id": null,
        "type": "agent.published",
        "source": "system",
        "title": "Agent published",
        "content": "Support Assistant is now available to the team.",
        "level": "medium",
        "data": {"agent_id": "1c2f1a2e-0f4e-4a8f-9d4a-2b0d7b3dcb6d"},
        "link_url": "/app/apps/1c2f1a2e-0f4e-4a8f-9d4a-2b0d7b3dcb6d",
        "status": "active",
        "expires_at": null,
        "created_at": "2026-09-26T08:15:00Z",
        "updated_at": "2026-09-26T08:15:00Z",
        "is_read": false,
        "read_at": null,
        "deliveries": []
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  },
  "msg": "success"
}

The pagination envelope is data.items / data.total / data.page / data.page_size.


Get unread count

GET /api/v1/notifications/unread-count

Counts the caller's visible, unread notifications (same visibility and expiry rules).

200 OK:

{
  "code": 0,
  "data": {"total": 3},
  "msg": "success"
}

Mark as read

POST /api/v1/notifications/read

Marks notifications by ID, or marks every visible notification as read with mark_all.

Request body

FieldTypeRequiredDescription
notification_idsarray of string (UUID)NoNotifications to mark as read
mark_allbooleanNoMark every visible notification as read, default false

At least one of notification_ids or mark_all must be present, otherwise the request fails with 400 + 1002 (validation_error).

{
  "notification_ids": ["9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"],
  "mark_all": false
}

200 OK:

{
  "code": 0,
  "data": {"updated": 1},
  "msg": "Notification read status updated"
}

data.updated is the number of newly created read records; already-read notifications are not counted and are not an error.

Marking as read uses the visibility scope with include_expired=true: a notification that is now expired can still be marked read if it was visible. Read records are unique per notification and user, so repeated submissions are safe.


Browse notifications (admin)

GET /api/v1/admin/notifications

Query parameters

ParameterTypeRequiredDefaultDescription
scoperepeatable stringNo-Filter by one or more scopes
team_idstring (UUID)No-Filter by team
user_idstring (UUID)No-Filter by target user
typestringNo-Filter by notification type key
levelrepeatable stringNo-Filter by one or more levels
searchstringNo-Contains match on title, content, type
include_expiredbooleanNofalseInclude expired notifications
pageintegerNo1Page number, minimum 1
page_sizeintegerNo20Items per page, 1–100

Returns the same {items, total, page, page_size} envelope as the user route, but:

  • items[].is_read is always false and read_at always null (read state is per user and is not aggregated here);
  • items[].deliveries is populated with per-channel records.

Authorization rules:

CallerBehaviour
Global adminMay query any scope and omit team_id
Team admin (non-global)Must pass a team_id they own/administer as OWNER or ADMIN; including global in scope returns 403; omitting team_id returns 400

Errors:

HTTPCodeMeaning
4033001insufficient_privileges: a non-global admin requested global scope
4001002notification_scope_requires_team: a non-global admin omitted team_id
4033003team_admin_required: not an OWNER/ADMIN of that team
4044004team_not_found

Create and dispatch a notification

POST /api/v1/admin/notifications

Requires the admin:notification:create permission.

Request body

FieldTypeRequiredDescription
scopestringYesglobal, team, or user
team_idstring (UUID)ConditionalRequired when scope=team
user_idstring (UUID)ConditionalTarget user when scope=user
user_idsarray of string (UUID)ConditionalBatch-target multiple users when scope=user
typestringYesNotification type key, 1–100 characters
sourcestringNosystem, user, biz, default system
titlestringYesTitle, 1–255 characters
contentstringYesBody text
levelstringNolow, medium, high, default medium
dataobjectNoArbitrary payload
link_urlstringNoIn-app link
expires_atstring (ISO 8601)NoExpiry timestamp
notify_channelsarray of stringNoExternal delivery channels; nothing is delivered by default

Scope authorization:

scopeRequirement
globalSuperuser only, otherwise 403 + 3001
teamteam_id required (else 400 + 1002), and the caller must be an OWNER/ADMIN of that team
useruser_id or user_ids required (else 400 + 1002); any unknown user in user_ids returns 404 + 4001

Batch mode (user_ids) creates one notification per user and returns only the first notification object (msg is Notifications created successfully).

curl -X POST "https://your-domain.com/api/v1/admin/notifications" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "team",
    "team_id": "550e8400-e29b-41d4-a716-446655440000",
    "type": "maintenance_notice",
    "source": "system",
    "title": "Scheduled maintenance",
    "content": "The platform will be unavailable on Sunday 02:00-04:00 UTC.",
    "level": "high",
    "link_url": "/status",
    "notify_channels": ["email", "webhook"]
  }'

200 OK: data is the created notification object; deliveries contains the newly created delivery records (pending, sent asynchronously by a background task).

External channels must be configured first

Every channel in notify_channels is validated for enabled state and required configuration before creation; any failure returns 400: smtp_not_enabled / smtp_not_configured, dingtalk_not_enabled / dingtalk_not_configured, wechat_not_enabled / wechat_not_configured, feishu_not_enabled / feishu_not_configured, webhook_not_enabled / webhook_not_configured, slack_not_enabled / slack_not_configured. See the Settings API for channel configuration.


Delete notification

DELETE /api/v1/admin/notifications/{notification_id}

Requires the admin:notification:delete permission. An audit record is written before deletion.

Authorization: global and user scoped notifications can only be deleted by a global admin; team scoped notifications require the caller to be an OWNER/ADMIN of that team (the team comes from the notification itself, not from the request).

200 OK:

{
  "code": 0,
  "data": {"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"},
  "msg": "Notification deleted successfully"
}

Errors:

HTTPCodeMeaning
4044000notification_not_found
4033001insufficient_privileges: a non-global admin deleting a global / user notification
4001002notification_scope_requires_team: a team notification without a team_id
4033003team_admin_required

Error handling

HTTPCodeTrigger
4012000 / 2001 / 2002Missing, invalid, or expired token
4001002validation_error (read with no arguments), notification_scope_requires_team, or an unconfigured channel
4033000 / 3001Missing permission code, or scope beyond the caller's privileges
4033003Not an OWNER/ADMIN of the team
4044001 / 4004 / 4000User / team / notification not found
4221001Request validation failed: unknown scope/level, page < 1, page_size outside 1–100

How is this guide?

On this page