CorebanqCorebanq Developer Docs
Notificationsv1

Delete one notification

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.

DELETE
/v1/notifications/{notification_id}

Authorization

BearerAuth
AuthorizationBearer <token>

In: header

Path Parameters

notification_id*string

NOT the same id as on the other routes. DeleteNotification loads models.Notification, i.e. notifications.notifications — THE FAN-OUT SOURCE ROW — by primary key, while the list, the read-one and the mark-read all address notifications.user_notifications, the per-recipient copy. Feeding an id taken from GET /v1/notifications back into this route therefore finds no row and answers 500, not 404.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X DELETE "https://example.com/v1/notifications/497f6eca-6276-4993-bfeb-53cbbbba6f08"
{
  "message": "Notification deleted"
}
{
  "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"
}

GETNOT IMPLEMENTED — notification preferences (read)

STUB. The handler takes no request at all — its parameter is discarded — and unconditionally answers 200 {"message": "Not implemented"}. No preferences are stored or read anywhere. Do not build against this route; it cannot fail and it cannot return data.

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.