diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2562f6c2..abcfeb3a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,7 +12,7 @@ cmake_minimum_required( VERSION 3.12 FATAL_ERROR )
 
 find_package( ecbuild 3.7.2 REQUIRED HINTS ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../ecbuild)
 
-project( metkit LANGUAGES CXX )
+project( metkit LANGUAGES CXX C )
 
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
diff --git a/src/metkit/CMakeLists.txt b/src/metkit/CMakeLists.txt
index 49a3426b..b079cf83 100644
--- a/src/metkit/CMakeLists.txt
+++ b/src/metkit/CMakeLists.txt
@@ -4,7 +4,6 @@ ecbuild_generate_config_headers( DESTINATION ${INSTALL_INCLUDE_DIR}/metkit )
 
 configure_file( metkit_config.h.in    metkit_config.h )
 configure_file( metkit_version.h.in   metkit_version.h )
-configure_file( metkit_version.cc.in  metkit_version.cc )
 
 install(FILES
             ${CMAKE_CURRENT_BINARY_DIR}/metkit_config.h
@@ -15,7 +14,7 @@ install(FILES
 ### metkit sources
 
 list( APPEND metkit_srcs
-    ${CMAKE_CURRENT_BINARY_DIR}/metkit_version.cc
+    metkit_version.c
     config/LibMetkit.cc
     config/LibMetkit.h
     mars/BaseProtocol.cc
@@ -100,6 +99,8 @@ list( APPEND metkit_srcs
     hypercube/HyperCube.cc
     hypercube/HyperCube.h
     hypercube/HyperCubePayloaded.h
+    api/metkit_c.cc 
+    api/metkit_c.h
 )
 
 list( APPEND metkit_persistent_srcs
@@ -198,6 +199,7 @@ ecbuild_add_library(
     PUBLIC_INCLUDES
        $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>
+       $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> # for metkit_version.h
 
     PRIVATE_LIBS
         "${odc_libs}"
diff --git a/src/metkit/api/metkit_c.cc b/src/metkit/api/metkit_c.cc
new file mode 100644
index 00000000..32701f40
--- /dev/null
+++ b/src/metkit/api/metkit_c.cc
@@ -0,0 +1,346 @@
+#include "metkit_c.h"
+#include "metkit/mars/MarsExpension.h"
+#include "metkit/mars/MarsRequest.h"
+#include "metkit/metkit_version.h"
+#include "eckit/runtime/Main.h"
+#include <functional>
+
+// ---------------------------------------------------------------------------------------------------------------------
+
+struct metkit_marsrequest_t : public metkit::mars::MarsRequest {
+    using metkit::mars::MarsRequest::MarsRequest;
+
+    metkit_marsrequest_t(metkit::mars::MarsRequest&& req) :
+        metkit::mars::MarsRequest(std::move(req)) {}
+};
+
+struct metkit_requestiterator_t {
+    explicit metkit_requestiterator_t(std::vector<metkit::mars::MarsRequest>&& vec) :
+        vector_(std::move(vec)), current_(vector_.begin()) {}
+
+    metkit_iterator_status_t next() {
+        if (first_) {
+            first_ = false;
+
+            if (current_ != vector_.end()) {
+                return METKIT_ITERATOR_SUCCESS;
+            }
+        }
+
+        if (current_ == vector_.end()) {
+            return METKIT_ITERATOR_COMPLETE;
+        }
+
+        ++current_;
+        return current_ == vector_.end() ? METKIT_ITERATOR_COMPLETE : METKIT_ITERATOR_SUCCESS;
+    }
+
+    // Note: expected to call next() before calling current()
+    metkit_iterator_status_t current(metkit_marsrequest_t* out) {
+        if (first_ || current_ == vector_.end()) {
+            return METKIT_ITERATOR_ERROR;
+        }
+
+        *out = std::move(*current_);
+        return METKIT_ITERATOR_SUCCESS;
+    }
+
+private:
+    bool first_ = true;
+    std::vector<metkit::mars::MarsRequest> vector_;
+    std::vector<metkit::mars::MarsRequest>::iterator current_;
+};
+
+// ---------------------------------------------------------------------------------------------------------------------
+/// @note: Unlike metkit_requestiterator_t, lifetime of the char* returned from this iterator are have the same as the iterator's lifetime.
+struct metkit_paramiterator_t {
+    metkit_paramiterator_t(std::vector<std::string> vec) :
+        vector_(vec), current_(vector_.begin()) {}
+
+    metkit_iterator_status_t next() {
+        if (first_) {
+            first_ = false;
+
+            if (current_ != vector_.end()) {
+                return METKIT_ITERATOR_SUCCESS;
+            }
+        }
+
+        if (current_ == vector_.end()) {
+            return METKIT_ITERATOR_COMPLETE;
+        }
+
+        ++current_;
+        return current_ == vector_.end() ? METKIT_ITERATOR_COMPLETE : METKIT_ITERATOR_SUCCESS;
+    }
+
+    // Note: expected to call next() before calling current()
+    metkit_iterator_status_t current(const char** out) const {
+        if (first_ || current_ == vector_.end()) {
+            return METKIT_ITERATOR_ERROR;
+        }
+
+        *out = current_->c_str();
+        return METKIT_ITERATOR_SUCCESS;
+    }
+
+    bool first_ = true;
+    std::vector<std::string> vector_;
+    std::vector<std::string>::const_iterator current_;
+};
+
+// ---------------------------------------------------------------------------------------------------------------------
+//                           ERROR HANDLING
+
+static thread_local std::string g_current_error_string;
+
+const char* metkit_get_error_string(metkit_error_t err) {
+    switch (err) {
+        case METKIT_SUCCESS:
+            return "Success";
+        case METKIT_ERROR:
+        case METKIT_ERROR_USER:
+        case METKIT_ERROR_ASSERT:
+            return g_current_error_string.c_str();
+        default:
+            return "<unknown>";
+    }
+}
+
+metkit_error_t innerWrapFn(std::function<metkit_error_t()> f) {
+    return f();
+}
+
+metkit_error_t innerWrapFn(std::function<void()> f) {
+    f();
+    return METKIT_SUCCESS;
+}
+
+template <typename FN>
+[[nodiscard]] metkit_error_t tryCatch(FN&& fn) {
+    try {
+        return innerWrapFn(std::forward<FN>(fn));
+    }
+    catch (const eckit::UserError& e) {
+        g_current_error_string = e.what();
+        return METKIT_ERROR_USER;
+    }
+    catch (const eckit::AssertionFailed& e) {
+        g_current_error_string = e.what();
+        return METKIT_ERROR_ASSERT;
+    }
+    catch (const eckit::Exception& e) {
+        g_current_error_string = e.what();
+        return METKIT_ERROR;
+    }
+    catch (const std::exception& e) {
+        g_current_error_string = e.what();
+        return METKIT_ERROR_UNKNOWN;
+    }
+    catch (...) {
+        return METKIT_ERROR_UNKNOWN;
+    }
+}
+
+// -----------------------------------------------------------------------------
+//                           HELPERS
+// -----------------------------------------------------------------------------
+
+metkit_error_t metkit_initialise() {
+    return tryCatch([] {
+        static bool initialised = false;
+
+        if (initialised) {
+            eckit::Log::warning()
+                << "Initialising Metkit library twice" << std::endl;
+        }
+
+        if (!initialised) {
+            const char* argv[2] = {"metkit-api", nullptr};
+            eckit::Main::initialise(1, const_cast<char**>(argv));
+            initialised = true;
+        }
+    });
+}
+
+// -----------------------------------------------------------------------------
+//                           PARSING
+// -----------------------------------------------------------------------------
+
+metkit_error_t metkit_parse_marsrequests(const char* str, metkit_requestiterator_t** requests, bool strict) {
+    return tryCatch([requests, str, strict] {
+        ASSERT(requests);
+        ASSERT(str);
+        std::istringstream in(str);
+        *requests = new metkit_requestiterator_t(metkit::mars::MarsRequest::parse(in, strict));
+    });
+}
+
+metkit_error_t metkit_parse_marsrequest(const char* str, metkit_marsrequest_t* request, bool strict) {
+    return tryCatch([request, str, strict] {
+        ASSERT(request);
+        ASSERT(str);
+        *request = metkit::mars::MarsRequest::parse(str, strict);
+    });
+}
+
+// -----------------------------------------------------------------------------
+//                           REQUEST
+// -----------------------------------------------------------------------------
+
+metkit_error_t metkit_marsrequest_new(metkit_marsrequest_t** request) {
+    return tryCatch([request] {
+        ASSERT(request);
+        *request = new metkit_marsrequest_t();
+    });
+}
+
+metkit_error_t metkit_marsrequest_delete(const metkit_marsrequest_t* request) {
+    return tryCatch([request] {
+        delete request;
+    });
+}   
+
+metkit_error_t metkit_marsrequest_set(metkit_marsrequest_t* request, const char* param, const char* values[], int numValues) {
+    return tryCatch([request, param, values, numValues] {
+        ASSERT(request);
+        ASSERT(param);
+        ASSERT(values);
+        std::string param_str(param);
+        std::vector<std::string> values_vec;
+        values_vec.reserve(numValues);
+        std::copy(values, values + numValues, std::back_inserter(values_vec));
+
+        request->values(param_str, values_vec);
+    });
+}
+    
+metkit_error_t metkit_marsrequest_set_one(metkit_marsrequest_t* request, const char* param, const char* value) {
+    return metkit_marsrequest_set(request, param, &value, 1);
+}
+
+metkit_error_t metkit_marsrequest_set_verb(metkit_marsrequest_t* request, const char* verb) {
+    return tryCatch([request, verb] {
+        ASSERT(request);
+        ASSERT(verb);
+        request->verb(verb);
+    });
+}
+
+metkit_error_t metkit_marsrequest_verb(const metkit_marsrequest_t* request, const char** verb) {
+    return tryCatch([request, verb] {
+        ASSERT(request);
+        ASSERT(verb);
+        *verb = request->verb().c_str();
+    });
+}
+
+metkit_error_t metkit_marsrequest_has_param(const metkit_marsrequest_t* request, const char* param, bool* has) {
+    return tryCatch([request, param, has] {
+        ASSERT(request);
+        ASSERT(param);
+        ASSERT(has);
+        *has = request->has(param);
+    });
+}
+
+metkit_error_t metkit_marsrequest_params(const metkit_marsrequest_t* request, metkit_paramiterator_t** params) {
+    return tryCatch([request, params] {
+        ASSERT(request);
+        ASSERT(params);
+        *params = new metkit_paramiterator_t(request->params());
+    });
+}
+
+metkit_error_t metkit_marsrequest_count_values(const metkit_marsrequest_t* request, const char* param, size_t* count) {
+    return tryCatch([request, param, count] {
+        ASSERT(request);
+        ASSERT(param);
+        ASSERT(count);
+        *count = request->countValues(param);   
+    });
+}
+
+metkit_error_t metkit_marsrequest_value(const metkit_marsrequest_t* request, const char* param, int index, const char** value) {
+    return tryCatch([request, param, index, value] {
+        ASSERT(request);
+        ASSERT(param);
+        ASSERT(value);
+        *value = request->values(param, false)[index].c_str();
+    });
+}
+metkit_error_t metkit_marsrequest_expand(const metkit_marsrequest_t* request, bool inherit, bool strict, metkit_marsrequest_t* expandedRequest) {
+    return tryCatch([request, expandedRequest, inherit, strict] {
+        ASSERT(request);
+        ASSERT(expandedRequest);
+        ASSERT(expandedRequest->empty());
+        metkit::mars::MarsExpension expand(inherit, strict);
+        *expandedRequest = expand.expand(*request);
+    });
+}
+
+metkit_error_t metkit_marsrequest_merge(metkit_marsrequest_t* request, const metkit_marsrequest_t* otherRequest) {
+    return tryCatch([request, otherRequest] {
+        ASSERT(request);
+        ASSERT(otherRequest);
+        request->merge(*otherRequest);
+    });
+}
+
+// -----------------------------------------------------------------------------
+//                           REQUEST ITERATOR
+// -----------------------------------------------------------------------------
+
+metkit_error_t metkit_delete_requestiterator(const metkit_requestiterator_t* it) {
+    return tryCatch([it] {
+        delete it;
+    });
+}
+
+metkit_iterator_status_t metkit_requestiterator_next(metkit_requestiterator_t* it) {
+    if (!it) {
+        return METKIT_ITERATOR_ERROR;
+    }
+    
+    return it->next();
+}
+
+metkit_iterator_status_t metkit_requestiterator_current(metkit_requestiterator_t* it, metkit_marsrequest_t* request) {
+    if (!it || !request || !request->empty()) {
+        return METKIT_ITERATOR_ERROR;
+    }
+
+    return it->current(request);
+}
+
+// -----------------------------------------------------------------------------
+//                           PARAM ITERATOR
+// -----------------------------------------------------------------------------
+
+metkit_error_t metkit_paramiterator_delete(const metkit_paramiterator_t* it) {
+    return tryCatch([it] {
+        delete it;
+    });
+}
+
+metkit_iterator_status_t metkit_paramiterator_next(metkit_paramiterator_t* it) {
+    if (!it) {
+        return METKIT_ITERATOR_ERROR;
+    }
+    return it->next();
+}
+
+metkit_iterator_status_t metkit_paramiterator_current(const metkit_paramiterator_t* it, const char** param) {
+    if (!it || !param) {
+        return METKIT_ITERATOR_ERROR;
+    }
+    return it->current(param);
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Bridge between C and C++
+const metkit::mars::MarsRequest& metkit::mars::MarsRequest::fromOpaque(const metkit_marsrequest_t* request) {
+    return *static_cast<const metkit::mars::MarsRequest*>(request);
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
diff --git a/src/metkit/api/metkit_c.h b/src/metkit/api/metkit_c.h
new file mode 100644
index 00000000..014bdaa6
--- /dev/null
+++ b/src/metkit/api/metkit_c.h
@@ -0,0 +1,254 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ---------------------------------------------------------------------------------------------------------------------
+ * TYPES
+ * -----*/
+
+struct metkit_marsrequest_t;
+typedef struct metkit_marsrequest_t metkit_marsrequest_t;
+
+struct metkit_requestiterator_t;
+/** RequestIterator for iterating over vector of Request instances */
+typedef struct metkit_requestiterator_t metkit_requestiterator_t;
+
+struct metkit_paramiterator_t;
+/** Iterator for iterating over parameters in Request */
+typedef struct metkit_paramiterator_t metkit_paramiterator_t;
+
+/* ---------------------------------------------------------------------------------------------------------------------
+ * ERROR HANDLING
+ * -------------- */
+
+typedef enum metkit_error_values_t
+{
+    METKIT_SUCCESS            = 0, /* Operation succeded. */
+    METKIT_ERROR              = 1, /* Operation failed. */
+    METKIT_ERROR_UNKNOWN      = 2, /* Failed with an unknown error. */
+    METKIT_ERROR_USER         = 3, /* Failed with an user error. */
+    METKIT_ERROR_ASSERT       = 4  /* Failed with an assert() */
+} metkit_error_t;
+
+typedef enum metkit_iterator_status_t
+{
+    METKIT_ITERATOR_SUCCESS  = 0, /* Operation succeded. */
+    METKIT_ITERATOR_COMPLETE = 1, /* All elements have been returned */
+    METKIT_ITERATOR_ERROR    = 2  /* Operation failed. */
+} metkit_iterator_status_t;
+
+
+const char* metkit_get_error_string(enum metkit_error_values_t err);
+
+/* -----------------------------------------------------------------------------
+ * HELPERS
+ * ------- */
+
+/**
+ * @brief Get metkit version.
+ *
+ * @return const char* version string
+ */
+const char* metkit_version();
+
+/**
+ * @brief Get metkit git sha1 version.
+ *
+ * @return const char* git sha1 version string
+ */
+const char* metkit_git_sha1();
+
+/**
+ * @brief Initialise Main() context.
+ *
+ * @note This is ONLY required when Main() is NOT initialised, such as loading
+ * the MetKit as shared library in Python.
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_initialise();
+
+/* ---------------------------------------------------------------------------------------------------------------------
+ * PARSING
+ * --- */
+
+/**
+ * Parse MARS requests into RequestIterator of Request instances. Resulting RequestIterator
+ * must be deallocated with metkit_delete_requestiterator
+ * @param str MARS requests
+ * @param[out] requests Allocates RequestIterator object
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_parse_marsrequests(const char* str, metkit_requestiterator_t** requests, bool strict);
+
+/**
+ * Parse MARS request into Request instance
+ * @note will error if request expands to multiple requests: use metkit_parse_marsrequests instead
+ * @param str MARS request
+ * @param[out] request Request instance
+ * @param strict if true, raise error rather than warning on invalid values
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_parse_marsrequest(const char* str, metkit_marsrequest_t* request, bool strict);
+/* ---------------------------------------------------------------------------------------------------------------------
+ * REQUEST
+ * --- */
+
+/** Allocates new Request object. Must be deallocated with mekit_delete_request
+ * @param[out] request new Request instance
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_new(metkit_marsrequest_t** request);
+
+/** Deallocates Request object and associated resources.
+ * @param request Request instance
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_delete(const metkit_marsrequest_t* request);
+
+/** Add parameter and values to request
+ * @param request Request instance
+ * @param param parameter name
+ * @param values array of values for parameter
+ * @param numValues number of values
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_set(metkit_marsrequest_t* request, const char* param, const char* values[], int numValues);
+
+/** Add parameter and values to request
+ * @param request Request instance
+ * @param param parameter name
+ * @param values value to add
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_set_one(metkit_marsrequest_t* request, const char* param, const char* value);
+
+/** Set verb in Request object
+ * @param request Request instance
+ * @param verb verb to set
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_set_verb(metkit_marsrequest_t* request, const char* verb);
+
+/** Returns the verb in Request object
+ * @param request Request instance
+ * @param[out] verb verb in request
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_verb(const metkit_marsrequest_t* request, const char** verb);
+
+/** Returns whether parameter is in Request object
+ * @param request Request instance
+ * @param param parameter name
+ * @param[out] has whether parameter exists in request
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_has_param(const metkit_marsrequest_t* request, const char* param, bool* has);
+
+
+/** Returns parameter iterator for Request object
+ * Must be deallocated with metkit_paramiterator_delete
+ * @note: The strings obtained from next() are owned by the iterator and should be copied if they need to outlive it.
+ * @param request Request instance
+ * @param[out] params parameter iterator
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_params(const metkit_marsrequest_t* request, metkit_paramiterator_t** params);
+
+
+/** Returns number of values for specific parameter in Request object
+ * @param request Request instance
+ * @param param parameter name in request
+ * @param[out] count number of values for param in request
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_count_values(const metkit_marsrequest_t* request, const char* param, size_t* count);
+
+/** Returns value for specific parameter and index in Request object
+ * @param request Request instance
+ * @param param parameter name in request
+ * @param index index of value to retrieve for param in request
+ * @param[out] value retrieved value
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_value(const metkit_marsrequest_t* request, const char* param, int index, const char** value);
+
+/** Populates empty Request object by expanding existing request
+ * @param request Request instance to be expanded
+ * @param inherit if true, populate expanded request with default values
+ * @param strict it true, raise error rather than warning on invalid values
+ * @param[out] expandedRequest empty Request instance to be populated
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_expand(const metkit_marsrequest_t* request,  bool inherit, bool strict, metkit_marsrequest_t* expandedRequest);
+
+/** Merges other Request object into existing request
+ * @param request Request instance to contain result of merge
+ * @param otherRequest other Request instance to merge
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_marsrequest_merge(metkit_marsrequest_t* request, const metkit_marsrequest_t* otherRequest);
+
+/* ---------------------------------------------------------------------------------------------------------------------
+ * REQUEST ITERATOR
+ * --- */
+
+/** Deallocates RequestIterator object and associated resources.
+ * @param it RequestIterator instance
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_delete_requestiterator(const metkit_requestiterator_t* it);
+
+/** Moves to the next Request element in RequestIterator
+ * @param it RequestIterator instance
+ * @return metkit_iterator_status_t Status of iterator
+ */
+// metkit_error_t metkit_requestiterator_next(metkit_requestiterator_t* it);
+metkit_iterator_status_t metkit_requestiterator_next(metkit_requestiterator_t* it);
+
+/** Populates empty Request object with data from current element in RequestIterator
+ * @param it RequestIterator instance
+ * @param request empty Request instance to populate with data
+ * @return metkit_iterator_status_t Status of iterator
+ * @note must call metkit_requestiterator_next before calling metkit_requestiterator_current.
+ *
+ * Example usage:
+    * while (metkit_requestiterator_next(it) == METKIT_ITERATOR_SUCCESS) {
+    *     metkit_marsrequest_t* req{};
+    *     metkit_marsrequest_new(&req);
+    *     metkit_requestiterator_current(it, req);
+    *     // use req ...
+    * }
+ */
+metkit_iterator_status_t metkit_requestiterator_current(metkit_requestiterator_t* it, metkit_marsrequest_t* request);
+
+/* ---------------------------------------------------------------------------------------------------------------------
+ * PARAMETER ITERATOR
+ * --- */
+
+/** Deallocates ParamIterator object and the char* strings it owns.
+ * @param it ParamIterator instance
+ * @return metkit_error_t Error code
+ */
+metkit_error_t metkit_paramiterator_delete(const metkit_paramiterator_t* it);
+
+/** Moves to the next parameter in ParamIterator
+ * @param it ParamIterator instance
+ * @return metkit_iterator_status_t Status of iterator
+ */
+metkit_iterator_status_t metkit_paramiterator_next(metkit_paramiterator_t* it);
+
+/** Returns current parameter in ParamIterator
+ * @param it ParamIterator instance
+ * @param[out] param current parameter
+ * @return metkit_iterator_status_t Status of iterator
+ */
+metkit_iterator_status_t metkit_paramiterator_current(const metkit_paramiterator_t* it, const char** param);
+    
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/metkit/mars/MarsRequest.h b/src/metkit/mars/MarsRequest.h
index 30bdbc4b..e5e1d5f3 100644
--- a/src/metkit/mars/MarsRequest.h
+++ b/src/metkit/mars/MarsRequest.h
@@ -27,6 +27,8 @@ class Message;
 }
 }
 
+struct metkit_marsrequest_t;
+
 namespace metkit {
 namespace mars {
 
@@ -126,6 +128,9 @@ class MarsRequest {
     static MarsRequest parse(const std::string& s, bool strict = false);
     static std::vector<MarsRequest> parse(std::istream&, bool strict = false);
 
+    /// Implementation in api/metkit_c.cc
+    static const MarsRequest& fromOpaque(const metkit_marsrequest_t* request);
+
 private:  // members
     std::string verb_;
     std::list<Parameter> params_;
diff --git a/src/metkit/metkit_version.cc.in b/src/metkit/metkit_version.c
similarity index 52%
rename from src/metkit/metkit_version.cc.in
rename to src/metkit/metkit_version.c
index 60a39ecc..f54b3f32 100644
--- a/src/metkit/metkit_version.cc.in
+++ b/src/metkit/metkit_version.c
@@ -1,20 +1,19 @@
-#include "metkit/metkit_version.h"
+#include "metkit_version.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-const char * metkit_version()     { return metkit_VERSION; }
+const char * metkit_version() { return metkit_VERSION; }
 
-const char * metkit_version_str() { return metkit_VERSION_STR; }
-
-unsigned int metkit_version_int()
-{
+unsigned int metkit_version_int() {
   return 10000*metkit_VERSION_MAJOR + 100*metkit_VERSION_MINOR + 1*metkit_VERSION_PATCH;
 }
 
-const char * metkit_git_sha1() { return "@metkit_GIT_SHA1@"; }
+const char * metkit_version_str() { return metkit_VERSION_STR; }
+
+const char * metkit_git_sha1() { return metkit_GIT_SHA1; }
 
 #ifdef __cplusplus
 }
-#endif
\ No newline at end of file
+#endif
diff --git a/src/metkit/metkit_version.h.in b/src/metkit/metkit_version.h.in
index 5f42d64b..e55c3b0e 100644
--- a/src/metkit/metkit_version.h.in
+++ b/src/metkit/metkit_version.h.in
@@ -8,6 +8,8 @@
 #define metkit_VERSION_MINOR @metkit_VERSION_MINOR@
 #define metkit_VERSION_PATCH @metkit_VERSION_PATCH@
 
+#define metkit_GIT_SHA1 "@metkit_GIT_SHA1@"
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -24,4 +26,5 @@ const char * metkit_git_sha1();
 }
 #endif
 
+
 #endif // metkit_version_h
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e59524ae..9067809a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -71,7 +71,7 @@ ecbuild_add_test( TARGET        metkit_test_gribhandle
                   NO_AS_NEEDED
                   ENVIRONMENT   "${metkit_env}")
 
-list(APPEND testFileSuffixes typesfactory expand request param_axis steprange_axis time hypercube type_levelist )
+list(APPEND testFileSuffixes typesfactory expand request param_axis steprange_axis time hypercube type_levelist c_api )
 
 foreach(test IN LISTS testFileSuffixes)
     ecbuild_add_test( TARGET    "metkit_test_${test}"
@@ -82,6 +82,14 @@ foreach(test IN LISTS testFileSuffixes)
                       LIBS      metkit)
 endforeach()
 
+# Compile C test
+ecbuild_add_test( TARGET        metkit_test_c_compiled
+                  SOURCES       test_c_api.c
+                  INCLUDES      "${ECKIT_INCLUDE_DIRS}"
+                  LIBS          metkit
+                  NO_AS_NEEDED
+                  ENVIRONMENT   "${metkit_env}")
+
 # if ( HAVE_NETCDF )
 #    add_subdirectory(netcdf)
 # endif()
diff --git a/tests/test_c_api.c b/tests/test_c_api.c
new file mode 100644
index 00000000..d15dd508
--- /dev/null
+++ b/tests/test_c_api.c
@@ -0,0 +1,27 @@
+/*
+ * (C) Copyright 1996- ECMWF.
+ *
+ * This software is licensed under the terms of the Apache Licence Version 2.0
+ * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+ * In applying this licence, ECMWF does not waive the privileges and immunities
+ * granted to it by virtue of its status as an intergovernmental organisation
+ * nor does it submit to any jurisdiction.
+ */
+
+/// @file   test_c_api.c
+/// @date   Dec 2024
+/// @author Christopher Bradley
+
+#include "metkit/api/metkit_c.h"
+#include <stdio.h>
+
+int main(int argc, char **argv) {
+
+    const char* version = metkit_version();
+
+    metkit_error_t err = metkit_initialise();
+
+    fprintf(stdout, "MetKit version: %s\n", version);
+
+    return 0;
+}
diff --git a/tests/test_c_api.cc b/tests/test_c_api.cc
new file mode 100644
index 00000000..d2f47821
--- /dev/null
+++ b/tests/test_c_api.cc
@@ -0,0 +1,226 @@
+/*
+ * (C) Copyright 1996- ECMWF.
+ *
+ * This software is licensed under the terms of the Apache Licence Version 2.0
+ * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+ * In applying this licence, ECMWF does not waive the privileges and immunities
+ * granted to it by virtue of its status as an intergovernmental organisation
+ * nor does it submit to any jurisdiction.
+ */
+
+/// @file   test_c_api.cc
+/// @date   Dec 2024
+/// @author Christopher Bradley
+
+#include "metkit/api/metkit_c.h"
+#include "eckit/testing/Test.h"
+#include "eckit/types/Date.h"
+#include "metkit/mars/MarsRequest.h"
+#include <cstring>
+
+using namespace eckit::testing;
+namespace metkit::test {
+
+// Wrapper around the C function calls that will throw an exception if the function fails
+// i.e. if the return value is not METKIT_SUCCESS
+
+void test_succes(int e) {
+    metkit_error_values_t err = static_cast<metkit_error_values_t>(e);
+    if (err != METKIT_SUCCESS)
+        throw TestException("C-API error: " + std::string(metkit_get_error_string(err)), Here());
+}
+
+void EXPECT_STR_EQUAL(const char* a, const char* b) {
+    if (strcmp(a, b) != 0)
+        throw TestException("Expected: " + std::string(a) + " == " + std::string(b), Here());
+}
+
+// Fairly minimal test coverage
+CASE( "metkit_marsrequest" ) {
+
+    // -----------------------------------------------------------------
+    // Basics
+    // -----------------------------------------------------------------
+
+    metkit_marsrequest_t* request{};
+
+    test_succes(metkit_marsrequest_new(&request));
+    EXPECT(request);
+
+    // set/get verb
+    test_succes(metkit_marsrequest_set_verb(request, "retrieve"));
+    const char* verb{};
+    test_succes(metkit_marsrequest_verb(request, &verb));
+    EXPECT_STR_EQUAL(verb, "retrieve");
+
+    // set array of values
+    const char* dates[] = {"20200101", "20200102", "-1"};
+    test_succes(metkit_marsrequest_set(request, "date", dates, 3));
+
+    // set single value
+    const char* expver = "xxxx";
+    test_succes(metkit_marsrequest_set_one(request, "expver", expver));
+    test_succes(metkit_marsrequest_set_one(request, "param", "2t"));
+
+    // check values
+    bool has = false;
+    test_succes(metkit_marsrequest_has_param(request, "date", &has));
+    EXPECT(has);
+
+    test_succes(metkit_marsrequest_has_param(request, "random", &has));
+    EXPECT(!has);
+
+    size_t count = 0;
+    test_succes(metkit_marsrequest_count_values(request, "date", &count));
+    EXPECT_EQUAL(count, 3);
+
+    for (size_t i = 0; i < count; i++) {
+        const char* value{};
+        test_succes(metkit_marsrequest_value(request, "date", i, &value));
+        EXPECT_STR_EQUAL(value, dates[i]);
+    }
+
+    // -----------------------------------------------------------------
+    // Expand
+    // -----------------------------------------------------------------
+
+    metkit_marsrequest_t* expandedRequest{};
+    test_succes(metkit_marsrequest_new(&expandedRequest));
+    test_succes(metkit_marsrequest_expand(request, false, true, expandedRequest));
+
+    // Check date expanded -1 -> yesterday
+    test_succes(metkit_marsrequest_count_values(expandedRequest, "date", &count));
+    EXPECT_EQUAL(count, 3);
+    const char** dates_expanded = new const char*[count];
+    for (size_t i = 0; i < count; i++) {
+        test_succes(metkit_marsrequest_value(expandedRequest, "date", i, &dates_expanded[i]));
+    }
+
+    EXPECT_STR_EQUAL(dates_expanded[2], std::to_string(eckit::Date(-1).yyyymmdd()).c_str());
+    // check param expanded 2t -> 167
+    const char* param{};
+    test_succes(metkit_marsrequest_value(expandedRequest, "param", 0, &param));
+    EXPECT_STR_EQUAL(param, "167");
+    
+    // -----------------------------------------------------------------
+    // Merge
+    // -----------------------------------------------------------------
+
+    metkit_marsrequest_t* req_manydates{};
+    test_succes(metkit_marsrequest_new(&req_manydates));
+    const char* dates_many[] = {"19000101", "19000102", "19000103"};
+    test_succes(metkit_marsrequest_set(req_manydates, "date", dates_many, 3));
+
+    test_succes(metkit_marsrequest_merge(request, req_manydates));
+    test_succes(metkit_marsrequest_count_values(request, "date", &count));
+    EXPECT_EQUAL(count, 6);
+
+    // -----------------------------------------------------------------
+    // done
+
+    metkit_marsrequest_delete(request);
+    metkit_marsrequest_delete(expandedRequest);
+    metkit_marsrequest_delete(req_manydates);
+}
+//-----------------------------------------------------------------------------
+
+CASE( "metkit_requestiterator_t parsing" ) {
+    
+    metkit_requestiterator_t* it{};
+    test_succes(metkit_parse_marsrequests("retrieve,date=-1,param=2t \n retrieve,date=20200102,param=2t,step=10/to/20/by/2", &it, true)); // two separate requests
+
+    std::vector <metkit_marsrequest_t*> requests;
+    metkit_iterator_status_t status;
+    while ((status = metkit_requestiterator_next(it)) == METKIT_ITERATOR_SUCCESS) {
+        metkit_marsrequest_t* req{};
+        test_succes(metkit_marsrequest_new(&req));
+        EXPECT_EQUAL(metkit_requestiterator_current(it, req), METKIT_ITERATOR_SUCCESS);
+        requests.push_back(req);
+    }
+    EXPECT_EQUAL(status, METKIT_ITERATOR_COMPLETE);
+    EXPECT_EQUAL(requests.size(), 2);
+    
+    // check the date
+    const char* date{};
+    test_succes(metkit_marsrequest_value(requests[0], "date", 0, &date));
+    EXPECT_STR_EQUAL(date, std::to_string(eckit::Date(-1).yyyymmdd()).c_str()); // parser also calls expand
+
+    test_succes(metkit_marsrequest_value(requests[1], "date", 0, &date));
+    EXPECT_STR_EQUAL(date, "20200102");
+
+    // Check steps have been parsed
+    size_t count = 0;
+    test_succes(metkit_marsrequest_count_values(requests[1], "step", &count));
+    EXPECT_EQUAL(count, 6);
+    for (size_t i = 0; i < count; i++) {
+        const char* step{};
+        test_succes(metkit_marsrequest_value(requests[1], "step", i, &step));
+        EXPECT_STR_EQUAL(step, std::to_string(10 + i*2).c_str());
+    }
+
+    // cleanup
+    metkit_delete_requestiterator(it); // NB: requests have been moved out of the iterator
+    for (auto req : requests) {
+        metkit_marsrequest_delete(req);
+    }
+}
+
+// Ensure that the param iterator works as expected
+CASE( "metkit_paramiterator_t " ) {
+
+    std::string str = "retrieve,date=20200102,param=2t,step=10/to/20/by/2";
+    metkit_marsrequest_t* request{};
+    test_succes(metkit_marsrequest_new(&request));
+    test_succes(metkit_parse_marsrequest(str.c_str(), request, true));
+
+    metkit_paramiterator_t* it{};
+    test_succes(metkit_marsrequest_params(request, &it));
+    metkit_iterator_status_t status;
+    std::set<std::string> keys;
+    while ((status = metkit_paramiterator_next(it)) == METKIT_ITERATOR_SUCCESS) {
+        const char* key{};
+        EXPECT_EQUAL(metkit_paramiterator_current(it, &key), METKIT_ITERATOR_SUCCESS);
+        keys.insert(key);
+    }
+    EXPECT_EQUAL(status, METKIT_ITERATOR_COMPLETE);
+
+    // Compare with C++ impl for consistency
+
+    mars::MarsRequest req = mars::MarsRequest::parse(str, true);
+    std::set<std::string> keys_cpp;
+    for (const auto& k : req.params()) {
+        keys_cpp.insert(k);
+    }
+
+    EXPECT_EQUAL(keys, keys_cpp);
+}
+
+CASE( "metkit_requestiterator_t 1 item" ) {
+    // Edge case: verify iterator with one item works the same way.
+
+    metkit_requestiterator_t* it{};
+    test_succes(metkit_parse_marsrequests("retrieve,date=-1,param=2t", &it, true));
+
+    std::vector <metkit_marsrequest_t*> requests;
+    metkit_iterator_status_t status;
+    while ((status = metkit_requestiterator_next(it)) == METKIT_ITERATOR_SUCCESS) {
+        metkit_marsrequest_t* req{};
+        test_succes(metkit_marsrequest_new(&req));
+        EXPECT_EQUAL(metkit_requestiterator_current(it, req), METKIT_ITERATOR_SUCCESS);
+        requests.push_back(req);
+    }
+    EXPECT_EQUAL(status, METKIT_ITERATOR_COMPLETE);
+    EXPECT_EQUAL(requests.size(), 1);
+
+    // cleanup
+    metkit_delete_requestiterator(it);
+    for (auto req : requests) {
+        metkit_marsrequest_delete(req);
+    }
+}
+
+}  // namespace metkit::test
+
+int main(int argc, char **argv) {
+    return run_tests ( argc, argv );
+}