List available products for a customer
Returns products available to a customer from active matrix rules. Requires JWT and **RBAC read** on `customer_id`. Unlike pre-flight, this route reads `customer_id` from the body only — there is no URL fallback at all. ProductListRequest.CustomerID is `validate:"required"`, so a missing or zero uuid is caught inside DecodeJSONInput and the handler re-stamps it `400 common.failed_to_serialize` carrying `details[0] = {field: customer_id, rule: required}`. The handler's own `common.invalid_input` branch for the zero uuid is therefore unreachable.
Authorization
BearerAuth HTTP bearer JWT from the Corebanq auth service.
Authorization is enforced with RBAC (record-level permissions), not ad-hoc OAuth-style scopes in this spec. See each tag description and operation summaries for which record grants apply.
In: header
Header Parameters
BCP 47 language tag. Selects locale for product name/description. Falls back to the tenant's default locale when absent or unsupported.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/products/list" \ -H "Content-Type: application/json" \ -d '{ "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }'{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"customer_type": "SMALL_BUSINESS",
"products": [
{
"id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"code": "OWT",
"name": "Transfer",
"description": "Send funds to external bank accounts via wire transfer.",
"type": "transfers",
"route": "payment"
},
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"code": "OFR",
"name": "Convert (Off-Ramp)",
"description": "Convert crypto assets to fiat currency.",
"type": "transfers",
"route": "convert"
},
{
"id": "c9e66790-7425-40de-944b-e07fc1f90000",
"code": "CRD",
"name": "Crypto Receive (Deposit)",
"description": "Deposit cryptocurrency to your account on the platform.",
"type": "transfers",
"route": "deposit"
}
]
}{
"status": 400,
"code": "validation_error",
"message": "At least one of product_ids, source_account_id, destination_account_id, or destination_counterparty_id is required.",
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
],
"class": "validation",
"retryable": false
}{
"status": 400,
"code": "validation_error",
"message": "At least one of product_ids, source_account_id, destination_account_id, or destination_counterparty_id is required.",
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
],
"class": "validation",
"retryable": false
}{
"status": 400,
"code": "validation_error",
"message": "At least one of product_ids, source_account_id, destination_account_id, or destination_counterparty_id is required.",
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
],
"class": "validation",
"retryable": false
}{
"status": 400,
"code": "validation_error",
"message": "At least one of product_ids, source_account_id, destination_account_id, or destination_counterparty_id is required.",
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
],
"class": "validation",
"retryable": false
}{
"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"
}{
"status": 503,
"message": "Internal server error",
"code": "auth_m.internal_server_error",
"class": "temporary",
"retryable": true
}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.
Given a partial transfer context for a **target customer** (`customer_id`), returns eligible active products, active counterparty-shaped source rows, active counterparty-shaped destination rows, viable active routes, and optional limits/fees/FX. The target customer itself is represented as a counterparty row, matching the counterparties domain model. Selectable nested payment-source rows expose `cp_account_type` (`account`, `bank_account`, `crypto_wallet`), `cp_account_id`, and `products_allowed`. Inactive catalogue products, inactive matrix rules, inactive transaction channels, inactive source accounts, inactive destination counterparties, inactive bank accounts, and inactive crypto wallets are not returned. Modern finalize clients build selectors from the parent counterparty row `id` plus the chosen nested `cp_account_id`, then send them as `source.counterparty_id` / `source.cp_account_id` and `destination.counterparty_id` / `destination.cp_account_id`. The optional `finalize_recipient` hint is legacy convenience data for older recipient-shaped finalize clients. When a product is pinned via a single effective `product_ids[]` selection and enough route context is provided, `transfer_params` includes the allow/deny verdict, normalized amount, calculated fees, advisory FX, velocity hints, and optional quote state. Requires JWT and **RBAC read** on that customer. **`customer_id` must be in the BODY.** The resolver prefers a `customer_id` URL path parameter and only then falls back to the body — but this route is registered as `/v1/products/pre-flight` with no path segment, so `chi.URLParam` always returns empty and the URL branch is unreachable. A body without a non-nil, non-zero `customer_id` is refused `400 common.invalid_input` with `field: customer_id`. **A `422` here is usually a tenant configuration problem, not a bad request** — see that response.