diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53f9ab9..831591f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([#225](https://github.com/Open-EO/openeo-python-client/issues/225), [openeo-api#410](https://github.com/Open-EO/openeo-api/issues/410))
 - Add `DataCube.dimension_labels()` (EP-4008)
 - Add `Connection.load_result()` (EP-4008)
+- Add proper support for child callbacks in `fit_curve` and `predict_curve` ([#229](https://github.com/Open-EO/openeo-python-client/issues/229))
 
 
 ### Changed
diff --git a/openeo/rest/datacube.py b/openeo/rest/datacube.py
index 213923d..1468349 100644
--- a/openeo/rest/datacube.py
+++ b/openeo/rest/datacube.py
@@ -785,7 +785,7 @@ class DataCube(ImageCollection, _FromNodeMixin):
         as `apply`, `apply_dimension`, `reduce`, ....)
 
         :param process: process id string, PGNode or callable that uses the ProcessBuilder mechanism to build a process
-        :parameter parameter_mapping: mapping of child (callback) parameters names to parent process parameter names
+        :param parent_parameters: list of parameter names defined for child process
         :return:
         """
 
@@ -895,7 +895,7 @@ class DataCube(ImageCollection, _FromNodeMixin):
         Add a reduce process with given reducer callback along given dimension
 
         :param dimension: the label of the dimension to reduce
-        :param reducer: a callback function that creates a process graph, see :ref:`callbackfunctions`
+        :param reducer: "child callback" function, see :ref:`callbackfunctions`
         """
         # TODO: check if dimension is valid according to metadata? #116
         # TODO: #125 use/test case for `reduce_dimension_binary`?
@@ -1680,8 +1680,7 @@ class DataCube(ImageCollection, _FromNodeMixin):
             arguments["options"] = options
         return self.process(process_id="sar_backscatter", arguments=arguments)
 
-
-    def fit_curve(self, parameters, function, dimension ):
+    def fit_curve(self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str):
         """
         EXPERIMENTAL: https://github.com/Open-EO/openeo-processes/pull/240
         Use non-linear least squares to fit a model function `y = f(x, parameters)` to data.
@@ -1689,32 +1688,34 @@ class DataCube(ImageCollection, _FromNodeMixin):
         The process throws an `InvalidValues` exception if invalid values are encountered.
         Invalid values are finite numbers (see also ``is_valid()``).
 
-        @param parameters:
-        @param function:
-        @param dimension:
-        @return:
+        :param parameters:
+        :param function: "child callback" function, see :ref:`callbackfunctions`
+        :dimension:
         """
         return self.process(process_id="fit_curve", arguments={
             "data": THIS,
             "parameters": parameters,
-            "function": function,
+            "function": self._get_callback(function, parent_parameters=["x", "parameters"]),
             "dimension": dimension
         })
 
-    def predict_curve(self, parameters, function, dimension, labels = None ):
+    def predict_curve(
+            self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str,
+            labels=None
+    ):
         """
         EXPERIMENTAL: https://github.com/Open-EO/openeo-processes/pull/240
         Predict values using a model function and pre-computed parameters.
 
-        @param parameters:
-        @param function:
-        @param dimension:
+        :param parameters:
+        :param function: "child callback" function, see :ref:`callbackfunctions`
+        :dimension:
         @return:
         """
         return self.process(process_id="predict_curve", arguments={
             "data": THIS,
             "parameters": parameters,
-            "function": function,
+            "function": self._get_callback(function, parent_parameters=["x", "parameters"]),
             "dimension": dimension,
             "labels": labels
         })

diff --git a/tests/rest/datacube/test_datacube100.py b/tests/rest/datacube/test_datacube100.py
index 77cc452..68da969 100644
--- a/tests/rest/datacube/test_datacube100.py
+++ b/tests/rest/datacube/test_datacube100.py
@@ -1174,3 +1174,100 @@ def test_dimension_labels_invalid(con100):
     # Don't validate when no metadata
     cube = con100.load_collection("S2", fetch_metadata=False).dimension_labels("unv6lidd")
     assert cube.flat_graph()["dimensionlabels1"]["arguments"]["dimension"] == "unv6lidd"
+
+
+def test_fit_curve_callback(con100: Connection):
+    from openeo.processes import array_element
+    def model(x, parameters):
+        return array_element(parameters, 0) + array_element(parameters, 1) * x
+
+    img = con100.load_collection("S2")
+    res = img.fit_curve(parameters=[0, 0], function=model, dimension="t")
+    expected = {
+        'loadcollection1': {
+            'process_id': 'load_collection',
+            'arguments': {'id': 'S2', 'spatial_extent': None, 'temporal_extent': None},
+        },
+        'fitcurve1': {
+            'process_id': 'fit_curve',
+            'arguments': {
+                'data': {'from_node': 'loadcollection1'},
+                'parameters': [0, 0],
+                'function': {
+                    'process_graph': {
+                        'arrayelement1': {
+                            'process_id': 'array_element',
+                            'arguments': {'data': {'from_parameter': 'parameters'}, 'index': 0},
+                        },
+                        'arrayelement2': {
+                            'process_id': 'array_element',
+                            'arguments': {'data': {'from_parameter': 'parameters'}, 'index': 1},
+                        },
+                        'multiply1': {
+                            'process_id': 'multiply',
+                            'arguments': {'x': {'from_node': 'arrayelement2'}, 'y': {'from_parameter': 'x'}},
+                        },
+                        'add1': {
+                            'process_id': 'add',
+                            'arguments': {'x': {'from_node': 'arrayelement1'}, 'y': {'from_node': 'multiply1'}},
+                            'result': True
+                        },
+                    }
+                },
+                'dimension': 't',
+            },
+            'result': True
+        },
+    }
+    assert res.graph == expected
+
+
+def test_predict_curve_callback(con100: Connection):
+    from openeo.processes import array_element, cos
+    def model(x, parameters):
+        return array_element(parameters, 0) * cos(array_element(parameters, 1) * x)
+
+    img = con100.load_collection("S2")
+    res = img.predict_curve(parameters=[0, 0], function=model, dimension="t")
+    expected = {
+        'loadcollection1': {
+            'process_id': 'load_collection',
+            'arguments': {'id': 'S2', 'spatial_extent': None, 'temporal_extent': None},
+        },
+        'predictcurve1': {
+            'process_id': 'predict_curve',
+            'arguments': {
+                'data': {'from_node': 'loadcollection1'},
+                'parameters': [0, 0],
+                'function': {
+                    'process_graph': {
+                        'arrayelement1': {
+                            'process_id': 'array_element',
+                            'arguments': {'data': {'from_parameter': 'parameters'}, 'index': 0},
+                        },
+                        'arrayelement2': {
+                            'process_id': 'array_element',
+                            'arguments': {'data': {'from_parameter': 'parameters'}, 'index': 1},
+                        },
+                        'multiply1': {
+                            'process_id': 'multiply',
+                            'arguments': {'x': {'from_node': 'arrayelement2'}, 'y': {'from_parameter': 'x'}},
+                        },
+                        'cos1': {
+                            'process_id': 'cos',
+                            'arguments': {'x': {'from_node': "multiply1"}},
+                        },
+                        'multiply2': {
+                            'process_id': 'multiply',
+                            'arguments': {'x': {'from_node': 'arrayelement1'}, 'y': {'from_node': 'cos1'}},
+                            'result': True
+                        },
+                    }
+                },
+                'dimension': 't',
+                'labels': None,
+            },
+            'result': True
+        },
+    }
+    assert res.graph == expected

Proper callable support in fit_curve and related
from https://github.com/openEOPlatform/documentation/issues/10

```python
curve_fitting = l2a_bands.fit_curve(
    parameters=[1,1,1], # Initial guess of the parameters
    dimension="t",      # Fit the function along the temporal dimension
    function=l2a_bands._get_callback(fit_function, parent_parameters=["data","parameters"])
)
```

it's unfortunate that you have to call a private method `l2a_bands._get_callback` like that, we should make sure that you can just do `function=fit_function` directly
tests/rest/datacube/test_datacube100.py::test_fit_curve_callback tests/rest/datacube/test_datacube100.py::test_predict_curve_callback
tests/rest/datacube/test_datacube100.py::test_datacube_graph tests/rest/datacube/test_datacube100.py::test_datacube_flat_graph tests/rest/datacube/test_datacube100.py::test_datacube_legacy_flatten tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs0-expected0] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs1-expected1] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs2-expected2] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs3-expected3] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs4-expected4] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs5-expected5] tests/rest/datacube/test_datacube100.py::test_filter_bbox_kwargs[kwargs6-expected6] tests/rest/datacube/test_datacube100.py::test_filter_bbox_parameter tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args0-expected0] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args1-expected1] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args2-expected2] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args3-expected3] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args4-expected4] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args5-expected5] tests/rest/datacube/test_datacube100.py::test_filter_bbox_positional_args[args6-expected6] tests/rest/datacube/test_datacube100.py::test_filter_bbox_legacy_positional_args tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs[args0-kwargs0-expected0] tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs[args1-kwargs1-expected1] tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs[args2-kwargs2-expected2] tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs[args3-kwargs3-expected3] tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs[args4-kwargs4-expected4] tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs_conflict[args0-kwargs0-Don't tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs_conflict[args1-kwargs1-Don't tests/rest/datacube/test_datacube100.py::test_filter_bbox_args_and_kwargs_conflict[args2-kwargs2-Don't tests/rest/datacube/test_datacube100.py::test_filter_spatial tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_basic tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_types[polygon0-expected_geometries0] tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_types[polygon1-expected_geometries1] tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_types[polygon2-expected_geometries2] tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_types[polygon3-expected_geometries3] tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_types[polygon4-expected_geometries4] tests/rest/datacube/test_datacube100.py::test_aggregate_spatial_with_crs tests/rest/datacube/test_datacube100.py::test_aggregate_temporal tests/rest/datacube/test_datacube100.py::test_mask_polygon_basic tests/rest/datacube/test_datacube100.py::test_mask_polygon_types[polygon0-expected_mask0] tests/rest/datacube/test_datacube100.py::test_mask_polygon_types[polygon1-expected_mask1] tests/rest/datacube/test_datacube100.py::test_mask_polygon_types[polygon2-expected_mask2] tests/rest/datacube/test_datacube100.py::test_mask_polygon_types[polygon3-expected_mask3] tests/rest/datacube/test_datacube100.py::test_mask_polygon_types[polygon4-expected_mask4] tests/rest/datacube/test_datacube100.py::test_mask_polygon_with_crs tests/rest/datacube/test_datacube100.py::test_mask_polygon_parameter tests/rest/datacube/test_datacube100.py::test_mask_polygon_path tests/rest/datacube/test_datacube100.py::test_mask_raster tests/rest/datacube/test_datacube100.py::test_merge_cubes tests/rest/datacube/test_datacube100.py::test_resample_spatial tests/rest/datacube/test_datacube100.py::test_ndvi_simple tests/rest/datacube/test_datacube100.py::test_ndvi_args tests/rest/datacube/test_datacube100.py::test_rename_dimension tests/rest/datacube/test_datacube100.py::test_reduce_dimension tests/rest/datacube/test_datacube100.py::test_reduce_dimension_binary tests/rest/datacube/test_datacube100.py::test_reduce_dimension_name tests/rest/datacube/test_datacube100.py::test_metadata_load_collection_100 tests/rest/datacube/test_datacube100.py::test_apply_absolute_pgnode tests/rest/datacube/test_datacube100.py::test_load_collection_properties tests/rest/datacube/test_datacube100.py::test_load_collection_properties_process_builder_function tests/rest/datacube/test_datacube100.py::test_load_collection_temporalextent_process_builder_function tests/rest/datacube/test_datacube100.py::test_apply_dimension_temporal_cumsum_with_target tests/rest/datacube/test_datacube100.py::test_apply_dimension_modify_bands tests/rest/datacube/test_datacube100.py::test_apply_neighborhood_udf tests/rest/datacube/test_datacube100.py::test_filter_spatial_callback tests/rest/datacube/test_datacube100.py::test_custom_process_kwargs_datacube tests/rest/datacube/test_datacube100.py::test_custom_process_kwargs_datacube_pg tests/rest/datacube/test_datacube100.py::test_custom_process_kwargs_this tests/rest/datacube/test_datacube100.py::test_custom_process_kwargs_namespaced tests/rest/datacube/test_datacube100.py::test_custom_process_arguments_datacube tests/rest/datacube/test_datacube100.py::test_custom_process_arguments_datacube_pg tests/rest/datacube/test_datacube100.py::test_custom_process_arguments_this tests/rest/datacube/test_datacube100.py::test_custom_process_arguments_namespacd tests/rest/datacube/test_datacube100.py::test_save_user_defined_process tests/rest/datacube/test_datacube100.py::test_save_user_defined_process_public tests/rest/datacube/test_datacube100.py::test_save_result_format tests/rest/datacube/test_datacube100.py::test_to_json tests/rest/datacube/test_datacube100.py::test_to_json_compact tests/rest/datacube/test_datacube100.py::test_sar_backscatter_defaults tests/rest/datacube/test_datacube100.py::test_sar_backscatter_custom tests/rest/datacube/test_datacube100.py::test_sar_backscatter_coefficient_none tests/rest/datacube/test_datacube100.py::test_sar_backscatter_coefficient_invalid tests/rest/datacube/test_datacube100.py::test_datacube_from_process tests/rest/datacube/test_datacube100.py::test_datacube_from_process_namespace tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_datacube_from_flat_graph_minimal tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_datacube_from_json_minimal_string tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_datacube_from_json_minimal_file[str] tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_datacube_from_json_minimal_file[Path] tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_datacube_from_json_minimal_http tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_process_dict_wrapper tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_minimal tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_cube tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_udp tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_parameter_again tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_no_params tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_missing_params tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_default[kwargs0-100] tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_default[kwargs1-100] tests/rest/datacube/test_datacube100.py::TestDataCubeFromFlatGraph::test_parameter_substitution_default[kwargs2-86] tests/rest/datacube/test_datacube100.py::test_send_nan_json tests/rest/datacube/test_datacube100.py::test_dimension_labels tests/rest/datacube/test_datacube100.py::test_dimension_labels_invalid