CorebanqCorebanq Developer Docs
Countriesv2

List countries (v2)

Returns countries in the standard paginated envelope through the shared query parser. Filterable and sortable fields: id, iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, order, iban_length, name, description, tags, created_at, created_by, modified_at, modified_by, active. Free-text search covers iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, name and description. Two fields are deliberately NOT listed. metadata is not in validFieldsV2 at all, so querying it answers 500 query_m.invalid_search_field — NOT a 400, and not query_m.invalid_field. validateFieldName builds MsgInvalidSearchField with no WithCode, and an empty Code sends HandleAppErrorWithCode to 500. Do not confuse it with query_m.invalid_field: that one has two unrelated sources, both answering 400 — validateFieldName raises it when the name fails the character regex, and the stack parser raises it for an unknown stack field. An unknown SORT field is query_m.invalid_sort_field, 400. icon_url IS in validFieldsV2 but has no column behind it (models.Country marks it gorm:"-"), so filtering or sorting on it builds SQL referencing a column that does not exist and returns 500 — a pre-existing defect in validFieldsV2, not something to rely on. Sorting on it is subject to the same data-pass caveat as any other sort key: see the sort parameter. NO RECORD-LEVEL PERMISSION FILTER APPLIES. The service passes UserID: uuid.Nil (its own comment calls this a guest route), and the shared getAll path short-circuits on uuid.Nil before the permitted-records lookup, merging a nil scope. So any caller with a valid token receives every row and unfiltered total/total_unfiltered counts, regardless of record grants. RbacRecordType is passed but unused on this path. The v1 list has no record scoping either.

GET
/v2/countries

Authorization

bearerAuth
AuthorizationBearer <token>

In: header

Query Parameters

limit?integer

Maximum records to return. This route does NOT go through the shared 100-item clamp: the handler calls query.GetLimitWithoutMaxFilter, which re-reads the raw parameter, so a large limit is honoured as given. 0 and negatives other than -1 fall back to 10; a non-numeric value is rejected with 400 common.invalid_input. -1 is the count-only mode — ShouldSkipDataFetching returns total and total_unfiltered with an empty data array.

offset?integer

Records to skip. A negative value is neither rejected nor honoured — parseLimitOffset returns 0 for it, so ?offset=-10 silently returns the first page.

sort?string

Sort expression. ParseSortFields splits on commas and understands ONLY an optional leading minus: sort=order ascending, sort=-order descending, sort=-order,name for two keys. There is no :asc / :desc suffix — the whole token is looked up in validFieldsV2, so sort=order:asc is not found and answers 400 query_m.invalid_sort_field. BUT THAT CHECK ONLY RUNS WHEN ROWS ARE FETCHED. getAllQueryStorage.applyParams sorts only on the data pass (isCount == false), and SrvGetAllCountriesV2 skips that pass entirely when the count is 0 or limit=-1. So ?sort=order:asc&limit=-1, and the same sort against a search that matches nothing, both answer 200 with the bad key silently accepted. stack behaves the same way. A 200 is not proof the sort key is valid.

filter?string

JSON-encoded filter criteria — ACCEPTED AND THEN IGNORED on this route. ParseQueryJSON never extracts a filter key into models.Parameters, and get_all_service assigns Filter from an input field SrvGetAllCountriesV2 never sets, so it stays nil. ?filter={"active":true} returns exactly the same unfiltered page as omitting it. The only real effect is the 400 when the value is not valid JSON. Use search. to filter.

search_text?string

Free-text search over the searchable fields — iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, name and description. Also accepted as search. Both forms are translated into _text.like; the explicit search._text. form described under search. reaches the same expression with a choice of operator.

stack?string

Group results by this field; data then comes back as an object keyed by the field value.

distinct?string

Return distinct values of this field.

search.<field>?string

Per-field filter with an optional operator suffix (.eq, .ne, .gte, .lte, .gt, .lt, .in, .nin, .like, .start_with, .end_with). An unrecognised FIELD is answered 500 query_m.invalid_search_field, not 400 — that branch of validateFieldName omits WithCode. A name that fails the character regex is a normal 400, query_m.invalid_field, and a bad VALUE on a recognised field is a normal 400, query_m.invalid_search_value. TWO SUFFIXES PARSE BUT DO NOT VALIDATE. ParseFieldAndOperator also recognises .ilike and .contains, so ?search.name.ilike=zurich splits cleanly into a field and an operator — and then validateOperator rejects it, because validOperators holds neither. The answer is 400 query_m.invalid_operator, a fifth query_m code. Use .like, which is synonymous. THE RESERVED _text FIELD. search._text is not a column: validFieldsV2 maps it to an expression concatenating iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, name and description, and ExtractTextSearch peels search._text.* off before ordinary field validation, so the 500 above does not apply to it. It takes the WIDER operator set — .like, .ilike, .contains, .eq, .start_with, .end_with — which is where .ilike and .contains are legal. A bare search._text means .like. Anything outside that set is 400 query_m.invalid_operator, and two _text operators in one query are 400 query_m.invalid_search_value.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v2/countries"
{
  "data": [
    {
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "iso_alpha_2": "CH",
      "iso_alpha_3": "CHE",
      "iso_numeric": "756",
      "icon": "string",
      "icon_url": "string",
      "emoji": "🇨🇭",
      "phone_prefix": "+41",
      "order": 999,
      "tags": [
        "string"
      ],
      "iban_length": 0,
      "name": "Switzerland",
      "description": "string",
      "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": {}
    }
  ],
  "total": 0,
  "total_unfiltered": 0,
  "metadata": {},
  "keys": [
    "string"
  ],
  "has_more": true
}
{
  "status": 404,
  "message": "Country not found",
  "code": "countries_m.country_not_found",
  "class": "business",
  "retryable": true,
  "details": [
    {
      "field": "string",
      "rule": "string",
      "param": "string",
      "message": "string"
    }
  ]
}
{
  "status": 404,
  "message": "Country not found",
  "code": "countries_m.country_not_found",
  "class": "business",
  "retryable": true,
  "details": [
    {
      "field": "string",
      "rule": "string",
      "param": "string",
      "message": "string"
    }
  ]
}
{
  "status": 404,
  "message": "Country not found",
  "code": "countries_m.country_not_found",
  "class": "business",
  "retryable": true,
  "details": [
    {
      "field": "string",
      "rule": "string",
      "param": "string",
      "message": "string"
    }
  ]
}
{
  "status": 429,
  "message": "rate limit exceeded",
  "code": "rate_limits_m.exceeded",
  "class": "temporary",
  "retryable": true
}
{
  "status": 404,
  "message": "Country not found",
  "code": "countries_m.country_not_found",
  "class": "business",
  "retryable": true,
  "details": [
    {
      "field": "string",
      "rule": "string",
      "param": "string",
      "message": "string"
    }
  ]
}

{
  "overall_status": "unhealthy",
  "message": "Service is shutting down",
  "timestamp": "2026-08-27T15:04:05.123456789+02:00"
}