Screening statistics
Aggregate counts over EVERY screening row in the tenant, not the caller's permitted subset. The handler resolves a user id and passes it down, but GetScreeningStats hands the repository only the date range — the id reaches no query and no RbacRecordType is set — so any caller with the endpoint grant sees the whole table's totals. GET /v1/kyc/screening is no better: it asks for CUSTOMER-record scoping through GetAllTotalInput and then applies the permitted customer ids to kyc.screening.id, so it answers with an empty page or an unscoped one. Neither route is a tenant boundary. THE DATE RANGE DOES NOT WORK. The handler runs the full shared query parser and hands the result to the service, which then looks for the keys date_from and date_to. The shared parser never emits those keys — its switch recognises limit, offset, sort, stack, distinct, filter, search, search_text and fill_gaps, plus any search.<field> key, and everything else hits default: return nil. So both are always nil and the statistics are ALWAYS COMPUTED OVER ALL TIME, whatever the caller sends. Every other query parameter is parsed and then discarded too: only the two date keys are read, and neither can arrive. DATE RANGE: THERE IS NO WAY TO ASK FOR ONE. GetScreeningStatsWithQuery reads queryMap["date_from"] and queryMap["date_to"] and passes the parsed pair to GetScreeningStats, so the service is built for a range — but the keys never arrive. parseStandardQueryParameter has cases for limit/offset, sort/stack/distinct/fill_gaps, search/search_text and filter, and everything else falls to default: return nil. ?date_from=2026-01-01 is therefore handled IDENTICALLY to ?banana=1: both are dropped, and both answer 200 with all-time totals. The two are deliberately NOT declared as parameters for that reason — declaring one would be a contract statement the code does not make, and a generated client would emit a dateFrom argument whose caller receives all-time figures rendered as a filtered range. Contrast offset, which IS declared here and on the list route: parseLimitOffset really parses it and ?offset=abc really is a 400 common.invalid_input, so it exists in the request contract even though FetchKYCScreenings never applies it. date_from has no such foothold.
Authorization
bearerAuth JWT token from the authentication endpoint.
In: header
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/kyc/screening/stats"{
"total_screenings": 0,
"total_matches": 0,
"match_rate": 0.1,
"by_entity_type": {
"property1": 0,
"property2": 0
},
"by_collection": {
"property1": 0,
"property2": 0
},
"by_status": {
"property1": 0,
"property2": 0
},
"recent_screenings": [
{}
]
}{
"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": 408,
"message": "Failed to search KYC screening records",
"code": "kyc_m.failed_to_search",
"class": "temporary",
"retryable": true
}{
"status": 429,
"message": "Rate limit for 203.0.113.7 to POST:/v1/kyc/screening exceeded.",
"code": "rate_limits_m.exceeded",
"class": "temporary",
"retryable": true
}{
"status": 500,
"message": "Failed to retrieve KYC screening record",
"code": "kyc_m.failed_to_get",
"class": "business"
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}Returns one stored screening, rendered as UniversalKYCResponse rather than as the stored row. The two query parameters below are accepted and parsed but DISCARDED before they can change the body: matches are always included, and the provider's raw response is never included, whatever is sent.
Lists stored screening rows through the shared GetAll pipeline, so the body is the standard models.GetAllResponseAPI envelope of KYCScreening rows — NOT the results/limit/offset object this spec used to document, and not UniversalKYCResponse. RECORD SCOPING ON THIS ROUTE IS NOT USABLE AS WRITTEN. The input sets RbacRecordType: constants.CustomerRecName, so GetPermittedRecordsCtx resolves the caller's permitted CUSTOMER ids — and MergeSearchParams injects them under constants.IDIn ("id.in"), a predicate on the queried model's own id column, here kyc.screening.id. A screening id is never a customer id, so the two possible outcomes are both wrong: a caller WITHOUT customer read-all gets a non-nil permitted list and therefore an EMPTY page, whatever screenings exist; a caller WITH read-all gets nil, MergeSearchParams returns the parameters untouched, and the page is UNSCOPED. There is no middle case in which a caller sees their own customers' screenings. Each row carries a full serialised UniversalKYCResponse in response — the module's own normalised body, not the provider's raw payload — with no projection, so pages can be large.