List notifications
The shared list envelope, scoped through GetAllTotalInput with RbacRecordType = the notification record type. THAT RBAC SCOPE IS THE ONLY ONE — THESE ARE NOT "THE CALLER'S OWN ROWS". Neither the handler nor GetTotalNotifications/FetchNotifications adds a user_id predicate, so what a caller sees is decided entirely by GetPermittedRecords: a caller holding read_all on the notification record type gets EVERY user's notifications, and any other caller gets the rows they hold a read grant on, whoever they belong to. Pass search.user_id to narrow the list to one user. Filterable fields are id, customer_id, user_id, event, title, content, rec_id, rec_type, created_at, sent_at, read and read_at. title AND content DO NOT HOLD WHAT THE USER SEES. Both filter against the stored columns, and CreateUserNotifications never assigns Title at all, so user_notifications.title is empty on every row and search.title can never match anything. content is only the caller-supplied pre-rendered text, so it is empty for key-based notifications. The keys live in title_key and content_key, which are hidden from JSON and are not filterable. The title and content in the RESPONSE are rendered per request, after the filter has already run, and on this route they come from the event push template with the row metadata whenever one resolves — replacing the stored content. See the UserNotification schema: the three routes that return it resolve these two fields three different ways. ANY OTHER search.<field> IS REFUSED, AND REFUSED AS A 500: query.validateFieldName builds query_m.invalid_search_field without a status, and an empty Code sends HandleAppErrorWithCode to InternalServerError500. It is a caller error wearing a server error's status. sort is validated too, but on the fetch pass only — applyParams skips sorting when counting. So an unknown sort field is 400 query_m.invalid_sort_field when the filter matched at least one row, and is never noticed at all when the count came back zero or limit=-1 skipped the fetch: those answer 200 with an empty page.
Authorization
BearerAuth In: header
Query Parameters
NOT A SCOPE. GetNotificationsSrv uses it for one thing only — picking the language for the rendered title and content, through GetUserPreferredLanguage. It filters nothing; use search.customer_id for that. Omitted, it stays the nil uuid and is passed through as such. A value that does not parse is refused 400 — but with common.invalid_input, not the notifications_m.invalid_id the service builds: the handler discards that AppError and calls BadRequest400 with no argument.
Default 10, over 100 clamps to 100, zero or negative becomes 10, -1 is the skip-data-fetching sentinel. Non-integer is 400.
Default 0; negative is clamped to 0.
GROUPS THE PAGE AND CHANGES THE SHAPE OF data FROM AN ARRAY TO AN OBJECT keyed by the formatted value of the named field, with keys listing those group keys in order. Accepts the same field names as the filter list, optionally prefixed with - for descending and suffixed with [format]. A field outside the list is 400 query_m.invalid_field and an unterminated bracket is 400 query_m.invalid_format — but, like sort, only once rows have been fetched: a zero-row result returns the empty stacked shape without parsing the value at all.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/notifications"{
"data": [
{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"title": "string",
"content": "string",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",
"event": "string",
"rec_id": "0ba687c4-349e-490d-969f-fd374e232d42",
"rec_type": "string",
"created_at": "2019-08-24T14:15:22Z",
"sent_at": "2019-08-24T14:15:22Z",
"read": true,
"read_at": "2019-08-24T14:15:22Z",
"system_sse_delivered_at": "2019-08-24T14:15:22Z",
"channels_attempted": [
"string"
],
"channels_sent": [
"string"
],
"channels_delivered": [
"string"
],
"channels_failed": [
"string"
],
"link_url": "string",
"link_text": "string",
"created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
"modified_at": "2019-08-24T14:15:22Z",
"modified_by": "e8d4374d-93a1-4e98-a6c6-fdcf00c5059f",
"active": true,
"metadata": {}
}
],
"total": 0,
"total_unfiltered": 0,
"has_more": true,
"keys": [
"string"
],
"metadata": {}
}{
"status": 400,
"message": "Invalid input",
"code": "common.invalid_input",
"class": "validation"
}{
"status": 401,
"message": "Unauthorized",
"code": "common.unauthorized",
"class": "business"
}{
"status": 403,
"message": "No access to the record",
"code": "common.rbac_no_rec_access",
"class": "business"
}{
"status": 429,
"message": "Rate limit for 203.0.113.10 to 8f1d2c34-5b6e-4a70-9c81-2d3e4f5a6b7c exceeded.",
"code": "rate_limits_m.exceeded",
"class": "temporary",
"retryable": true
}{
"status": 500,
"message": "Invalid search field: status. Valid fields are: content, created_at, customer_id, event, id, read, read_at, rec_id, rec_type, sent_at, title, user_id",
"code": "query_m.invalid_search_field",
"class": "business"
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}READ THE notification_id NOTE BELOW: this route addresses A DIFFERENT TABLE from every other route in this document. The caller's user id is NOT a scope. It is only written into deleted_by; the lookup is by primary key alone, so any caller the endpoint grant admits can soft-delete any notification whose id they know. customer_id is not read either, unlike the read and mark-read routes. The delete is logical: deleted, deleted_at and deleted_by are set and the row is saved. Answers 200 with a {"message": "Notification deleted"} map, NOT 204 and NOT the StdResponse envelope. The message is a fixed English string with no i18n key.
MARKS READ ONLY, AND read IS A PRESENCE FLAG RATHER THAN A VALUE. Whatever the field carries, true or false, the row is written SET read = true, read_at = now(): the service only nil-checks patch.Read and never passes it down, and the storage statement hard-codes the column. There is no way to mark a notification unread through this API — no other route clears the flag. AN ABSENT read FIELD IS A NO-OP THAT STILL ANSWERS 200 null. The service returns (nil, nil) when the decoded patch has no read member, and the handler serialises that nil pointer, so the body is the four bytes null and nothing was written. {} and {"read": null} both do this. Send true; sending false does the same thing.