Aggregate counts for companies in the caller's tray
Counts users, accounts and payments for each requested company. customer_ids IS EFFECTIVELY REQUIRED. It reads like an optional filter, but omitting it does NOT mean "every company in my tray": the authorisation step returns an empty set for an empty request, so the answer is 200 {"data": []}. Send the ids you want. IDS YOU MAY NOT SEE ARE DROPPED SILENTLY. The requested ids are intersected with the companies in the caller's own tray — entries whose type is exactly "company" — and anything not in that set is removed with no error and no marker. A response can therefore be shorter than the request, and the only way to tell which ids survived is to compare customer_id values. A per-company failure does NOT fail the request: that row comes back with status "error" and an opaque code while the others still carry metrics. Companies are counted concurrently, at most six at a time.
Authorization
bearerAuth In: header
Query Parameters
Comma-separated uuids. Whitespace around each is trimmed, empty members are skipped, and duplicates are collapsed. AT MOST 32 after de-duplication — more is 400 profile.portfolio_metrics_too_many_customers. A member that does not parse fails the whole request with 400 profile.portfolio_metrics_invalid_customer; there is no partial parse. Omitting the parameter entirely yields an empty data array, not every company.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/profile/portfolio-metrics"{
"data": [
{
"customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",
"status": "ok",
"metrics": {
"users": 0,
"accounts": 0,
"payments": 0
},
"error": {
"code": "profile.portfolio_metrics_customer_failed"
}
}
]
}{
"status": 400,
"message": "One or more customer IDs are invalid",
"code": "profile.portfolio_metrics_invalid_customer",
"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.7 to 6f1c9a52-3a5e-4f2d-9d61-7c0b0a4e5f88 exceeded.",
"code": "rate_limits_m.exceeded",
"class": "temporary",
"retryable": true
}{
"status": 500,
"message": "Internal server error",
"code": "common.server_error",
"class": "business"
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}Description
Previous Page
Reads profiles.v_userlinks filtered to the signed-in user. Returns a BARE ARRAY — there is no envelope, no total and no pagination, and no query parameter is read. Two fields are rewritten on the way out: a blank name becomes a label built from profile_m.new_company_text, and the type kyb_flow becomes profile_m.kyb_flow_text. Both are resolved for the request's Accept-Language, and in every shipped bundle both resolve to a frontend translation token rather than to end-user text — see the field descriptions. Always an array; [] when the user is linked to nothing.