diff --git a/backend/src/baserow/contrib/database/api/rows/fields.py b/backend/src/baserow/contrib/database/api/rows/fields.py
new file mode 100644
index 000000000..840ebfd0c
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api/rows/fields.py
@@ -0,0 +1,8 @@
+from rest_framework import serializers
+
+from baserow.contrib.database.api.utils import extract_user_field_names_from_params
+
+
+class UserFieldNamesField(serializers.BooleanField):
+    def to_internal_value(self, data):
+        return extract_user_field_names_from_params({"user_field_names": data})
diff --git a/backend/src/baserow/contrib/database/api/rows/serializers.py b/backend/src/baserow/contrib/database/api/rows/serializers.py
index 9c3109227..8a2136847 100644
--- a/backend/src/baserow/contrib/database/api/rows/serializers.py
+++ b/backend/src/baserow/contrib/database/api/rows/serializers.py
@@ -9,6 +9,7 @@ from rest_framework import serializers
 
 from baserow.api.search.serializers import SearchQueryParamSerializer
 from baserow.api.utils import get_serializer_class
+from baserow.contrib.database.api.rows.fields import UserFieldNamesField
 from baserow.contrib.database.fields.registries import field_type_registry
 from baserow.contrib.database.rows.models import RowHistory
 from baserow.contrib.database.rows.registries import row_metadata_registry
@@ -241,8 +242,9 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False)
     optional_user_field_names_info = ""
     if user_field_names:
         optional_user_field_names_info = (
-            " If the GET parameter `user_field_names` is provided then the key will "
-            "instead be the actual name of the field."
+            " If the GET parameter user_field_names is provided and its value is "
+            "one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty, "
+            "then the key will instead be the actual name of the field."
         )
 
     for i, field_type in enumerate(field_types):
@@ -334,6 +336,12 @@ def remap_serialized_row_to_user_field_names(
     return new_row
 
 
+class UserFieldNamesSerializer(serializers.Serializer):
+    user_field_names = UserFieldNamesField(
+        required=False, default=False, allow_null=True
+    )
+
+
 class MoveRowQueryParamsSerializer(serializers.Serializer):
     before_id = serializers.IntegerField(required=False)
 
@@ -346,8 +354,9 @@ class BatchCreateRowsQueryParamsSerializer(serializers.Serializer):
     before = serializers.IntegerField(required=False)
 
 
-class ListRowsQueryParamsSerializer(SearchQueryParamSerializer):
-    user_field_names = serializers.BooleanField(required=False, default=False)
+class ListRowsQueryParamsSerializer(
+    SearchQueryParamSerializer, UserFieldNamesSerializer
+):
     order_by = serializers.CharField(required=False)
     include = serializers.CharField(required=False)
     exclude = serializers.CharField(required=False)
@@ -395,8 +404,9 @@ def get_example_batch_rows_serializer_class(example_type="get", user_field_names
     return class_object
 
 
-class GetRowAdjacentSerializer(SearchQueryParamSerializer, serializers.Serializer):
-    user_field_names = serializers.BooleanField(required=False, default=False)
+class GetRowAdjacentSerializer(
+    SearchQueryParamSerializer, UserFieldNamesSerializer, serializers.Serializer
+):
     previous = serializers.BooleanField(required=False, default=False)
     view_id = serializers.IntegerField(required=False)
 
diff --git a/backend/src/baserow/contrib/database/api/rows/views.py b/backend/src/baserow/contrib/database/api/rows/views.py
index 3d817171c..8690d1578 100644
--- a/backend/src/baserow/contrib/database/api/rows/views.py
+++ b/backend/src/baserow/contrib/database/api/rows/views.py
@@ -47,7 +47,10 @@ from baserow.contrib.database.api.rows.serializers import GetRowAdjacentSerializ
 from baserow.contrib.database.api.tables.errors import ERROR_TABLE_DOES_NOT_EXIST
 from baserow.contrib.database.api.tokens.authentications import TokenAuthentication
 from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE
-from baserow.contrib.database.api.utils import get_include_exclude_fields
+from baserow.contrib.database.api.utils import (
+    extract_user_field_names_from_params,
+    get_include_exclude_fields,
+)
 from baserow.contrib.database.api.views.errors import (
     ERROR_VIEW_DOES_NOT_EXIST,
     ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST,
@@ -259,9 +262,11 @@ class RowsView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided the returned json "
-                    "will use the user specified field names instead of internal "
-                    "Baserow field names (field_123 etc). "
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause the returned JSON to use the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             OpenApiParameter(
@@ -418,9 +423,11 @@ class RowsView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided this endpoint will "
-                    "expect and return the user specified field names instead of "
-                    "internal Baserow field names (field_123 etc)."
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause this endpoint to expect and return the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             CLIENT_SESSION_ID_SCHEMA_PARAMETER,
@@ -490,7 +497,8 @@ class RowsView(APIView):
             context=table,
         )
 
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
+
         model = table.get_model()
 
         validation_serializer = get_row_serializer_class(
@@ -658,9 +666,11 @@ class RowView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided the returned json "
-                    "will use the user specified field names instead of internal "
-                    "Baserow field names (field_123 etc). "
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause the returned JSON to use the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
         ],
@@ -707,7 +717,7 @@ class RowView(APIView):
         table = TableHandler().get_table(table_id)
 
         TokenHandler().check_table_permissions(request, "read", table, False)
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
         model = table.get_model()
         row = RowHandler().get_row(request.user, table, row_id, model)
         serializer_class = get_row_serializer_class(
@@ -736,9 +746,11 @@ class RowView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided this endpoint will "
-                    "expect and return the user specified field names instead of "
-                    "internal Baserow field names (field_123 etc)."
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause this endpoint to expect and return the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             CLIENT_SESSION_ID_SCHEMA_PARAMETER,
@@ -802,7 +814,7 @@ class RowView(APIView):
         table = TableHandler().get_table(table_id)
         TokenHandler().check_table_permissions(request, "update", table, False)
 
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
         field_ids, field_names = None, None
 
         if user_field_names:
@@ -923,9 +935,11 @@ class RowMoveView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided the returned json "
-                    "will use the user specified field names instead of internal "
-                    "Baserow field names (field_123 etc). "
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause the returned JSON to use the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             CLIENT_SESSION_ID_SCHEMA_PARAMETER,
@@ -967,7 +981,7 @@ class RowMoveView(APIView):
 
         TokenHandler().check_table_permissions(request, "update", table, False)
 
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
 
         model = table.get_model()
 
@@ -1015,9 +1029,11 @@ class BatchRowsView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided this endpoint will "
-                    "expect and return the user specified field names instead of "
-                    "internal Baserow field names (field_123 etc)."
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause this endpoint to expect and return the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             CLIENT_SESSION_ID_SCHEMA_PARAMETER,
@@ -1083,7 +1099,7 @@ class BatchRowsView(APIView):
         TokenHandler().check_table_permissions(request, "create", table, False)
         model = table.get_model()
 
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
         before_id = query_params.get("before")
         before_row = (
             RowHandler().get_row(request.user, table, before_id, model)
@@ -1130,9 +1146,11 @@ class BatchRowsView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided this endpoint will "
-                    "expect and return the user specified field names instead of "
-                    "internal Baserow field names (field_123 etc)."
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause this endpoint to expect and return the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             CLIENT_SESSION_ID_SCHEMA_PARAMETER,
@@ -1198,7 +1216,7 @@ class BatchRowsView(APIView):
         TokenHandler().check_table_permissions(request, "update", table, False)
         model = table.get_model()
 
-        user_field_names = "user_field_names" in request.GET
+        user_field_names = extract_user_field_names_from_params(request.GET)
 
         row_validation_serializer = get_row_serializer_class(
             model,
@@ -1326,9 +1344,11 @@ class RowAdjacentView(APIView):
                 location=OpenApiParameter.QUERY,
                 type=OpenApiTypes.BOOL,
                 description=(
-                    "A flag query parameter which if provided the returned json "
-                    "will use the user specified field names instead of internal "
-                    "Baserow field names (field_123 etc). "
+                    "A flag query parameter that, if provided with one of the "
+                    "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an "
+                    "empty value, will cause the returned JSON to use the "
+                    "user-specified field names instead of the internal Baserow "
+                    "field names (e.g., field_123)."
                 ),
             ),
             OpenApiParameter(
diff --git a/backend/src/baserow/contrib/database/api/utils.py b/backend/src/baserow/contrib/database/api/utils.py
index 387b98acb..21823e564 100644
--- a/backend/src/baserow/contrib/database/api/utils.py
+++ b/backend/src/baserow/contrib/database/api/utils.py
@@ -1,3 +1,4 @@
+from baserow.config.settings.utils import str_to_bool
 from baserow.contrib.database.fields.models import Field
 from baserow.contrib.database.fields.utils import get_field_id_from_field_key
 from baserow.core.utils import split_comma_separated_string
@@ -115,3 +116,20 @@ def extract_field_ids_from_string(value):
 
     ids = [get_field_id_from_field_key(v, False) for v in value.split(",")]
     return [_id for _id in ids if _id is not None]
+
+
+def extract_user_field_names_from_params(query_params):
+    """
+    Extracts the user_field_names parameter from the query_params and returns
+    boolean value
+    """
+
+    value = query_params.get("user_field_names", False)
+
+    if value is False:
+        return False
+
+    if value is None or value == "":
+        return True
+
+    return str_to_bool(value)
diff --git a/backend/tests/baserow/contrib/database/rows/test_rows_handler.py b/backend/tests/baserow/contrib/database/rows/test_rows_handler.py
index 7404946e5..b671dc078 100644
--- a/backend/tests/baserow/contrib/database/rows/test_rows_handler.py
+++ b/backend/tests/baserow/contrib/database/rows/test_rows_handler.py
@@ -11,6 +11,7 @@ from pyinstrument import Profiler
 
 from baserow.contrib.database.api.utils import (
     extract_field_ids_from_string,
+    extract_user_field_names_from_params,
     get_include_exclude_fields,
 )
 from baserow.contrib.database.rows.exceptions import RowDoesNotExist
@@ -39,6 +40,14 @@ def test_extract_field_ids_from_string():
     assert extract_field_ids_from_string("is,1,one") == [1]
 
 
+def test_extract_user_field_names_from_params():
+    assert extract_user_field_names_from_params({}) is False
+    assert extract_user_field_names_from_params({"user_field_names": None}) is True
+    assert extract_user_field_names_from_params({"user_field_names": ""}) is True
+    assert extract_user_field_names_from_params({"user_field_names": "true"}) is True
+    assert extract_user_field_names_from_params({"user_field_names": "false"}) is False
+
+
 @pytest.mark.django_db
 def test_get_include_exclude_fields(data_fixture):
     table = data_fixture.create_database_table()
diff --git a/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json b/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json
new file mode 100644
index 000000000..c055e430d
--- /dev/null
+++ b/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json
@@ -0,0 +1,7 @@
+{
+    "type": "bug",
+    "message": "Properly handle user_field_names in api calls",
+    "issue_number": 2784,
+    "bullet_points": [],
+    "created_at": "2024-07-11"
+}
\ No newline at end of file
diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json
index 16badc309..e6e02ceae 100644
--- a/web-frontend/locales/en.json
+++ b/web-frontend/locales/en.json
@@ -408,7 +408,7 @@
     "queryParameters": "Query parameters",
     "pathParameters": "Path parameters",
     "requestBodySchema": "Request body schema",
-    "userFieldNamesDescription": "When any value is provided for the `user_field_names` GET param then field names returned by this endpoint will be the actual names of the fields.\n\n If the `user_field_names` GET param is not provided, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.",
+    "userFieldNamesDescription": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.",
     "singleRow": "Single",
     "batchRows": "Batch",
     "fileUploads": "File uploads"
diff --git a/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue b/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue
index 5ab3d289e..6d527f3f5 100644
--- a/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue
+++ b/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue
@@ -23,7 +23,10 @@
       </h4>
       <ul class="api-docs__parameters">
         <APIDocsParameter name="user_field_names" :optional="true" type="any">
-          <MarkdownIt :content="$t('apiDocs.userFieldNamesDescription')" />
+          <MarkdownIt
+            class="api-docs__content"
+            :content="$t('apiDocs.userFieldNamesDescription')"
+          />
         </APIDocsParameter>
       </ul>
     </div>
diff --git a/web-frontend/modules/database/locales/en.json b/web-frontend/modules/database/locales/en.json
index fc68b658a..07ffcbe55 100644
--- a/web-frontend/modules/database/locales/en.json
+++ b/web-frontend/modules/database/locales/en.json
@@ -141,7 +141,7 @@
     "description": "To list rows in the *{name}* table a `GET` request has to be made to the *{name}* endpoint. The response is paginated and by default the first page is returned. The correct page can be fetched by providing the `page` and `size` query parameters.",
     "page": "Defines which page of rows should be returned.",
     "size": "Defines how many rows should be returned per page.",
-    "userFieldNames": "When any value is provided for the `user_field_names` GET param then field names returned by this endpoint will be the actual names of the fields.\n\n If the `user_field_names` GET param is not provided, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.\n\n Additionally when `user_field_names` is set then the behaviour of the other GET parameters `order_by`, `include` and `exclude` changes. They instead expect comma separated lists of the actual field names instead.",
+    "userFieldNames": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.\n\n Additionally when `user_field_names` is set then the behaviour of the other GET parameters `order_by`, `include` and `exclude` changes. They instead expect comma separated lists of the actual field names instead.",
     "search": "If provided only rows with data that matches the search query are going to be returned.",
     "orderBy": "Optionally the rows can be ordered by fields separated by comma. By default or if prepended with a '+' a field is ordered in ascending (A-Z) order, but by prepending the field with a '-' it can be ordered descending (Z-A).\n\n #### With `user_field_names`:\n\n `order_by` should be a comma separated list of the field names to order by. For example if you provide the following GET parameter `order_by=My Field,-My Field 2` the rows will ordered by the field called `My Field` in ascending order. If some fields have the same value, that subset will be ordered by the field called `My Field 2` in descending order.\n\n Ensure fields with names starting with a `+` or `-` are explicitly prepended with another `+` or `-`. E.g `+-Name`.\n\n The name of fields containing commas must be surrounded by quotes: `\"Name ,\"`. If the field names contain quotes, then they must be escaped using the `\\` character. Eg: `Name \\\"`. \n\n#### Without `user_field_names`:\n\n `order_by` should be a comma separated list of `field_` followed by the id of the field to order by. For example if you provide the following GET parameter `order_by=field_1,-field_2` the rows will ordered by `field_1` in ascending order. If some fields have the same value, that subset will be ordered by `field_2` in descending order.",
     "filters": "Rows can optionally be filtered using the same view filters that are available for the views. This parameter accepts a JSON serialized string containing the filter tree to apply to this view. The filter tree is a nested structure containing the filters that need to be applied. \n\n#### With `user_field_names`:\n\nAn example of a valid filter tree is the following: `{\"filter_type\": \"AND\", \"filters\": [{\"field\": \"Name\", \"type\": \"equal\", \"value\": \"test\"}]}`.\n\n#### Without `user_field_names`:\n\nFor example, if you optionally provide the following GET parameter: `{\"filter_type\": \"AND\", \"filters\": [{\"field\": 1, \"type\": \"equal\", \"value\": \"test\"}]}`\n\nPlease note that if this parameter is provided, all other `filter__{field}__{filter}` will be ignored, as well as the filter_type parameter.",