Get FX convert defaults
Operator-set default currency pair for FX conversion, read from app_config module `fx` (`convert.default_base` / `convert.default_target`) with YAML config fallback. Intended for customer frontends seeding a default FX pair. Only these two whitelisted paths are exposed; an unset default is returned as null, not as an error.
Authorization
bearerAuth In: header
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/fx/convert/defaults"{
"base": {
"code": "EUR",
"precision": 2
},
"target": {
"code": "CHF",
"precision": 2
}
}{
"status": 409,
"message": "Spread rule overlaps an existing rule",
"code": "fx_m.spread_overlap",
"class": "business",
"retryable": false,
"details": [
{
"field": "percent_fee",
"rule": "lte",
"param": "1",
"message": "string"
}
]
}{
"status": 409,
"message": "Spread rule overlaps an existing rule",
"code": "fx_m.spread_overlap",
"class": "business",
"retryable": false,
"details": [
{
"field": "percent_fee",
"rule": "lte",
"param": "1",
"message": "string"
}
]
}{
"status": 429,
"message": "Rate limit for 203.0.113.7 to GET:/v1/fx/rates exceeded.",
"code": "rate_limits_m.exceeded",
"class": "temporary",
"retryable": true
}{
"status": 409,
"message": "Spread rule overlaps an existing rule",
"code": "fx_m.spread_overlap",
"class": "business",
"retryable": false,
"details": [
{
"field": "percent_fee",
"rule": "lte",
"param": "1",
"message": "string"
}
]
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}Creates a fee range. Answers 200 with the created row, not 201. Amounts are INTEGER MINOR units; percent_fee is a decimal rate between 0 and 1 and IS validated, so 1.5 is rejected with 400.
Lists stored rate rows. This route does NOT use the shared query parser and does NOT use the shared list envelope — it has its own parseQueryParams and its own RatesResponse. Pagination is silently clamped rather than validated: limit defaults to 10, is capped at 100, and a non-numeric or non-positive value is IGNORED (the default is used) instead of answering 400. offset defaults to 0 and a negative or non-numeric value is likewise ignored. Results are limited to the currencies the caller is permitted to read; when none are permitted the reply is an empty data set with total 0, not a 403. rate is a quoted decimal STRING in this response — see BaseCurrencyPair. SEARCH FILTERS. Every search.* parameter below is one filter on one column, and the list is the whole set the route accepts. Append an operator to compare instead of match — search.{field}.{op}, op one of eq, ne, gt, gte, lt, lte, in, nin, like, start_with, end_with; with no suffix the operator is eq. in and nin take a comma-separated list. like, start_with and end_with are case-insensitive ILIKE. search.{field}.eq=null and search.{field}.ne=null test for NULL; any other operator with an empty value is 400 query.operator_requires_value. A STRING VALUE IS SILENTLY REWRITTEN BEFORE IT REACHES THE QUERY. sanitizeStringValue drops every character that is not a letter, a digit, or one of . / @ - _ and space, and it never reports an error — so search.provider=A*B filters on AB and answers 200, not 400. A value that parses as a timestamp is passed through instead of stripped. Which rejections are 400 and which are 500 is set out under the 400 and 500 responses; the split is not the one a caller would expect. A DATE-TYPED FILTER IS TRUNCATED TO THE DAY. Where the route's field map types a column as date, normalizeSearchValueByFieldType rewrites the value as YYYY-MM-DD at midnight UTC before it reaches SQL, so a time component on such a filter is discarded without a word. Those parameters carry format: date below; the format: date-time ones are compared as sent.