CorebanqCorebanq Developer Docs
Notificationsv1

Mark one notification read

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.

PATCH
/v1/notifications/{notification_id}/read

Authorization

BearerAuth
AuthorizationBearer <token>

In: header

Path Parameters

notification_id*string

Id of the notifications.user_notifications row — the per-recipient copy, not the fan-out source row's id.

Query Parameters

customer_id?string

Optional scope. 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.

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

The only field this route reads, and only its presence is read. An absent read makes the whole call a no-op that still answers 200 — see the operation's 200 description.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X PATCH "https://example.com/v1/notifications/497f6eca-6276-4993-bfeb-53cbbbba6f08/read" \  -H "Content-Type: application/json" \  -d '{}'
{
  "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": {}
}
{
  "status": 400,
  "message": "Invalid notification id",
  "code": "notifications_m.invalid_id",
  "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": "Database error",
  "code": "common.database_error",
  "class": "business"
}

{
  "overall_status": "unhealthy",
  "message": "Service is shutting down",
  "timestamp": "2026-08-27T15:04:05Z"
}

GETList 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.

POSTSend a notification straight to connected SSE clients (test route)

AUTHENTICATED BY X-API-Key, not by a bearer token — auth.APIKeyMiddleware replaces the usual chain, so none of the RBAC or licence refusals apply and no user context exists. Bypasses the queue and writes to connected clients directly. Recipients are every currently connected SSE client UNIONED with user_ids; supplying user_ids does not exclude connected users. When there are no connected clients AND no user_ids, the route answers 200 {"message": "No connected users to notify"} having done nothing. The body is not validated at all — no struct validator, no field checks.