CorebanqCorebanq Developer Docs
Personasv2Personal Identity

List personas (v2)

The shared list envelope — data, total, total_unfiltered, keys — with real filtering, sorting and pagination, which /v1/personas has none of. Filterable fields are id, created_at, created_by, modified_at, modified_by, active, metadata, first_name, last_name, date_of_birth, date_of_death, nationality and hash_id. Any other search.<field> is dropped by the shared parser rather than rejected, so the response comes back UNFILTERED instead of empty. search.linked_to.record_id and search.linked_to.record_type are the exception: they are not columns and would be dropped like any unknown key, so the handler lifts them out of the raw query string and puts them back into the search map by hand before the service runs. The .eq suffix form is accepted for both.

GET
/v2/personas

Authorization

bearerAuth
AuthorizationBearer <token>

In: header

Query Parameters

limit?integer

Default 10, over 100 clamps to 100, non-positive becomes 10. -1 is the platform's "totals only" sentinel and survives the query parser, but this route ignores it: the data query runs GetLimit, which maps every non-positive value to 10, so limit=-1 returns a normal page of 10.

offset?integer

Default 0. A negative offset is normalised to 0.

sort?string

Comma-separated sort fields. Prefix with '-' for descending, e.g. -created_at

stack?string

Optional stack field, e.g. active or created_at:month

search?string

JSON-encoded search object

search.id.eq?string

Filter by persona ID

search.created_at.eq?string

Filter by created_at (date-time)

search.created_by.eq?string

Filter by created_by

search.modified_at.eq?string

Filter by modified_at (date-time)

search.modified_by.eq?string

Filter by modified_by

search.active.eq?boolean

Filter by active flag

search.metadata.like?string

Search in metadata JSON as text

search.first_name.like?string

Filter by first name

search.last_name.like?string

Filter by last name

search.date_of_birth.eq?string

Filter by date of birth

search.date_of_death.eq?string

Filter by date of death

search.nationality.eq?string

Filter by nationality

search.hash_id.like?string

Filter by hash_id

search.linked_to.record_id?string

Filter personas linked to a specific record ID

search.linked_to.record_type?string

Filter personas linked to a specific record type (e.g. users, customers)

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v2/personas"
{
  "data": [
    {
      "first_name": "string",
      "last_name": "string",
      "date_of_birth": "2019-08-24",
      "date_of_death": "2019-08-24",
      "nationality": "string",
      "hash_id": "string",
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "active": true,
      "metadata": {},
      "created_at": "2019-08-24T14:15:22Z",
      "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
      "modified_at": "2019-08-24T14:15:22Z",
      "modified_by": "e8d4374d-93a1-4e98-a6c6-fdcf00c5059f",
      "linked_to": [
        {
          "record_type": "string",
          "record_id": "8bf519b6-a3e0-49d2-8e42-039542d9a489"
        }
      ]
    }
  ],
  "total": 0,
  "total_unfiltered": 0,
  "keys": [
    "string"
  ],
  "has_more": true
}
{
  "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 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"
}

PUTUpdate user's persona

Updates the persona linked to a user. Carries every caveat of PUT /v1/personas/{persona_id} — no validation, and Updates(struct) skips zero values so active: false and empty strings do nothing — plus two of its own. THE LINK IS RESOLVED FROM THE CALLER'S OWN RECORD GRANTS, NOT FROM THE PATH USER'S LINKS. The handler calls links.FetchLinksByType with the AUTHENTICATED CALLER's id and canReadAll hardcoded to false, so it sees only links touching a user record the caller — or one of the caller's roles — holds a row for in rbac.record_permissions. rbac.RecPermission, which guards the route, returns true for an Administrator without consulting that table at all, so an Administrator passes the permission check and then finds no links: the caller must hold an explicit record grant on the user in the path for this route to resolve anything. IT ONLY WORKS WHEN EXACTLY ONE LINK TOUCHES THAT USER. The ids collected from those links are bound to "id = ?", which GORM expands as a parenthesised list: one id gives id = ($1) and updates that row; none gives id = (NULL), which matches nothing; two or more gives id = ($1,$2), which PostgreSQL rejects as a comparison against a row constructor. Every failure lands in the same branch and answers 404 having written nothing. The collected ids are not filtered by record type either — the customer-to-user and signatory-to-user links other modules create count toward the same total, and a single one of them can be handed to First() in place of the persona, which then finds no persona row and also answers 404.

GETGet persona (v2)

Same persona payload as v1 plus linked_to, the list of records this persona is linked to as {record_type, record_id} pairs. Unlike the v1 equivalents, the refusals here come from the v2 service and use the shared codes: common.forbidden for 403 and common.record_not_found for 404, rather than v1's common.rbac_no_rec_access and bare NotFound404.