CorebanqCorebanq Developer Docs
FXv1

Convert an amount for a customer

Converts an amount for a customer, applying that customer's tariff, fee range and spread. The money fields in the response are MAJOR units, and fee_range is the major-unit shape (FxTariffFeeRangeMajor) so the breakdown matches total_fee. /v2/fx/rates/convert carries the stored minor-unit row instead — the two surfaces deliberately differ.

GET
/v1/fx/rates/convert/{customer_id}

Authorization

bearerAuth
AuthorizationBearer <token>

In: header

Path Parameters

customer_id*string

Customer whose tariff and spreads apply.

Query Parameters

amount*number

Amount to convert, in MAJOR units of base.

base*string

Source currency. REQUIRED on this route — there is NO AppConfig fallback here. applyDefaultCurrency is called only from resolveConvertV2Defaults, i.e. the v2 handler; the v1 handler reads the values raw and getAndValidateParams rejects an empty base, target or type with 400 common.invalid_input. GET /v1/fx/rates/convert/{customer_id}?amount=100 is a 400, not a conversion at the configured default pair.

target*string

Destination currency. REQUIRED on this route — there is NO AppConfig fallback here. applyDefaultCurrency is called only from resolveConvertV2Defaults, i.e. the v2 handler; the v1 handler reads the values raw and getAndValidateParams rejects an empty base, target or type with 400 common.invalid_input. GET /v1/fx/rates/convert/{customer_id}?amount=100 is a 400, not a conversion at the configured default pair.

date?string

Rate date; the latest available rate is used when omitted.

type*string

Conversion type. REQUIRED on this route. Accepted values are BUY, SELL and MID (models.ASK, models.BID and models.MID); the same set as CurrencyPair.type. getAndValidateParams rejects an empty value with 400 common.invalid_input.

indicative?string

Ask for an indicative rate. Compared against the LITERAL STRING "true": the handler evaluates r.URL.Query().Get("indicative") == "true", so "1", "TRUE", "yes" and an empty value all read as false, silently. It is not a boolean and no parse error is possible.

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/fx/rates/convert/497f6eca-6276-4993-bfeb-53cbbbba6f08?amount=0.1&base=string&target=string&type=BUY"
{
  "currency": "string",
  "amount": 0.1,
  "total_fee": 0.1,
  "fee_range": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "min_range": 0.1,
    "max_range": 0.1,
    "fixed_fee": 0.1,
    "percent_fee": 0.1,
    "min_fee": 0.1,
    "max_fee": 0.1,
    "method": "fixed",
    "base_currency": "string",
    "target_currency": "string",
    "fx_tariff_id": "f12d958c-c80c-4752-862c-9e0c09506488",
    "date_start": "2019-08-24T14:15:22Z",
    "date_end": "2019-08-24T14:15:22Z",
    "active": true
  },
  "tariff": {
    "name": "string",
    "description": "string",
    "fallback_tariff_id": "e4b6bec8-3c7c-45ce-b8c3-85abac4ed74b",
    "is_default": true,
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "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",
    "active": true,
    "metadata": {}
  },
  "currency_pair": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "base_currency": "string",
    "target_currency": "string",
    "base_ccy_precision": 0,
    "target_ccy_precision": 0,
    "rate": 0.1,
    "type": "BUY",
    "date": "string",
    "source": "string",
    "rate_at": "string",
    "imported_at": "2019-08-24T14:15:22Z"
  },
  "mid_rate": 0.1,
  "spread_rule_id": "28d9bf19-dcef-4542-aa5e-139adf85f95e",
  "applied_rule_ids": [
    "c3f728eb-7f89-44c5-8a0d-db30827f9b2e"
  ],
  "fallback_tariff_used": true,
  "fallback_tariff_id": "e4b6bec8-3c7c-45ce-b8c3-85abac4ed74b"
}
{
  "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": 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"
}

GETRetrieve exchange rates

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.

GETGet available rate sources

Retrieve a distinct list of all rate sources available in the system