Repository: pact-foundation/pact-python (language: python)

Issue:
Add support and example for verifying a message provider using the Rust engine
“Add support” means adding DSL methods that call the underlying Rust functionality.

“Add example” means create an example Python project that showcases using the functionality.
Add support and example for verifying a message provider using the Rust engine
“Add support” means adding DSL methods that call the underlying Rust functionality.

“Add example” means create an example Python project that showcases using the functionality.


Discussion / hints:
Hi there - I can work on this
Hi there - I can work on this

Fix patch:
diff --git a/examples/src/message_producer.py b/examples/src/message_producer.py
new file mode 100644
index 0000000000..efbe7e7621
--- /dev/null
+++ b/examples/src/message_producer.py
@@ -0,0 +1,106 @@
+"""
+Message producer for non-HTTP interactions.
+
+This modules implements a very basic message producer which could
+send to an eventing system, such as Kafka, or a message queue.
+"""
+
+from __future__ import annotations
+
+import enum
+import json
+from typing import Literal, NamedTuple
+
+
+class FileSystemAction(enum.Enum):
+    """
+    Represents a file system action.
+    """
+
+    READ = "READ"
+    WRITE = "WRITE"
+
+
+class FileSystemEvent(NamedTuple):
+    """
+    Represents a file system event.
+    """
+
+    action: Literal[FileSystemAction.READ, FileSystemAction.WRITE]
+    path: str
+    contents: str | None
+
+
+class MockMessageQueue:
+    """
+    A mock message queue.
+    """
+
+    def __init__(self) -> None:
+        """
+        Initialize the message queue.
+        """
+        self.messages: list[str] = []
+
+    def send(self, message: str) -> None:
+        """
+        Send a message to the queue.
+
+        Args:
+            message: The message to send.
+        """
+        self.messages.append(message)
+
+
+class FileSystemMessageProducer:
+    """
+    A message producer for file system events.
+    """
+
+    def __init__(self) -> None:
+        """
+        Initialize the message producer.
+        """
+        self.queue = MockMessageQueue()
+
+    def send_to_queue(self, message: FileSystemEvent) -> None:
+        """
+        Send a message to a message queue.
+
+        :param message: The message to send.
+        """
+        self.queue.send(
+            json.dumps({
+                "action": message.action.value,
+                "path": message.path,
+                "contents": message.contents,
+            })
+        )
+
+    def send_write_event(self, filename: str, contents: str) -> None:
+        """
+        Send a write event to a message queue.
+
+        Args:
+            filename: The name of the file.
+            contents: The contents of the file.
+        """
+        message = FileSystemEvent(
+            action=FileSystemAction.WRITE,
+            path=filename,
+            contents=contents,
+        )
+        self.send_to_queue(message)
+
+    def send_read_event(self, filename: str) -> None:
+        """
+        Send a read event to a message queue.
+
+        :param filename: The name of the file.
+        """
+        message = FileSystemEvent(
+            action=FileSystemAction.READ,
+            path=filename,
+            contents=None,
+        )
+        self.send_to_queue(message)
diff --git a/hatch_build.py b/hatch_build.py
index ab5e84e0f9..dcb65ec1c2 100644
--- a/hatch_build.py
+++ b/hatch_build.py
@@ -36,7 +36,7 @@
 
 # Latest version available at:
 # https://github.com/pact-foundation/pact-reference/releases
-PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.21")
+PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.22")
 PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}"
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 8c7c392673..0a1b0e463b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -208,6 +208,7 @@ markers = [
   # Markers for the compatibility suite
   "consumer",
   "provider",
+  "message",
 ]
 
 ################################################################################
diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py
index 8108dcee44..0231ac4d99 100644
--- a/src/pact/v3/ffi.py
+++ b/src/pact/v3/ffi.py
@@ -417,7 +417,7 @@ class InteractionHandle:
     Handle to a HTTP Interaction.
 
     [Rust
-    `InteractionHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.InteractionHandle.html)
+    `InteractionHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.InteractionHandle.html)
     """
 
     def __init__(self, ref: int) -> None:
@@ -879,7 +879,7 @@ class PactHandle:
     Handle to a Pact.
 
     [Rust
-    `PactHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.PactHandle.html)
+    `PactHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.PactHandle.html)
     """
 
     def __init__(self, ref: int) -> None:
@@ -1517,7 +1517,7 @@ class VerifierHandle:
     """
     Handle to a Verifier.
 
-    [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/verifier/handle/struct.VerifierHandle.html)
+    [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/verifier/handle/struct.VerifierHandle.html)
     """
 
     def __init__(self, ref: cffi.FFI.CData) -> None:
@@ -1553,7 +1553,7 @@ class ExpressionValueType(Enum):
     """
     Expression Value Type.
 
-    [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/expressions/enum.ExpressionValueType.html)
+    [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/expressions/enum.ExpressionValueType.html)
     """
 
     UNKNOWN = lib.ExpressionValueType_Unknown
@@ -1580,7 +1580,7 @@ class GeneratorCategory(Enum):
     """
     Generator Category.
 
-    [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/generators/enum.GeneratorCategory.html)
+    [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/generators/enum.GeneratorCategory.html)
     """
 
     METHOD = lib.GeneratorCategory_METHOD
@@ -1608,7 +1608,7 @@ class InteractionPart(Enum):
     """
     Interaction Part.
 
-    [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/enum.InteractionPart.html)
+    [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/enum.InteractionPart.html)
     """
 
     REQUEST = lib.InteractionPart_Request
@@ -1654,7 +1654,7 @@ class MatchingRuleCategory(Enum):
     """
     Matching Rule Category.
 
-    [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html)
+    [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html)
     """
 
     METHOD = lib.MatchingRuleCategory_METHOD
@@ -1683,7 +1683,7 @@ class PactSpecification(Enum):
     """
     Pact Specification.
 
-    [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/pact_specification/enum.PactSpecification.html)
+    [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/pact_specification/enum.PactSpecification.html)
     """
 
     UNKNOWN = lib.PactSpecification_Unknown
@@ -1736,7 +1736,7 @@ class _StringResult(Enum):
         """
         Internal enum from Pact FFI.
 
-        [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/enum.StringResult.html)
+        [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/enum.StringResult.html)
         """
 
         FAILED = lib.StringResult_Failed
@@ -1892,7 +1892,7 @@ def version() -> str:
     """
     Return the version of the pact_ffi library.
 
-    [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_version)
+    [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_version)
 
     Returns:
         The version of the pact_ffi library as a string, in the form of `x.y.z`.
@@ -1912,7 +1912,7 @@ def init(log_env_var: str) -> None:
     tracing subscriber.
 
     [Rust
-    `pactffi_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init)
+    `pactffi_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init)
 
     # Safety
 
@@ -1929,7 +1929,7 @@ def init_with_log_level(level: str = "INFO") -> None:
     tracing subscriber.
 
     [Rust
-    `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init_with_log_level)
+    `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init_with_log_level)
 
     Args:
         level:
@@ -1949,7 +1949,7 @@ def enable_ansi_support() -> None:
     On non-Windows platforms, this function is a no-op.
 
     [Rust
-    `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_enable_ansi_support)
+    `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_enable_ansi_support)
 
     # Safety
 
@@ -1967,7 +1967,7 @@ def log_message(
     Log using the shared core logging facility.
 
     [Rust
-    `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_message)
+    `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_message)
 
     This is useful for callers to have a single set of logs.
 
@@ -1999,7 +1999,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator:
     Get an iterator over mismatches.
 
     [Rust
-    `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_get_iter)
+    `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_get_iter)
     """
     raise NotImplementedError
 
@@ -2008,7 +2008,7 @@ def mismatches_delete(mismatches: Mismatches) -> None:
     """
     Delete mismatches.
 
-    [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_delete)
+    [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_delete)
     """
     raise NotImplementedError
 
@@ -2017,7 +2017,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch:
     """
     Get the next mismatch from a mismatches iterator.
 
-    [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_next)
+    [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_next)
 
     Returns a null pointer if no mismatches remain.
     """
@@ -2028,7 +2028,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None:
     """
     Delete a mismatches iterator when you're done with it.
 
-    [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_delete)
+    [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_delete)
     """
     raise NotImplementedError
 
@@ -2037,7 +2037,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str:
     """
     Get a JSON representation of the mismatch.
 
-    [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_to_json)
+    [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_to_json)
     """
     raise NotImplementedError
 
@@ -2046,7 +2046,7 @@ def mismatch_type(mismatch: Mismatch) -> str:
     """
     Get the type of a mismatch.
 
-    [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_type)
+    [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_type)
     """
     raise NotImplementedError
 
@@ -2055,7 +2055,7 @@ def mismatch_summary(mismatch: Mismatch) -> str:
     """
     Get a summary of a mismatch.
 
-    [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_summary)
+    [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_summary)
     """
     raise NotImplementedError
 
@@ -2064,7 +2064,7 @@ def mismatch_description(mismatch: Mismatch) -> str:
     """
     Get a description of a mismatch.
 
-    [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_description)
+    [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_description)
     """
     raise NotImplementedError
 
@@ -2073,7 +2073,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str:
     """
     Get an ANSI-compatible description of a mismatch.
 
-    [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_ansi_description)
+    [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_ansi_description)
     """
     raise NotImplementedError
 
@@ -2083,7 +2083,7 @@ def get_error_message(length: int = 1024) -> str | None:
     Provide the error message from `LAST_ERROR` to the calling C code.
 
     [Rust
-    `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_error_message)
+    `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_error_message)
 
     This function should be called after any other function in the pact_matching
     FFI indicates a failure with its own error message, if the caller wants to
@@ -2137,7 +2137,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int:
     """
     Convenience function to direct all logging to stdout.
 
-    [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stdout)
+    [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stdout)
     """
     raise NotImplementedError
 
@@ -2147,7 +2147,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None:
     Convenience function to direct all logging to stderr.
 
     [Rust
-    `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stderr)
+    `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stderr)
 
     Args:
         level_filter:
@@ -2172,7 +2172,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int:
     Convenience function to direct all logging to a file.
 
     [Rust
-    `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_file)
+    `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_file)
 
     # Safety
 
@@ -2186,7 +2186,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None:
     """
     Convenience function to direct all logging to a task local memory buffer.
 
-    [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_buffer)
+    [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_buffer)
 
     Raises:
         RuntimeError:
@@ -2204,7 +2204,7 @@ def logger_init() -> None:
     """
     Initialize the FFI logger with no sinks.
 
-    [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_init)
+    [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_init)
 
     This initialized logger does nothing until `pactffi_logger_apply` has been called.
 
@@ -2226,7 +2226,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int:
     Attach an additional sink to the thread-local logger.
 
     [Rust
-    `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_attach_sink)
+    `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_attach_sink)
 
     This logger does nothing until `pactffi_logger_apply` has been called.
 
@@ -2273,7 +2273,7 @@ def logger_apply() -> int:
     Apply the previously configured sinks and levels to the program.
 
     [Rust
-    `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_apply)
+    `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_apply)
 
     If no sinks have been setup, will set the log level to info and the target
     to standard out.
@@ -2289,7 +2289,7 @@ def fetch_log_buffer(log_id: str) -> str:
     Fetch the in-memory logger buffer contents.
 
     [Rust
-    `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_fetch_log_buffer)
+    `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_fetch_log_buffer)
 
     This will only have any contents if the `buffer` sink has been configured to
     log to. The contents will be allocated on the heap and will need to be freed
@@ -2315,7 +2315,7 @@ def parse_pact_json(json: str) -> Pact:
     Parses the provided JSON into a Pact model.
 
     [Rust
-    `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_pact_json)
+    `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_pact_json)
 
     The returned Pact model must be freed with the `pactffi_pact_model_delete`
     function when no longer needed.
@@ -2332,7 +2332,7 @@ def pact_model_delete(pact: Pact) -> None:
     """
     Frees the memory used by the Pact model.
 
-    [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_delete)
+    [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_delete)
     """
     raise NotImplementedError
 
@@ -2342,7 +2342,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator:
     Returns an iterator over all the interactions in the Pact.
 
     [Rust
-    `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_interaction_iterator)
+    `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_interaction_iterator)
 
     The iterator will have to be deleted using the
     `pactffi_pact_interaction_iter_delete` function. The iterator will contain a
@@ -2361,7 +2361,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification:
     """
     Returns the Pact specification enum that the Pact is for.
 
-    [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_spec_version)
+    [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_spec_version)
     """
     raise NotImplementedError
 
@@ -2370,7 +2370,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None:
     """
     Frees the memory used by the Pact interaction model.
 
-    [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_delete)
+    [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_delete)
     """
     raise NotImplementedError
 
@@ -2379,7 +2379,7 @@ def async_message_new() -> AsynchronousMessage:
     """
     Get a mutable pointer to a newly-created default message on the heap.
 
-    [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_new)
+    [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_new)
 
     # Safety
 
@@ -2396,7 +2396,7 @@ def async_message_delete(message: AsynchronousMessage) -> None:
     """
     Destroy the `AsynchronousMessage` being pointed to.
 
-    [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_delete)
+    [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_delete)
     """
     lib.pactffi_async_message_delete(message._ptr)
 
@@ -2406,7 +2406,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents
     Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer.
 
     [Rust
-    `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents)
+    `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents)
 
     If the message contents are missing, this function will return `None`.
     """
@@ -2425,7 +2425,7 @@ def async_message_generate_contents(
     contents as would be received by the consumer.
 
     [Rust
-    `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_generate_contents)
+    `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_generate_contents)
 
     If the message contents are missing, this function will return `None`.
     """
@@ -2439,7 +2439,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str:
     """
     Get the message contents of an `AsynchronousMessage` in string form.
 
-    [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_str)
+    [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_str)
 
     # Safety
 
@@ -2466,7 +2466,7 @@ def async_message_set_contents_str(
     Sets the contents of the message as a string.
 
     [Rust
-    `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_str)
+    `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_str)
 
     - `message` - the message to set the contents for
     - `contents` - pointer to contents to copy from. Must be a valid
@@ -2494,7 +2494,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int:
     Get the length of the contents of a `AsynchronousMessage`.
 
     [Rust
-    `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_length)
+    `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_length)
 
     # Safety
 
@@ -2513,7 +2513,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str:
     Get the contents of an `AsynchronousMessage` as bytes.
 
     [Rust
-    `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_bin)
+    `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_bin)
 
     # Safety
 
@@ -2540,7 +2540,7 @@ def async_message_set_contents_bin(
     Sets the contents of the message as an array of bytes.
 
     [Rust
-    `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_bin)
+    `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_bin)
 
     * `message` - the message to set the contents for
     * `contents` - pointer to contents to copy from
@@ -2567,7 +2567,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str:
     Get a copy of the description.
 
     [Rust
-    `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_description)
+    `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_description)
 
     Raises:
         RuntimeError:
@@ -2587,7 +2587,7 @@ def async_message_set_description(
     """
     Write the `description` field on the `AsynchronousMessage`.
 
-    [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_description)
+    [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_description)
 
     # Safety
 
@@ -2612,7 +2612,7 @@ def async_message_get_provider_state(
     Get a copy of the provider state at the given index from this message.
 
     [Rust
-    `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state)
+    `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state)
 
     Raises:
         RuntimeError:
@@ -2631,7 +2631,7 @@ def async_message_get_provider_state_iter(
     """
     Get an iterator over provider states.
 
-    [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state_iter)
+    [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state_iter)
 
     # Safety
 
@@ -2646,7 +2646,7 @@ def consumer_get_name(consumer: Consumer) -> str:
     r"""
     Get a copy of this consumer's name.
 
-    [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_consumer_get_name)
+    [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_consumer_get_name)
 
     The copy must be deleted with `pactffi_string_delete`.
 
@@ -2692,7 +2692,7 @@ def pact_get_consumer(pact: Pact) -> Consumer:
     `pactffi_pact_consumer_delete` when no longer required.
 
     [Rust
-    `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_consumer)
+    `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_consumer)
 
     # Errors
 
@@ -2706,7 +2706,7 @@ def pact_consumer_delete(consumer: Consumer) -> None:
     """
     Frees the memory used by the Pact consumer.
 
-    [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_consumer_delete)
+    [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_consumer_delete)
     """
     raise NotImplementedError
 
@@ -2722,7 +2722,7 @@ def message_contents_delete(contents: MessageContents) -> None:
     Deleting a message content which is associated with an interaction
     will result in undefined behaviour.
 
-    [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_delete)
+    [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_delete)
     """
     lib.pactffi_message_contents_delete(contents._ptr)
 
@@ -2731,7 +2731,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None:
     """
     Get the message contents in string form.
 
-    [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_str)
+    [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_str)
 
     If the message has no contents or contain invalid UTF-8 characters, this
     function will return `None`.
@@ -2751,7 +2751,7 @@ def message_contents_set_contents_str(
     Sets the contents of the message as a string.
 
     [Rust
-    `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_str)
+    `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_str)
 
     * `contents` - the message contents to set the contents for
     * `contents_str` - pointer to contents to copy from. Must be a valid
@@ -2778,7 +2778,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int:
     """
     Get the length of the message contents.
 
-    [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_length)
+    [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_length)
 
     If the message has not contents, this function will return 0.
     """
@@ -2790,7 +2790,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None
     Get the contents of a message as a pointer to an array of bytes.
 
     [Rust
-    `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_bin)
+    `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_bin)
 
     If the message has no contents, this function will return `None`.
     """
@@ -2813,7 +2813,7 @@ def message_contents_set_contents_bin(
     Sets the contents of the message as an array of bytes.
 
     [Rust
-    `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_bin)
+    `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_bin)
 
     * `message` - the message contents to set the contents for
     * `contents_bin` - pointer to contents to copy from
@@ -2842,7 +2842,7 @@ def message_contents_get_metadata_iter(
     Get an iterator over the metadata of a message.
 
     [Rust
-    `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_metadata_iter)
+    `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_metadata_iter)
 
     # Safety
 
@@ -2871,7 +2871,7 @@ def message_contents_get_matching_rule_iter(
     Get an iterator over the matching rules for a category of a message.
 
     [Rust
-    `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter)
+    `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter)
 
     The returned pointer must be deleted with
     `pactffi_matching_rules_iter_delete` when done with it.
@@ -2913,7 +2913,7 @@ def request_contents_get_matching_rule_iter(
     r"""
     Get an iterator over the matching rules for a category of an HTTP request.
 
-    [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter)
+    [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter)
 
     The returned pointer must be deleted with
     `pactffi_matching_rules_iter_delete` when done with it.
@@ -2950,7 +2950,7 @@ def response_contents_get_matching_rule_iter(
     r"""
     Get an iterator over the matching rules for a category of an HTTP response.
 
-    [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter)
+    [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter)
 
     The returned pointer must be deleted with
     `pactffi_matching_rules_iter_delete` when done with it.
@@ -2988,7 +2988,7 @@ def message_contents_get_generators_iter(
     Get an iterator over the generators for a category of a message.
 
     [Rust
-    `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_generators_iter)
+    `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_generators_iter)
 
     # Safety
 
@@ -3014,7 +3014,7 @@ def request_contents_get_generators_iter(
     Get an iterator over the generators for a category of an HTTP request.
 
     [Rust
-    `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_generators_iter)
+    `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_generators_iter)
 
     The returned pointer must be deleted with `pactffi_generators_iter_delete`
     when done with it.
@@ -3039,7 +3039,7 @@ def response_contents_get_generators_iter(
     Get an iterator over the generators for a category of an HTTP response.
 
     [Rust
-    `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_generators_iter)
+    `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_generators_iter)
 
     The returned pointer must be deleted with `pactffi_generators_iter_delete`
     when done with it.
@@ -3064,7 +3064,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult:
     any generator.
 
     [Rust
-    `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_matcher_definition)
+    `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_matcher_definition)
 
     The following are examples of matching rule definitions:
 
@@ -3100,7 +3100,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str:
     using the `pactffi_string_delete` function once done with it.
 
     [Rust
-    `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_error)
+    `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_error)
     """
     raise NotImplementedError
 
@@ -3114,7 +3114,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str:
     the `pactffi_string_delete` function once done with it.
 
     [Rust
-    `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value)
+    `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value)
 
     Note that different expressions values can have types other than a string.
     Use `pactffi_matcher_definition_value_type` to get the actual type of the
@@ -3128,7 +3128,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None:
     """
     Frees the memory used by the result of parsing the matching definition expression.
 
-    [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_delete)
+    [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_delete)
     """
     raise NotImplementedError
 
@@ -3141,7 +3141,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge
     NULL pointer, otherwise returns the generator as a pointer.
 
     [Rust
-    `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_generator)
+    `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_generator)
 
     The generator pointer will be a valid pointer as long as
     `pactffi_matcher_definition_delete` has not been called on the definition.
@@ -3160,7 +3160,7 @@ def matcher_definition_value_type(
     If there was an error parsing the expression, it will return Unknown.
 
     [Rust
-    `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value_type)
+    `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value_type)
     """
     raise NotImplementedError
 
@@ -3169,7 +3169,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None:
     """
     Free the iterator when you're done using it.
 
-    [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_delete)
+    [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_delete)
     """
     raise NotImplementedError
 
@@ -3184,7 +3184,7 @@ def matcher_definition_iter(
     `pactffi_matching_rule_iter_delete` function once done with it.
 
     [Rust
-    `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_iter)
+    `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_iter)
 
     If there was an error parsing the expression, this function will return a
     NULL pointer.
@@ -3200,7 +3200,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult:
     deleted but will be cleaned up when the iterator is deleted.
 
     [Rust
-    `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_next)
+    `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_next)
 
     Will return a NULL pointer when the iterator has advanced past the end of
     the list.
@@ -3222,7 +3222,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int:
     Return the ID of the matching rule.
 
     [Rust
-    `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_id)
+    `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_id)
 
     The ID corresponds to the following rules:
 
@@ -3268,7 +3268,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str:
     pointer.
 
     [Rust
-    `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_value)
+    `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_value)
 
     The associated values for the rules are:
 
@@ -3316,7 +3316,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule:
     Will return a NULL pointer if the matching rule result was a reference.
 
     [Rust
-    `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_pointer)
+    `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_pointer)
 
     # Safety
 
@@ -3334,7 +3334,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str:
     structure. I.e.,
 
     [Rust
-    `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_reference_name)
+    `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_reference_name)
 
     ```json
     {
@@ -3361,7 +3361,7 @@ def validate_datetime(value: str, format: str) -> None:
     Validates the date/time value against the date/time format string.
 
     [Rust
-    `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_validate_datetime)
+    `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_validate_datetime)
 
     Raises:
         ValueError:
@@ -3388,7 +3388,7 @@ def generator_to_json(generator: Generator) -> str:
     Get the JSON form of the generator.
 
     [Rust
-    `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_to_json)
+    `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_to_json)
 
     The returned string must be deleted with `pactffi_string_delete`.
 
@@ -3411,7 +3411,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str:
     function).
 
     [Rust
-    `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_string)
+    `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_string)
 
     If anything goes wrong, it will return a NULL pointer.
     """
@@ -3434,7 +3434,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int:
     should be the values returned from the Provider State callback function).
 
     [Rust
-    `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_integer)
+    `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_integer)
 
     If anything goes wrong or the generator is not a type that can generate an
     integer value, it will return a zero value.
@@ -3450,7 +3450,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_delete)
+    `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_delete)
     """
     lib.pactffi_generators_iter_delete(iter._ptr)
 
@@ -3460,7 +3460,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa
     Get the next path and generator out of the iterator, if possible.
 
     [Rust
-    `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_next)
+    `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_next)
 
     The returned pointer must be deleted with
     `pactffi_generator_iter_pair_delete`.
@@ -3480,7 +3480,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None:
     Free a pair of key and value returned from `pactffi_generators_iter_next`.
 
     [Rust
-    `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_pair_delete)
+    `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_pair_delete)
     """
     lib.pactffi_generators_iter_pair_delete(pair._ptr)
 
@@ -3489,7 +3489,7 @@ def sync_http_new() -> SynchronousHttp:
     """
     Get a mutable pointer to a newly-created default interaction on the heap.
 
-    [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_new)
+    [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_new)
 
     # Safety
 
@@ -3507,7 +3507,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None:
     Destroy the `SynchronousHttp` interaction being pointed to.
 
     [Rust
-    `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_delete)
+    `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_delete)
     """
     lib.pactffi_sync_http_delete(interaction)
 
@@ -3517,7 +3517,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest:
     Get the request of a `SynchronousHttp` interaction.
 
     [Rust
-    `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request)
+    `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request)
 
     # Safety
 
@@ -3537,7 +3537,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None:
     Get the request contents of a `SynchronousHttp` interaction in string form.
 
     [Rust
-    `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents)
+    `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents)
 
     Note that this function will return `None` if either the body is missing or
     is `null`.
@@ -3557,7 +3557,7 @@ def sync_http_set_request_contents(
     Sets the request contents of the interaction.
 
     [Rust
-    `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents)
+    `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents)
 
     - `interaction` - the interaction to set the request contents for
     - `contents` - pointer to contents to copy from. Must be a valid
@@ -3585,7 +3585,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int:
     Get the length of the request contents of a `SynchronousHttp` interaction.
 
     [Rust
-    `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_length)
+    `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_length)
 
     This function will return 0 if the body is missing.
     """
@@ -3597,7 +3597,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes |
     Get the request contents of a `SynchronousHttp` interaction as bytes.
 
     [Rust
-    `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin)
+    `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin)
 
     Note that this function will return `None` if either the body is missing or
     is `null`.
@@ -3621,7 +3621,7 @@ def sync_http_set_request_contents_bin(
     Sets the request contents of the interaction as an array of bytes.
 
     [Rust
-    `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin)
+    `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin)
 
     - `interaction` - the interaction to set the request contents for
     - `contents` - pointer to contents to copy from
@@ -3648,7 +3648,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse:
     Get the response of a `SynchronousHttp` interaction.
 
     [Rust
-    `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response)
+    `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response)
 
     # Safety
 
@@ -3668,7 +3668,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None:
     Get the response contents of a `SynchronousHttp` interaction in string form.
 
     [Rust
-    `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents)
+    `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents)
 
     Note that this function will return `None` if either the body is missing or
     is `null`.
@@ -3688,7 +3688,7 @@ def sync_http_set_response_contents(
     Sets the response contents of the interaction.
 
     [Rust
-    `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents)
+    `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents)
 
     - `interaction` - the interaction to set the response contents for
     - `contents` - pointer to contents to copy from. Must be a valid
@@ -3716,7 +3716,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int:
     Get the length of the response contents of a `SynchronousHttp` interaction.
 
     [Rust
-    `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_length)
+    `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_length)
 
     This function will return 0 if the body is missing.
     """
@@ -3728,7 +3728,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes |
     Get the response contents of a `SynchronousHttp` interaction as bytes.
 
     [Rust
-    `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin)
+    `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin)
 
     Note that this function will return `None` if either the body is missing or
     is `null`.
@@ -3752,7 +3752,7 @@ def sync_http_set_response_contents_bin(
     Sets the response contents of the `SynchronousHttp` interaction as bytes.
 
     [Rust
-    `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin)
+    `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin)
 
     - `interaction` - the interaction to set the response contents for
     - `contents` - pointer to contents to copy from
@@ -3779,7 +3779,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str:
     Get a copy of the description.
 
     [Rust
-    `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_description)
+    `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_description)
 
     Raises:
         RuntimeError:
@@ -3797,7 +3797,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) ->
     Write the `description` field on the `SynchronousHttp`.
 
     [Rust
-    `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_description)
+    `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_description)
 
     # Safety
 
@@ -3822,7 +3822,7 @@ def sync_http_get_provider_state(
     Get a copy of the provider state at the given index from this interaction.
 
     [Rust
-    `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state)
+    `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state)
 
     # Safety
 
@@ -3848,7 +3848,7 @@ def sync_http_get_provider_state_iter(
     Get an iterator over provider states.
 
     [Rust
-    `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter)
+    `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter)
 
     # Safety
 
@@ -3877,7 +3877,7 @@ def pact_interaction_as_synchronous_http(
     longer required.
 
     [Rust
-    `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http)
+    `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http)
 
     # Safety This function is safe as long as the interaction pointer is a valid
     pointer.
@@ -3899,7 +3899,7 @@ def pact_interaction_as_asynchronous_message(
     no longer required.
 
     [Rust
-    `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message)
+    `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message)
 
     Note that if the interaction is a V3 `Message`, it will be converted to a V4
     `AsynchronousMessage` before being returned.
@@ -3924,7 +3924,7 @@ def pact_interaction_as_synchronous_message(
     no longer required.
 
     [Rust
-    `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message)
+    `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message)
 
     # Safety This function is safe as long as the interaction pointer is a valid
     pointer.
@@ -3939,7 +3939,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous
     Get the next asynchronous message from the iterator.
 
     [Rust
-    `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_next)
+    `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_next)
 
     Raises:
         StopIteration:
@@ -3956,7 +3956,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_delete)
+    `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_delete)
     """
     lib.pactffi_pact_async_message_iter_delete(iter._ptr)
 
@@ -3966,7 +3966,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes
     Get the next synchronous request/response message from the V4 pact.
 
     [Rust
-    `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_next)
+    `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_next)
 
     Raises:
         StopIteration:
@@ -3983,7 +3983,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_delete)
+    `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_delete)
     """
     lib.pactffi_pact_sync_message_iter_delete(iter._ptr)
 
@@ -3993,7 +3993,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp:
     Get the next synchronous HTTP request/response interaction from the V4 pact.
 
     [Rust
-    `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_next)
+    `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_next)
 
     Raises:
         StopIteration:
@@ -4010,7 +4010,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_delete)
+    `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_delete)
     """
     lib.pactffi_pact_sync_http_iter_delete(iter._ptr)
 
@@ -4020,7 +4020,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction
     Get the next interaction from the pact.
 
     [Rust
-    `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_next)
+    `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_next)
 
     Raises:
         StopIteration:
@@ -4038,7 +4038,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_delete)
+    `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_delete)
     """
     lib.pactffi_pact_interaction_iter_delete(iter._ptr)
 
@@ -4048,7 +4048,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str:
     Get the JSON form of the matching rule.
 
     [Rust
-    `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_to_json)
+    `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_to_json)
 
     The returned string must be deleted with `pactffi_string_delete`.
 
@@ -4065,7 +4065,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None:
     Free the iterator when you're done using it.
 
     [Rust
-    `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_delete)
+    `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_delete)
     """
     lib.pactffi_matching_rules_iter_delete(iter._ptr)
 
@@ -4077,7 +4077,7 @@ def matching_rules_iter_next(
     Get the next path and matching rule out of the iterator, if possible.
 
     [Rust
-    `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_next)
+    `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_next)
 
     The returned pointer must be deleted with
     `pactffi_matching_rules_iter_pair_delete`.
@@ -4099,7 +4099,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None:
     Free a pair of key and value returned from `message_metadata_iter_next`.
 
     [Rust
-    `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete)
+    `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete)
     """
     lib.pactffi_matching_rules_iter_pair_delete(pair._ptr)
 
@@ -4109,7 +4109,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState:
     Get the next value from the iterator.
 
     [Rust
-    `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_next)
+    `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_next)
 
     # Safety
 
@@ -4130,7 +4130,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None:
     Delete the iterator.
 
     [Rust
-    `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_delete)
+    `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_delete)
     """
     lib.pactffi_provider_state_iter_delete(iter._ptr)
 
@@ -4140,7 +4140,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata
     Get the next key and value out of the iterator, if possible.
 
     [Rust
-    `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_next)
+    `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_next)
 
     The returned pointer must be deleted with
     `pactffi_message_metadata_pair_delete`.
@@ -4166,7 +4166,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None:
     Free the metadata iterator when you're done using it.
 
     [Rust
-    `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_delete)
+    `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_delete)
     """
     lib.pactffi_message_metadata_iter_delete(iter._ptr)
 
@@ -4176,7 +4176,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None:
     Free a pair of key and value returned from `message_metadata_iter_next`.
 
     [Rust
-    `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_pair_delete)
+    `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_pair_delete)
     """
     lib.pactffi_message_metadata_pair_delete(pair._ptr)
 
@@ -4186,7 +4186,7 @@ def provider_get_name(provider: Provider) -> str:
     Get a copy of this provider's name.
 
     [Rust
-    `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_get_name)
+    `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_get_name)
 
     The copy must be deleted with `pactffi_string_delete`.
 
@@ -4232,7 +4232,7 @@ def pact_get_provider(pact: Pact) -> Provider:
     `pactffi_pact_provider_delete` when no longer required.
 
     [Rust
-    `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_provider)
+    `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_provider)
 
     # Errors
 
@@ -4247,7 +4247,7 @@ def pact_provider_delete(provider: Provider) -> None:
     Frees the memory used by the Pact provider.
 
     [Rust
-    `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_provider_delete)
+    `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_provider_delete)
     """
     raise NotImplementedError
 
@@ -4257,7 +4257,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None:
     Get the name of the provider state as a string.
 
     [Rust
-    `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_name)
+    `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_name)
 
     Raises:
         RuntimeError:
@@ -4277,7 +4277,7 @@ def provider_state_get_param_iter(
     Get an iterator over the params of a provider state.
 
     [Rust
-    `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_param_iter)
+    `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_param_iter)
 
     # Safety
 
@@ -4305,7 +4305,7 @@ def provider_state_param_iter_next(
     Get the next key and value out of the iterator, if possible.
 
     [Rust
-    `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_next)
+    `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_next)
 
     # Safety
 
@@ -4326,7 +4326,7 @@ def provider_state_delete(provider_state: ProviderState) -> None:
     Free the provider state when you're done using it.
 
     [Rust
-    `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_delete)
+    `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_delete)
     """
     raise NotImplementedError
 
@@ -4336,7 +4336,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None:
     Free the provider state param iterator when you're done using it.
 
     [Rust
-    `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_delete)
+    `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_delete)
     """
     lib.pactffi_provider_state_param_iter_delete(iter._ptr)
 
@@ -4346,7 +4346,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None:
     Free a pair of key and value.
 
     [Rust
-    `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_pair_delete)
+    `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_pair_delete)
     """
     lib.pactffi_provider_state_param_pair_delete(pair._ptr)
 
@@ -4356,7 +4356,7 @@ def sync_message_new() -> SynchronousMessage:
     Get a mutable pointer to a newly-created default message on the heap.
 
     [Rust
-    `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_new)
+    `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_new)
 
     # Safety
 
@@ -4374,7 +4374,7 @@ def sync_message_delete(message: SynchronousMessage) -> None:
     Destroy the `Message` being pointed to.
 
     [Rust
-    `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_delete)
+    `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_delete)
     """
     lib.pactffi_sync_message_delete(message._ptr)
 
@@ -4384,7 +4384,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str:
     Get the request contents of a `SynchronousMessage` in string form.
 
     [Rust
-    `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_str)
+    `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_str)
 
     # Safety
 
@@ -4411,7 +4411,7 @@ def sync_message_set_request_contents_str(
     Sets the request contents of the message.
 
     [Rust
-    `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_str)
+    `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_str)
 
     - `message` - the message to set the request contents for
     - `contents` - pointer to contents to copy from. Must be a valid
@@ -4439,7 +4439,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int
     Get the length of the request contents of a `SynchronousMessage`.
 
     [Rust
-    `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_length)
+    `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_length)
 
     # Safety
 
@@ -4458,7 +4458,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes:
     Get the request contents of a `SynchronousMessage` as a bytes.
 
     [Rust
-    `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin)
+    `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin)
 
     # Safety
 
@@ -4485,7 +4485,7 @@ def sync_message_set_request_contents_bin(
     Sets the request contents of the message as an array of bytes.
 
     [Rust
-    `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin)
+    `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin)
 
     * `message` - the message to set the request contents for
     * `contents` - pointer to contents to copy from
@@ -4512,7 +4512,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon
     Get the request contents of an `SynchronousMessage` as a `MessageContents`.
 
     [Rust
-    `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents)
+    `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents)
 
     # Safety
 
@@ -4539,7 +4539,7 @@ def sync_message_generate_request_contents(
     contents as would be received by the consumer.
 
     [Rust
-    `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_request_contents)
+    `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_request_contents)
 
     Raises:
         RuntimeError:
@@ -4557,7 +4557,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int:
     Get the number of response messages in the `SynchronousMessage`.
 
     [Rust
-    `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_number_responses)
+    `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_number_responses)
 
     If the message is null, this function will return 0.
     """
@@ -4572,7 +4572,7 @@ def sync_message_get_response_contents_str(
     Get the response contents of a `SynchronousMessage` in string form.
 
     [Rust
-    `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_str)
+    `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_str)
 
     # Safety
 
@@ -4605,7 +4605,7 @@ def sync_message_set_response_contents_str(
     with default values.
 
     [Rust
-    `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_str)
+    `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_str)
 
     * `message` - the message to set the response contents for
     * `index` - index of the response to set. 0 is the first response.
@@ -4637,7 +4637,7 @@ def sync_message_get_response_contents_length(
     Get the length of the response contents of a `SynchronousMessage`.
 
     [Rust
-    `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_length)
+    `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_length)
 
     # Safety
 
@@ -4659,7 +4659,7 @@ def sync_message_get_response_contents_bin(
     Get the response contents of a `SynchronousMessage` as bytes.
 
     [Rust
-    `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin)
+    `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin)
 
     # Safety
 
@@ -4690,7 +4690,7 @@ def sync_message_set_response_contents_bin(
     responses will be padded with default values.
 
     [Rust
-    `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin)
+    `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin)
 
     * `message` - the message to set the response contents for
     * `index` - index of the response to set. 0 is the first response
@@ -4721,7 +4721,7 @@ def sync_message_get_response_contents(
     Get the response contents of an `SynchronousMessage` as a `MessageContents`.
 
     [Rust
-    `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents)
+    `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents)
 
     # Safety
 
@@ -4750,7 +4750,7 @@ def sync_message_generate_response_contents(
     received by the consumer.
 
     [Rust
-    `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_response_contents)
+    `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_response_contents)
 
     Raises:
         RuntimeError:
@@ -4768,7 +4768,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str:
     Get a copy of the description.
 
     [Rust
-    `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_description)
+    `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_description)
 
     Raises:
         RuntimeError:
@@ -4786,7 +4786,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str)
     Write the `description` field on the `SynchronousMessage`.
 
     [Rust
-    `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_description)
+    `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_description)
 
     # Safety
 
@@ -4811,7 +4811,7 @@ def sync_message_get_provider_state(
     Get a copy of the provider state at the given index from this message.
 
     [Rust
-    `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state)
+    `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state)
 
     # Safety
 
@@ -4837,7 +4837,7 @@ def sync_message_get_provider_state_iter(
     Get an iterator over provider states.
 
     [Rust
-    `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter)
+    `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter)
 
     # Safety
 
@@ -4859,7 +4859,7 @@ def string_delete(string: OwnedString) -> None:
     Delete a string previously returned by this FFI.
 
     [Rust
-    `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_string_delete)
+    `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_string_delete)
     """
     lib.pactffi_string_delete(string._ptr)
 
@@ -4874,7 +4874,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int:
     the mock server is returned.
 
     [Rust
-    `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server)
+    `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server)
 
     * `pact_str` - Pact JSON
     * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80)
@@ -4910,7 +4910,7 @@ def get_tls_ca_certificate() -> OwnedString:
     Fetch the CA Certificate used to generate the self-signed certificate.
 
     [Rust
-    `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_tls_ca_certificate)
+    `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_tls_ca_certificate)
 
     **NOTE:** The string for the result is allocated on the heap, and will have
     to be freed by the caller using pactffi_string_delete.
@@ -4931,7 +4931,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -
     operating system. The port of the mock server is returned.
 
     [Rust
-    `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_pact)
+    `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_pact)
 
     * `pact` - Handle to a Pact model created with created with
       `pactffi_new_pact`.
@@ -4974,7 +4974,7 @@ def create_mock_server_for_transport(
     Create a mock server for the provided Pact handle and transport.
 
     [Rust
-    `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_transport)
+    `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_transport)
 
     Args:
         pact:
@@ -5036,7 +5036,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool:
     if any request has not been successfully matched, or the method panics.
 
     [Rust
-    `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_matched)
+    `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_matched)
     """
     return lib.pactffi_mock_server_matched(mock_server_handle._ref)
 
@@ -5048,7 +5048,7 @@ def mock_server_mismatches(
     External interface to get all the mismatches from a mock server.
 
     [Rust
-    `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_mismatches)
+    `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_mismatches)
 
     # Errors
 
@@ -5075,7 +5075,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None:
     and cleanup any memory allocated for it.
 
     [Rust
-    `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_mock_server)
+    `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_mock_server)
 
     Args:
         mock_server_handle:
@@ -5104,7 +5104,7 @@ def write_pact_file(
     directory to write the file to is passed as the second parameter.
 
     [Rust
-    `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_write_pact_file)
+    `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_write_pact_file)
 
     Args:
         mock_server_handle:
@@ -5156,7 +5156,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str:
     started.
 
     [Rust
-    `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_logs)
+    `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_logs)
 
     Raises:
         RuntimeError:
@@ -5180,7 +5180,7 @@ def generate_datetime_string(format: str) -> StringResult:
     string needs to be freed with the `pactffi_string_delete` function
 
     [Rust
-    `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_datetime_string)
+    `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_datetime_string)
 
     # Safety
 
@@ -5197,7 +5197,7 @@ def check_regex(regex: str, example: str) -> bool:
     Checks that the example string matches the given regex.
 
     [Rust
-    `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_check_regex)
+    `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_check_regex)
 
     # Safety
 
@@ -5216,7 +5216,7 @@ def generate_regex_value(regex: str) -> StringResult:
     `pactffi_string_delete` function.
 
     [Rust
-    `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_regex_value)
+    `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_regex_value)
 
     # Safety
 
@@ -5231,7 +5231,7 @@ def free_string(s: str) -> None:
     [DEPRECATED] Frees the memory allocated to a string by another function.
 
     [Rust
-    `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_string)
+    `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_string)
 
     This function is deprecated. Use `pactffi_string_delete` instead.
 
@@ -5253,7 +5253,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle:
     Creates a new Pact model and returns a handle to it.
 
     [Rust
-    `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_pact)
+    `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_pact)
 
     Args:
         consumer_name:
@@ -5294,7 +5294,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle:
     will result in that interaction being replaced with the new one.
 
     [Rust
-    `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_interaction)
+    `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_interaction)
 
     Args:
         pact:
@@ -5322,7 +5322,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa
     will result in that interaction being replaced with the new one.
 
     [Rust
-    `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_message_interaction)
+    `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_message_interaction)
 
     Args:
         pact:
@@ -5353,7 +5353,7 @@ def new_sync_message_interaction(
     will result in that interaction being replaced with the new one.
 
     [Rust
-    `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_sync_message_interaction)
+    `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_sync_message_interaction)
 
     Args:
         pact:
@@ -5378,7 +5378,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None:
     Sets the description for the Interaction.
 
     [Rust
-    `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_upon_receiving)
+    `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_upon_receiving)
 
     This function
 
@@ -5419,7 +5419,7 @@ def given(interaction: InteractionHandle, description: str) -> None:
     Adds a provider state to the Interaction.
 
     [Rust
-    `pactffi_given`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given)
+    `pactffi_given`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given)
 
     Args:
         interaction:
@@ -5446,7 +5446,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non
     used with V4 interactions.
 
     [Rust
-    `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_test_name)
+    `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_test_name)
 
     Args:
         interaction:
@@ -5493,7 +5493,7 @@ def given_with_param(
     be parsed as JSON.
 
     [Rust
-    `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_param)
+    `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_param)
 
     Args:
         interaction:
@@ -5535,7 +5535,7 @@ def given_with_params(
     with a `value` key.
 
     [Rust
-    `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_params)
+    `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_params)
 
     Args:
         interaction:
@@ -5574,7 +5574,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None
     Configures the request for the Interaction.
 
     [Rust
-    `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_request)
+    `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_request)
 
     Args:
         interaction:
@@ -5588,7 +5588,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None
 
             This may be a simple string in which case it will be used as-is, or
             it may be a [JSON matching
-            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
             which allows regex patterns. For examples:
 
             ```json
@@ -5623,7 +5623,7 @@ def with_query_parameter_v2(
     Configures a query parameter for the Interaction.
 
     [Rust
-    `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_query_parameter_v2)
+    `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_query_parameter_v2)
 
     To setup a query parameter with multiple values, you can either call this
     function multiple times with a different index value:
@@ -5660,7 +5660,7 @@ def with_query_parameter_v2(
     )
     ```
 
-    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
 
     If you want the matching rules to apply to all values (and not just the one
     with the given index), make sure to set the value to be an array.
@@ -5712,7 +5712,7 @@ def with_query_parameter_v2(
 
             This may be a simple string in which case it will be used as-is, or
             it may be a [JSON matching
-            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md).
+            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md).
 
     Raises:
         RuntimeError:
@@ -5734,7 +5734,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None:
     Sets the specification version for a given Pact model.
 
     [Rust
-    `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_specification)
+    `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_specification)
 
     Args:
         pact:
@@ -5758,7 +5758,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification:
     Fetches the Pact specification version for the given Pact model.
 
     [Rust
-    `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_handle_get_pact_spec_version)
+    `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_handle_get_pact_spec_version)
 
     Args:
         handle:
@@ -5784,7 +5784,7 @@ def with_pact_metadata(
     mock server for it has already started)
 
     [Rust
-    `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_pact_metadata)
+    `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_pact_metadata)
 
     Args:
         pact:
@@ -5847,7 +5847,7 @@ def with_metadata(
     ```
 
     See
-    [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+    [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
 
     # Note
 
@@ -5896,7 +5896,7 @@ def with_header_v2(
     r"""
     Configures a header for the Interaction.
 
-    [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_header_v2)
+    [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_header_v2)
 
     To setup a header with multiple values, you can either call this
     function multiple times with a different index value:
@@ -5934,7 +5934,7 @@ def with_header_v2(
     )
     ```
 
-    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
 
     Args:
         interaction:
@@ -5956,7 +5956,7 @@ def with_header_v2(
 
             This may be a simple string in which case it will be used as-is, or
             it may be a [JSON matching
-            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md).
+            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md).
 
     Raises:
         RuntimeError:
@@ -5988,7 +5988,7 @@ def set_header(
     and generators can not be configured with it.
 
     [Rust
-    `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_header)
+    `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_header)
 
     If matching rules are required to be set, use `pactffi_with_header_v2`.
 
@@ -6026,7 +6026,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None:
     Configures the response for the Interaction.
 
     [Rust
-    `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status)
+    `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status)
 
     Args:
         interaction:
@@ -6050,7 +6050,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None:
     Configures the response for the Interaction.
 
     [Rust
-    `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status_v2)
+    `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status_v2)
 
     To include matching rules for the status (only statusCode or integer really
     makes sense to use), include the matching rule JSON format with the value as
@@ -6069,7 +6069,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None:
     )
     ```
 
-    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+    See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
 
     Args:
         interaction:
@@ -6080,7 +6080,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None:
 
             This may be a simple string in which case it will be used as-is, or
             it may be a [JSON matching
-            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md).
+            rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md).
 
     Raises:
         RuntimeError:
@@ -6104,12 +6104,26 @@ def with_body(
     Adds the body for the interaction.
 
     [Rust
-    `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_body)
+    `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_body)
 
-    For HTTP and async message interactions, this will overwrite the body. With
-    asynchronous messages, the part parameter will be ignored. With synchronous
-    messages, the request contents will be overwritten, while a new response
-    will be appended to the message.
+    Returns false if the interaction or Pact can't be modified (i.e. the mock
+    server for it has already started)
+
+    If the `content_type` is determined as follows, whichever is first:
+
+    - The `content_type` argument to this function
+    - The `Content-Type` header for HTTP interaction, or `contentType` metadata
+      entry for message interactions.
+    - From automatic detection of the body contents.
+    - Defaults to `text/plain` as a last resort.
+
+    Furthermore, the `Content-Type` header or `contentType` metadata entry will
+    be updated with the above determined content type, _unless_ it is already
+    set.
+
+    This function will overwrite the body contents if they exist, with the
+    exception of the response part of synchronous message interactions, where a
+    new response will be appended.
 
     Args:
         interaction:
@@ -6117,15 +6131,15 @@ def with_body(
 
         part:
             The part of the interaction to add the body to (Request or
-            Response).
+            Response). This is ignored for asynchronous message interactions.
 
         content_type:
-            The content type of the body. Will be ignored if a content type
-            header is already set.
+            The content type of the body, or `None` to use the internal logic.
 
         body:
             The body contents. For JSON payloads, matching rules can be embedded
-            in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md).
+            in the body. See
+            [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md).
 
     Raises:
         RuntimeError:
@@ -6152,7 +6166,7 @@ def with_binary_body(
     Adds the body for the interaction.
 
     [Rust
-    `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_body)
+    `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_body)
 
     For HTTP and async message interactions, this will overwrite the body. With
     asynchronous messages, the part parameter will be ignored. With synchronous
@@ -6179,7 +6193,23 @@ def with_binary_body(
         RuntimeError:
             If the body could not be modified.
     """
-    raise NotImplementedError
+    if len(gc.get_referrers(body)) == 0:
+        warnings.warn(
+            "Make sure to assign the body to a variable to avoid having the byte array"
+            " modified.",
+            UserWarning,
+            stacklevel=3,
+        )
+    success: bool = lib.pactffi_with_binary_body(
+        interaction._ref,
+        part.value,
+        content_type.encode("utf-8") if content_type else ffi.NULL,
+        body if body else ffi.NULL,
+        len(body) if body else 0,
+    )
+    if not success:
+        msg = f"Unable to set body for {interaction}."
+        raise RuntimeError(msg)
 
 
 def with_binary_file(
@@ -6191,12 +6221,20 @@ def with_binary_file(
     """
     Adds a binary file as the body with the expected content type and contents.
 
+    !!! warning
+
+        This function is deprecated. Use
+        [`with_binary_body`][pact.v3.ffi.with_binary_body] in order to set the
+        binary body, and use
+        [`with_matching_rules`][pact.v3.ffi.with_matching_rules] to set the
+        matching rules to ensure that only the content type is being matched.
+
     Will use a mime type matcher to match the body. Returns false if the
     interaction or Pact can't be modified (i.e. the mock server for it has
     already started)
 
     [Rust
-    `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_file)
+    `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_file)
 
     For HTTP and async message interactions, this will overwrite the body. With
     asynchronous messages, the part parameter will be ignored. With synchronous
@@ -6250,7 +6288,7 @@ def with_matching_rules(
     Add matching rules to the interaction.
 
     [Rust
-    `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_matching_rules)
+    `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_matching_rules)
 
     This function can be called multiple times, in which case the matching
     rules will be merged.
@@ -6288,7 +6326,7 @@ def with_generators(
     Add generators to the interaction.
 
     [Rust
-    `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_generators)
+    `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_generators)
 
     This function can be called multiple times, in which case the generators
     will be combined (provide they don't clash).
@@ -6336,7 +6374,7 @@ def with_multipart_file_v2(  # noqa: PLR0913
     already started) or an error occurs.
 
     [Rust
-    `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file_v2)
+    `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file_v2)
 
     This function can be called multiple times. In that case, each subsequent
     call will be appended to the existing multipart body as a new part.
@@ -6390,7 +6428,7 @@ def with_multipart_file(
     already started) or an error occurs.
 
     [Rust
-    `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file)
+    `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file)
 
     * `interaction` - Interaction handle to set the body for.
     * `part` - Request or response part.
@@ -6427,7 +6465,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None:
     Sets the key attribute for the interaction.
 
     [Rust
-    `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_key)
+    `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_key)
 
     Args:
         interaction:
@@ -6455,7 +6493,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None:
     Mark the interaction as pending.
 
     [Rust
-    `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_pending)
+    `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_pending)
 
     Args:
         interaction:
@@ -6479,7 +6517,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) ->
     Add a comment to the interaction.
 
     [Rust
-    `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_comment)
+    `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_comment)
 
     Args:
         interaction:
@@ -6513,7 +6551,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None:
     Add a text comment to the interaction.
 
     [Rust
-    `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_add_text_comment)
+    `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_add_text_comment)
 
     Args:
         interaction:
@@ -6543,7 +6581,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter
     `pactffi_pact_sync_message_iter_delete`.
 
     [Rust
-    `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter)
+    `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter)
 
     # Safety
 
@@ -6569,7 +6607,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat
     `pactffi_pact_sync_message_iter_delete`.
 
     [Rust
-    `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter)
+    `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter)
 
     # Safety
 
@@ -6595,7 +6633,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator:
     `pactffi_pact_sync_http_iter_delete`.
 
     [Rust
-    `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter)
+    `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter)
 
     # Safety
 
@@ -6621,7 +6659,7 @@ def pact_handle_write_file(
     External interface to write out the pact file.
 
     [Rust
-    `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_write_file)
+    `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_write_file)
 
     This function should be called if all the consumer tests have passed.
 
@@ -6665,7 +6703,7 @@ def free_pact_handle(pact: PactHandle) -> None:
     Delete a Pact handle and free the resources used by it.
 
     [Rust
-    `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_pact_handle)
+    `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_pact_handle)
 
     Raises:
         RuntimeError:
@@ -6685,7 +6723,7 @@ def verify(args: str) -> int:
     """
     External interface to verifier a provider.
 
-    [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verify)
+    [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verify)
 
     * `args` - the same as the CLI interface, except newline delimited
 
@@ -6711,8 +6749,13 @@ def verifier_new_for_application() -> VerifierHandle:
     """
     Get a Handle to a newly created verifier.
 
+    By default, verification results will not be published. To enable
+    publishing, use
+    [`pactffi_verifier_set_publish_options`][pact.v3.ffi.verifier_set_publish_options]
+    to set the required values and enable it.
+
     [Rust
-    `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_new_for_application)
+    `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application)
     """
     from pact import __version__
 
@@ -6727,7 +6770,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None:
     """
     Shutdown the verifier and release all resources.
 
-    [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_shutdown)
+    [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_shutdown)
     """
     lib.pactffi_verifier_shutdown(handle._ref)
 
@@ -6744,7 +6787,7 @@ def verifier_set_provider_info(  # noqa: PLR0913
     Set the provider details for the Pact verifier.
 
     [Rust
-    `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_info)
+    `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_info)
 
     Args:
         handle:
@@ -6790,7 +6833,7 @@ def verifier_add_provider_transport(
     Adds a new transport for the given provider.
 
     [Rust
-    `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_provider_transport)
+    `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_provider_transport)
 
     Args:
         handle:
@@ -6831,7 +6874,7 @@ def verifier_set_filter_info(
     Set the filters for the Pact verifier.
 
     [Rust
-    `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_filter_info)
+    `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_filter_info)
 
     Set filters to narrow down the interactions to verify.
 
@@ -6867,7 +6910,7 @@ def verifier_set_provider_state(
     Set the provider state URL for the Pact verifier.
 
     [Rust
-    `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_state)
+    `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_state)
 
     Args:
         handle:
@@ -6902,7 +6945,7 @@ def verifier_set_verification_options(
     Set the options used by the verifier when calling the provider.
 
     [Rust
-    `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_verification_options)
+    `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_verification_options)
 
     Args:
         handle:
@@ -6937,7 +6980,7 @@ def verifier_set_coloured_output(
     Enables or disables coloured output using ANSI escape codes.
 
     [Rust
-    `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_coloured_output)
+    `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_coloured_output)
 
     By default, coloured output is enabled.
 
@@ -6966,7 +7009,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) ->
     Enables or disables if no pacts are found to verify results in an error.
 
     [Rust
-    `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error)
+    `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error)
 
     Args:
         handle:
@@ -6999,7 +7042,7 @@ def verifier_set_publish_options(
     Set the options used when publishing verification results to the Broker.
 
     [Rust
-    `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_publish_options)
+    `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_publish_options)
 
     Args:
         handle:
@@ -7042,7 +7085,7 @@ def verifier_set_consumer_filters(
     Set the consumer filters for the Pact verifier.
 
     [Rust
-    `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_consumer_filters)
+    `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_consumer_filters)
     """
     lib.pactffi_verifier_set_consumer_filters(
         handle._ref,
@@ -7060,7 +7103,7 @@ def verifier_add_custom_header(
     Adds a custom header to be added to the requests made to the provider.
 
     [Rust
-    `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_custom_header)
+    `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_custom_header)
     """
     lib.pactffi_verifier_add_custom_header(
         handle._ref,
@@ -7074,7 +7117,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None:
     Adds a Pact file as a source to verify.
 
     [Rust
-    `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_file_source)
+    `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_file_source)
     """
     lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8"))
 
@@ -7086,7 +7129,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non
     All pacts from the directory that match the provider name will be verified.
 
     [Rust
-    `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_directory_source)
+    `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_directory_source)
 
     # Safety
 
@@ -7108,7 +7151,7 @@ def verifier_url_source(
     Adds a URL as a source to verify.
 
     [Rust
-    `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_url_source)
+    `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_url_source)
 
     Args:
         handle:
@@ -7148,7 +7191,7 @@ def verifier_broker_source(
     Adds a Pact broker as a source to verify.
 
     [Rust
-    `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source)
+    `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source)
 
     This will fetch all the pact files from the broker that match the provider
     name.
@@ -7196,12 +7239,16 @@ def verifier_broker_source_with_selectors(  # noqa: PLR0913
     Adds a Pact broker as a source to verify.
 
     [Rust
-    `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors)
+    `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors)
 
     This will fetch all the pact files from the broker that match the provider
     name and the consumer version selectors (See [Consumer Version
     Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)).
 
+    If a username and password is given, then basic authentication will be used
+    when fetching the pact file. If a token is provided, then bearer token
+    authentication will be used.
+
     Args:
         handle:
             The verifier handle to update.
@@ -7233,11 +7280,12 @@ def verifier_broker_source_with_selectors(  # noqa: PLR0913
 
         consumer_version_selectors:
             The consumer version selectors to use to filter the consumer pacts.
+            This must be passed in as a JSON string.
 
         consumer_version_tags:
             The tags to use to filter the consumer pacts.
     """
-    lib.pactffi_verifier_broker_source_with_selectors(
+    ret: int = lib.pactffi_verifier_broker_source_with_selectors(
         handle._ref,
         url.encode("utf-8"),
         username.encode("utf-8") if username else ffi.NULL,
@@ -7257,13 +7305,20 @@ def verifier_broker_source_with_selectors(  # noqa: PLR0913
         [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags],
         len(consumer_version_tags),
     )
+    if ret == 0:
+        return
+    if ret == -1:
+        msg = "Invalid version selector JSON."
+        raise ValueError(msg)
+    msg = "Unknown error adding broker source with selectors."
+    raise RuntimeError(msg)
 
 
 def verifier_execute(handle: VerifierHandle) -> None:
     """
     Runs the verification.
 
-    (https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_execute)
+    (https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_execute)
 
     Raises:
         RuntimeError:
@@ -7283,7 +7338,7 @@ def verifier_cli_args() -> str:
     string.
 
     [Rust
-    `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_cli_args)
+    `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_cli_args)
 
     The purpose is to then be able to use in other languages which wrap the FFI
     library, to implement the same CLI functionality automatically without
@@ -7339,7 +7394,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString:
     Extracts the logs for the verification run.
 
     [Rust
-    `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs)
+    `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs)
 
     This needs the memory buffer log sink to be setup before the verification is
     executed. The returned string will need to be freed with the `free_string`
@@ -7361,7 +7416,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString:
     Extracts the logs for the verification run for the provider name.
 
     [Rust
-    `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs_for_provider)
+    `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs_for_provider)
 
     This needs the memory buffer log sink to be setup before the verification is
     executed. The returned string will need to be freed with the `free_string`
@@ -7383,7 +7438,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString:
     Extracts the standard output for the verification run.
 
     [Rust
-    `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_output)
+    `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_output)
 
     Args:
         handle:
@@ -7410,7 +7465,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString:
     Extracts the verification result as a JSON document.
 
     [Rust
-    `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_json)
+    `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_json)
 
     Raises:
         RuntimeError:
@@ -7438,7 +7493,7 @@ def using_plugin(
     otherwise you will have plugin processes left running.
 
     [Rust
-    `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_using_plugin)
+    `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_using_plugin)
 
     Args:
         pact:
@@ -7481,7 +7536,7 @@ def cleanup_plugins(pact: PactHandle) -> None:
     zero).
 
     [Rust
-    `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_plugins)
+    `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_plugins)
     """
     lib.pactffi_cleanup_plugins(pact._ref)
 
@@ -7500,7 +7555,7 @@ def interaction_contents(
     format of the JSON contents.
 
     [Rust
-    `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_contents)
+    `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_contents)
 
     Args:
         interaction:
@@ -7560,7 +7615,7 @@ def matches_string_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_string_value)
+    `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_string_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get as a NULL terminated string
@@ -7591,7 +7646,7 @@ def matches_u64_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_u64_value)
+    `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_u64_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get
@@ -7621,7 +7676,7 @@ def matches_i64_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_i64_value)
+    `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_i64_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get
@@ -7651,7 +7706,7 @@ def matches_f64_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_f64_value)
+    `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_f64_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get
@@ -7681,7 +7736,7 @@ def matches_bool_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_bool_value)
+    `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_bool_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get, 0 == false and 1 == true
@@ -7713,7 +7768,7 @@ def matches_binary_value(  # noqa: PLR0913
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_binary_value)
+    `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_binary_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get
@@ -7748,7 +7803,7 @@ def matches_json_value(
     function once it is no longer required.
 
     [Rust
-    `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_json_value)
+    `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_json_value)
 
     * matching_rule - pointer to a matching rule
     * expected_value - value we expect to get as a NULL terminated string
diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py
index 9696f37a33..c920d1b238 100644
--- a/src/pact/v3/interaction/_base.py
+++ b/src/pact/v3/interaction/_base.py
@@ -300,7 +300,7 @@ def with_binary_body(
             body:
                 Body of the request.
         """
-        pact.v3.ffi.with_binary_file(
+        pact.v3.ffi.with_binary_body(
             self._handle,
             self._parse_interaction_part(part),
             content_type,
diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py
index 1f00bf1946..d7b5d5f8f7 100644
--- a/src/pact/v3/interaction/_http_interaction.py
+++ b/src/pact/v3/interaction/_http_interaction.py
@@ -149,7 +149,7 @@ def with_header(
         # JSON Matching
 
         Pact's matching rules are defined in the [upstream
-        documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md)
+        documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md)
         and support a wide range of matching rules. These can be specified
         using a JSON object as a strong using `json.dumps(...)`. For example,
         the above rule whereby the `X-Foo` header has multiple values can be
@@ -391,7 +391,7 @@ def with_query_parameter(self, name: str, value: str) -> Self:
         ```
 
         For more information on the format of the JSON object, see the [upstream
-        documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md).
+        documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md).
 
         Args:
             name:


Test patch:
diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py
new file mode 100644
index 0000000000..0bc9aad22f
--- /dev/null
+++ b/examples/tests/v3/provider_server.py
@@ -0,0 +1,253 @@
+"""
+HTTP Server to route message requests to message producer function.
+"""
+
+from __future__ import annotations
+
+import logging
+import re
+import signal
+import socket
+import subprocess
+import sys
+import time
+from contextlib import closing, contextmanager
+from importlib import import_module
+from pathlib import Path
+from threading import Thread
+from typing import Generator, NoReturn, Tuple
+
+import requests
+
+sys.path.append(str(Path(__file__).parent.parent.parent.parent))
+
+import flask
+from yarl import URL
+
+logger = logging.getLogger(__name__)
+
+
+class Provider:
+    """
+    Provider class to route message requests to message producer function.
+
+    Sets up three endpoints:
+        - /_test/ping: A simple ping endpoint for testing.
+        - /produce_message: Route message requests to the handler function.
+        - /set_provider_state: Set the provider state.
+
+    The specific `produce_message` and `set_provider_state` URLs can be configured
+    with the `produce_message_url` and `set_provider_state_url` arguments.
+    """
+
+    def __init__(  # noqa: PLR0913
+        self,
+        handler_module: str,
+        handler_function: str,
+        produce_message_url: str,
+        state_provider_module: str,
+        state_provider_function: str,
+        set_provider_state_url: str,
+    ) -> None:
+        """
+        Initialize the provider.
+
+        Args:
+            handler_module:
+                The name of the module containing the handler function.
+            handler_function:
+                The name of the handler function.
+            produce_message_url:
+                The URL to route message requests to the handler function.
+            state_provider_module:
+                The name of the module containing the state provider setup function.
+            state_provider_function:
+                The name of the state provider setup function.
+            set_provider_state_url:
+                The URL to set the provider state.
+        """
+        self.app = flask.Flask("Provider")
+        self.handler_function = getattr(import_module(handler_module), handler_function)
+        self.produce_message_url = produce_message_url
+        self.set_provider_state_url = set_provider_state_url
+        if state_provider_module:
+            self.state_provider_function = getattr(
+                import_module(state_provider_module), state_provider_function
+            )
+
+        @self.app.get("/_test/ping")
+        def ping() -> str:
+            """Simple ping endpoint for testing."""
+            return "pong"
+
+        @self.app.route(self.produce_message_url, methods=["POST"])
+        def produce_message() -> flask.Response | Tuple[str, int]:
+            """
+            Route a message request to the handler function.
+
+            Returns:
+                The response from the handler function.
+            """
+            try:
+                body, content_type = self.handler_function()
+                return flask.Response(
+                    response=body,
+                    status=200,
+                    content_type=content_type,
+                    direct_passthrough=True,
+                )
+            except Exception as e:  # noqa: BLE001
+                return str(e), 500
+
+        @self.app.route(self.set_provider_state_url, methods=["POST"])
+        def set_provider_state() -> Tuple[str, int]:
+            """
+            Calls the state provider function with the state provided in the request.
+
+            Returns:
+                A response indicating that the state has been set.
+            """
+            if self.state_provider_function:
+                self.state_provider_function(flask.request.args["state"])
+            return "Provider state set", 200
+
+    def _find_free_port(self) -> int:
+        """
+        Find a free port.
+
+        This is used to find a free port to host the API on when running locally. It
+        is allocated, and then released immediately so that it can be used by the
+        API.
+
+        Returns:
+            The port number.
+        """
+        with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+            s.bind(("", 0))
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            return s.getsockname()[1]
+
+    def run(self) -> None:
+        """
+        Start the provider.
+        """
+        url = URL(f"http://localhost:{self._find_free_port()}")
+        sys.stderr.write(f"Starting provider on {url}\n")
+
+        self.app.run(
+            host=url.host,
+            port=url.port,
+            debug=True,
+        )
+
+
+@contextmanager
+def start_provider(**kwargs: str) -> Generator[URL, None, None]:  # noqa: C901
+    """
+    Start the provider app.
+
+    Expects kwargs to to contain the following:
+        handler_module: Required. The name of the module containing
+                        the handler function.
+        handler_function: Required. The name of the handler function.
+        produce_message_url: Optional. The URL to route message requests to
+                             the handler function.
+        state_provider_module: Optional. The name of the module containing
+                               the state provider setup function.
+        state_provider_function: Optional. The name of the state provider
+                                 setup function.
+        set_provider_state_url: Optional. The URL to set the provider state.
+    """
+    process = subprocess.Popen(  # noqa: S603
+        [
+            sys.executable,
+            Path(__file__),
+            kwargs.pop("handler_module"),
+            kwargs.pop("handler_function"),
+            kwargs.pop("produce_message_url", "/produce_message"),
+            kwargs.pop("state_provider_module", ""),
+            kwargs.pop("state_provider_function", ""),
+            kwargs.pop("set_provider_state_url", "/set_provider_state"),
+        ],
+        cwd=Path.cwd(),
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        encoding="utf-8",
+    )
+
+    pattern = re.compile(r" \* Running on (?P<url>[^ ]+)")
+    while True:
+        if process.poll() is not None:
+            logger.error("Provider process exited with code %d", process.returncode)
+            logger.error(
+                "Provider stdout: %s", process.stdout.read() if process.stdout else ""
+            )
+            logger.error(
+                "Provider stderr: %s", process.stderr.read() if process.stderr else ""
+            )
+            msg = f"Provider process exited with code {process.returncode}"
+            raise RuntimeError(msg)
+        if (
+            process.stderr
+            and (line := process.stderr.readline())
+            and (match := pattern.match(line))
+        ):
+            break
+        time.sleep(0.1)
+
+    url = URL(match.group("url"))
+    logger.debug("Provider started on %s", url)
+    for _ in range(50):
+        try:
+            response = requests.get(str(url / "_test" / "ping"), timeout=1)
+            assert response.text == "pong"
+            break
+        except (requests.RequestException, AssertionError):
+            time.sleep(0.1)
+            continue
+    else:
+        msg = "Failed to ping provider"
+        raise RuntimeError(msg)
+
+    def redirect() -> NoReturn:
+        while True:
+            if process.stdout:
+                while line := process.stdout.readline():
+                    logger.debug("Provider stdout: %s", line.strip())
+            if process.stderr:
+                while line := process.stderr.readline():
+                    logger.debug("Provider stderr: %s", line.strip())
+
+    thread = Thread(target=redirect, daemon=True)
+    thread.start()
+
+    try:
+        yield url
+    finally:
+        process.send_signal(signal.SIGINT)
+
+
+if __name__ == "__main__":
+    import sys
+
+    if len(sys.argv) < 5:  # noqa: PLR2004
+        sys.stderr.write(
+            f"Usage: {sys.argv[0]} <state_provider_module> <state_provider_function> "
+            f"<handler_module> <handler_function>"
+        )
+        sys.exit(1)
+
+    handler_module = sys.argv[1]
+    handler_function = sys.argv[2]
+    produce_message_url = sys.argv[3]
+    state_provider_module = sys.argv[4]
+    state_provider_function = sys.argv[5]
+    set_provider_state_url = sys.argv[6]
+    Provider(
+        handler_module,
+        handler_function,
+        produce_message_url,
+        state_provider_module,
+        state_provider_function,
+        set_provider_state_url,
+    ).run()
diff --git a/examples/tests/v3/test_01_message_consumer.py b/examples/tests/v3/test_01_message_consumer.py
index 9016be9b8b..76f58c71a7 100644
--- a/examples/tests/v3/test_01_message_consumer.py
+++ b/examples/tests/v3/test_01_message_consumer.py
@@ -65,7 +65,7 @@ def pact() -> Generator[Pact, None, None]:
 
     ```
     """
-    pact_dir = Path(Path(__file__).parent.parent / "pacts")
+    pact_dir = Path(Path(__file__).parent.parent.parent / "pacts")
     pact = Pact("v3_message_consumer", "v3_message_provider")
     log.info("Creating Message Pact with V3 specification")
     yield pact.with_specification("V3")
@@ -136,7 +136,21 @@ def test_async_message_handler_write(
                 "action": "WRITE",
                 "path": "my_file.txt",
                 "contents": "Hello, world!",
-            })
+            }),
+            "application/json",
+        )
+        .with_matching_rules(
+            {
+                "body": {
+                    "$.path": {
+                        "combine": "AND",
+                        "matchers": [
+                            {"match": "type"},
+                        ],
+                    }
+                }
+            },
+            "Response",
         )
     )
     pact.verify(verifier, "Async")
@@ -161,9 +175,19 @@ def test_async_message_handler_read(
             json.dumps({
                 "action": "READ",
                 "path": "my_file.txt",
-                "contents": "Hello, world!",
-            })
+            }),
+            "application/json",
         )
+        .with_matching_rules({
+            "body": {
+                "$.path": {
+                    "combine": "AND",
+                    "matchers": [
+                        {"match": "type"},
+                    ],
+                },
+            }
+        })
     )
     pact.verify(verifier, "Async")
 
diff --git a/examples/tests/v3/test_02_message_provider.py b/examples/tests/v3/test_02_message_provider.py
new file mode 100644
index 0000000000..472829c1e7
--- /dev/null
+++ b/examples/tests/v3/test_02_message_provider.py
@@ -0,0 +1,74 @@
+"""
+Producer test of example message.
+
+This test will read a pact between the message handler and the message provider
+and then validate the pact against the provider.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Tuple
+from unittest.mock import MagicMock
+
+from examples.src.message_producer import FileSystemMessageProducer
+from examples.tests.v3.provider_server import start_provider
+from pact.v3 import Verifier
+
+PACT_DIR = (Path(__file__).parent.parent.parent / "pacts").resolve()
+
+responses: dict[str, dict[str, str]] = {
+    "a request to write test.txt": {
+        "function_name": "send_write_event",
+    },
+    "a request to read test.txt": {
+        "function_name": "send_read_event",
+    },
+}
+
+CURRENT_STATE: str | None = None
+
+
+def message_producer_function() -> Tuple[str, str]:
+    producer = FileSystemMessageProducer()
+    producer.queue = MagicMock()
+
+    assert CURRENT_STATE is not None, "State is not set"
+    function_name = responses.get(CURRENT_STATE, {}).get("function_name")
+    assert function_name is not None, "Function name could not be found"
+    producer_function = getattr(producer, function_name)
+
+    if producer_function.__name__ == "send_write_event":
+        producer_function("provider_file_name.txt", "Hello, world!")
+    elif producer_function.__name__ == "send_read_event":
+        producer_function("provider_file_name.txt")
+
+    return producer.queue.send.call_args[0][0], "application/json"
+
+
+def state_provider_function(state_name: str) -> None:
+    global CURRENT_STATE  # noqa: PLW0603
+    CURRENT_STATE = state_name
+
+
+def test_producer() -> None:
+    """
+    Test the message producer.
+    """
+    with start_provider(
+        handler_module=__name__,
+        handler_function="message_producer_function",
+        state_provider_module=__name__,
+        state_provider_function="state_provider_function",
+    ) as provider_url:
+        verifier = (
+            Verifier()
+            .set_state(
+                provider_url / "set_provider_state",
+                teardown=True,
+            )
+            .set_info("provider", url=f"{provider_url}/produce_message")
+            .filter_consumers("v3_message_consumer")
+            .add_source(PACT_DIR / "v3_message_consumer-v3_message_provider.json")
+        )
+        verifier.verify()
diff --git a/tests/v3/compatibility_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py
index 75c7b07d2c..8f222cbf52 100644
--- a/tests/v3/compatibility_suite/test_v1_consumer.py
+++ b/tests/v3/compatibility_suite/test_v1_consumer.py
@@ -6,10 +6,7 @@
 
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
-    InteractionDefinition,
-    parse_markdown_table,
-)
+from tests.v3.compatibility_suite.util import parse_markdown_table
 from tests.v3.compatibility_suite.util.consumer import (
     a_response_is_returned,
     request_n_is_made_to_the_mock_server,
@@ -34,6 +31,9 @@
     the_pact_test_is_done,
     the_payload_will_contain_the_json_document,
 )
+from tests.v3.compatibility_suite.util.interaction_definition import (
+    InteractionDefinition,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -320,7 +320,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in content:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
     return interactions
 
 
diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py
index 1078f714a4..60654a6291 100644
--- a/tests/v3/compatibility_suite/test_v1_provider.py
+++ b/tests/v3/compatibility_suite/test_v1_provider.py
@@ -10,9 +10,9 @@
 import pytest
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
+from tests.v3.compatibility_suite.util import parse_markdown_table
+from tests.v3.compatibility_suite.util.interaction_definition import (
     InteractionDefinition,
-    parse_markdown_table,
 )
 from tests.v3.compatibility_suite.util.provider import (
     a_failed_verification_result_will_be_published_back,
@@ -407,7 +407,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in content:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
     return interactions
 
 
diff --git a/tests/v3/compatibility_suite/test_v2_consumer.py b/tests/v3/compatibility_suite/test_v2_consumer.py
index cd1447cfef..b2b6e31011 100644
--- a/tests/v3/compatibility_suite/test_v2_consumer.py
+++ b/tests/v3/compatibility_suite/test_v2_consumer.py
@@ -6,10 +6,7 @@
 
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
-    InteractionDefinition,
-    parse_markdown_table,
-)
+from tests.v3.compatibility_suite.util import parse_markdown_table
 from tests.v3.compatibility_suite.util.consumer import (
     a_response_is_returned,
     request_n_is_made_to_the_mock_server,
@@ -35,6 +32,9 @@
     the_pact_test_is_done,
     the_payload_will_contain_the_json_document,
 )
+from tests.v3.compatibility_suite.util.interaction_definition import (
+    InteractionDefinition,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -196,7 +196,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in table:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
 
     return interactions
 
diff --git a/tests/v3/compatibility_suite/test_v2_provider.py b/tests/v3/compatibility_suite/test_v2_provider.py
index 94313bae29..f891b60d11 100644
--- a/tests/v3/compatibility_suite/test_v2_provider.py
+++ b/tests/v3/compatibility_suite/test_v2_provider.py
@@ -10,9 +10,9 @@
 import pytest
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
+from tests.v3.compatibility_suite.util import parse_markdown_table
+from tests.v3.compatibility_suite.util.interaction_definition import (
     InteractionDefinition,
-    parse_markdown_table,
 )
 from tests.v3.compatibility_suite.util.provider import (
     a_pact_file_for_interaction_is_to_be_verified,
@@ -125,7 +125,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in content:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
     return interactions
 
 
diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py
new file mode 100644
index 0000000000..b4eab3ee5d
--- /dev/null
+++ b/tests/v3/compatibility_suite/test_v3_message_producer.py
@@ -0,0 +1,443 @@
+"""V3 Message provider feature tests."""
+
+from __future__ import annotations
+
+import json
+import logging
+import pickle
+import re
+import sys
+from pathlib import Path
+from typing import TYPE_CHECKING, Generator
+
+import pytest
+from pytest_bdd import (
+    given,
+    parsers,
+    scenario,
+)
+
+from pact.v3.pact import Pact
+from tests.v3.compatibility_suite.util import (
+    parse_horizontal_markdown_table,
+    parse_markdown_table,
+)
+from tests.v3.compatibility_suite.util.interaction_definition import (
+    InteractionDefinition,
+    InteractionState,
+)
+from tests.v3.compatibility_suite.util.provider import (
+    a_pact_file_for_message_is_to_be_verified,
+    a_provider_is_started_that_can_generate_the_message,
+    a_provider_state_callback_is_configured,
+    start_provider,
+    the_provider_state_callback_will_be_called_after_the_verification_is_run,
+    the_provider_state_callback_will_be_called_before_the_verification_is_run,
+    the_provider_state_callback_will_receive_a_setup_call,
+    the_verification_is_run,
+    the_verification_results_will_contain_a_error,
+    the_verification_will_be_successful,
+)
+
+if TYPE_CHECKING:
+    from yarl import URL
+
+    from pact.v3.verifier import Verifier
+
+TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts")
+
+logger = logging.getLogger(__name__)
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Incorrect message is generated by the provider",
+)
+def test_incorrect_message_is_generated_by_the_provider() -> None:
+    """Incorrect message is generated by the provider."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with JSON body (negative case)",
+)
+def test_message_with_json_body_negative_case() -> None:
+    """Message with JSON body (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with JSON body (positive case)",
+)
+def test_message_with_json_body_positive_case() -> None:
+    """Message with JSON body (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with XML body (negative case)",
+)
+def test_message_with_xml_body_negative_case() -> None:
+    """Message with XML body (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with XML body (positive case)",
+)
+def test_message_with_xml_body_positive_case() -> None:
+    """Message with XML body (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with binary body (negative case)",
+)
+def test_message_with_binary_body_negative_case() -> None:
+    """Message with binary body (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with binary body (positive case)",
+)
+def test_message_with_binary_body_positive_case() -> None:
+    """Message with binary body (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with plain text body (negative case)",
+)
+def test_message_with_plain_text_body_negative_case() -> None:
+    """Message with plain text body (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Message with plain text body (positive case)",
+)
+def test_message_with_plain_text_body_positive_case() -> None:
+    """Message with plain text body (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Supports matching rules for the message body (negative case)",
+)
+def test_supports_matching_rules_for_the_message_body_negative_case() -> None:
+    """Supports matching rules for the message body (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Supports matching rules for the message body (positive case)",
+)
+def test_supports_matching_rules_for_the_message_body_positive_case() -> None:
+    """Supports matching rules for the message body (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Supports matching rules for the message metadata (negative case)",
+)
+def test_supports_matching_rules_for_the_message_metadata_negative_case() -> None:
+    """Supports matching rules for the message metadata (negative case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Supports matching rules for the message metadata (positive case)",
+)
+def test_supports_matching_rules_for_the_message_metadata_positive_case() -> None:
+    """Supports matching rules for the message metadata (positive case)."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@pytest.mark.skip("Currently unable to implement")
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Supports messages with body formatted for the Kafka schema registry",
+)
+def test_supports_messages_with_body_formatted_for_the_kafka_schema_registry() -> None:
+    """Supports messages with body formatted for the Kafka schema registry."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Verifies the message metadata",
+)
+def test_verifies_the_message_metadata() -> None:
+    """Verifies the message metadata."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Verifying a simple message",
+)
+def test_verifying_a_simple_message() -> None:
+    """Verifying a simple message."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Verifying an interaction with a defined provider state",
+)
+def test_verifying_an_interaction_with_a_defined_provider_state() -> None:
+    """Verifying an interaction with a defined provider state."""
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V3/message_provider.feature",
+    "Verifying multiple Pact files",
+)
+def test_verifying_multiple_pact_files() -> None:
+    """Verifying multiple Pact files."""
+
+
+################################################################################
+## Given
+################################################################################
+
+
+a_provider_is_started_that_can_generate_the_message()
+a_provider_state_callback_is_configured()
+a_pact_file_for_message_is_to_be_verified("V3")
+
+
+@given(
+    parsers.re(
+        r'a Pact file for "(?P<name>[^"]+)" is to be verified with the following:\n'
+        r"(?P<table>.+)",
+        re.DOTALL,
+    ),
+    converters={"table": parse_horizontal_markdown_table},
+)
+def a_pact_file_for_is_to_be_verified_with_the_following(
+    verifier: Verifier,
+    temp_dir: Path,
+    name: str,
+    table: dict[str, str | dict[str, str]],
+) -> None:
+    """
+    A Pact file for "basic" is to be verified with the following.
+    """
+    pact = Pact("consumer", "provider")
+    pact.with_specification("V3")
+
+    if "metadata" in table:
+        assert isinstance(table["metadata"], str)
+        metadata = {
+            k: json.loads(v.replace("JSON: ", "")) if v.startswith("JSON: ") else v
+            for k, _, v in (s.partition("=") for s in table["metadata"].split("; "))
+        }
+        table["metadata"] = metadata
+
+    interaction_definition = InteractionDefinition(
+        type="Async",
+        description=name,
+        **table,  # type: ignore[arg-type]
+    )
+    interaction_definition.add_to_pact(pact, name)
+    (temp_dir / "pacts").mkdir(exist_ok=True, parents=True)
+    pact.write_file(temp_dir / "pacts")
+    verifier.add_source(temp_dir / "pacts")
+
+
+@given(
+    parsers.re(
+        r'a Pact file for "(?P<name>[^"]+)":"(?P<fixture>[^"]+)"'
+        r' is to be verified with provider state "(?P<provider_state>[^"]+)"'
+    )
+)
+def a_pact_file_for_is_to_be_verified_with_provider_state(
+    temp_dir: Path,
+    verifier: Verifier,
+    name: str,
+    fixture: str,
+    provider_state: str,
+) -> None:
+    """A Pact file is to be verified with provider state."""
+    pact = Pact("consumer", "provider")
+    pact.with_specification("V3")
+    interaction_definition = InteractionDefinition(
+        type="Async",
+        description=name,
+        body=fixture,
+    )
+    interaction_definition.states = [InteractionState(provider_state)]
+    interaction_definition.add_to_pact(pact, name)
+    (temp_dir / "pacts").mkdir(exist_ok=True, parents=True)
+    pact.write_file(temp_dir / "pacts")
+    verifier.add_source(temp_dir / "pacts")
+    with (temp_dir / "provider_states").open("w") as f:
+        logger.debug("Writing provider state to %s", temp_dir / "provider_states")
+        json.dump([s.as_dict() for s in [InteractionState(provider_state)]], f)
+
+
+@given(
+    parsers.re(
+        r'a Pact file for "(?P<name>[^"]+)":"(?P<fixture>[^"]+)" is '
+        "to be verified with the following metadata:\n"
+        r"(?P<metadata>.+)",
+        re.DOTALL,
+    ),
+    converters={"metadata": parse_markdown_table},
+)
+def a_pact_file_for_is_to_be_verified_with_the_following_metadata(
+    temp_dir: Path,
+    verifier: Verifier,
+    name: str,
+    fixture: str,
+    metadata: list[dict[str, str]],
+) -> None:
+    """A Pact file is to be verified with the following metadata."""
+    pact = Pact("consumer", "provider")
+    pact.with_specification("V3")
+    interaction_definition = InteractionDefinition(
+        type="Async",
+        description=name,
+        body=fixture,
+        metadata={
+            row["key"]: json.loads(row["value"].replace("JSON: ", ""))
+            if row["value"].startswith("JSON: ")
+            else row["value"]
+            for row in metadata
+        },
+    )
+    interaction_definition.add_to_pact(pact, name)
+    (temp_dir / "pacts").mkdir(exist_ok=True, parents=True)
+    pact.write_file(temp_dir / "pacts")
+    verifier.add_source(temp_dir / "pacts")
+
+
+@given(
+    parsers.re(
+        r'a provider is started that can generate the "(?P<name>[^"]+)" '
+        r'message with "(?P<fixture>[^"]+)" and the following metadata:\n'
+        r"(?P<metadata>.+)",
+        re.DOTALL,
+    ),
+    converters={"metadata": parse_markdown_table},
+    target_fixture="provider_url",
+)
+def a_provider_is_started_that_can_generate_the_message_with_the_following_metadata(
+    temp_dir: Path,
+    name: str,
+    fixture: str,
+    metadata: list[dict[str, str]],
+) -> Generator[URL, None, None]:
+    """A provider is started that can generate the message with the following metadata."""  # noqa: E501
+    interaction_definitions: list[InteractionDefinition] = []
+    if (temp_dir / "interactions.pkl").exists():
+        with (temp_dir / "interactions.pkl").open("rb") as pkl_file:
+            interaction_definitions = pickle.load(pkl_file)  # noqa: S301
+
+    interaction_definition = InteractionDefinition(
+        type="Async",
+        description=name,
+        body=fixture,
+        metadata={
+            row["key"]: json.loads(row["value"].replace("JSON: ", ""))
+            if row["value"].startswith("JSON: ")
+            else row["value"]
+            for row in metadata
+        },
+    )
+    interaction_definitions.append(interaction_definition)
+
+    with (temp_dir / "interactions.pkl").open("wb") as pkl_file:
+        pickle.dump(interaction_definitions, pkl_file)
+
+    yield from start_provider(temp_dir)
+
+
+################################################################################
+## When
+################################################################################
+
+
+the_verification_is_run()
+
+
+################################################################################
+## Then
+################################################################################
+
+
+the_provider_state_callback_will_be_called_before_the_verification_is_run()
+the_provider_state_callback_will_be_called_after_the_verification_is_run()
+the_provider_state_callback_will_receive_a_setup_call()
+the_verification_will_be_successful()
+the_verification_results_will_contain_a_error()
diff --git a/tests/v3/compatibility_suite/test_v3_provider.py b/tests/v3/compatibility_suite/test_v3_provider.py
index eaa452a45e..b3f6442ea7 100644
--- a/tests/v3/compatibility_suite/test_v3_provider.py
+++ b/tests/v3/compatibility_suite/test_v3_provider.py
@@ -10,9 +10,9 @@
 import pytest
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
+from tests.v3.compatibility_suite.util import parse_markdown_table
+from tests.v3.compatibility_suite.util.interaction_definition import (
     InteractionDefinition,
-    parse_markdown_table,
 )
 from tests.v3.compatibility_suite.util.provider import (
     a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined,
@@ -100,7 +100,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in content:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
     return interactions
 
 
diff --git a/tests/v3/compatibility_suite/test_v4_message_provider.py b/tests/v3/compatibility_suite/test_v4_message_provider.py
new file mode 100644
index 0000000000..d5b337fa65
--- /dev/null
+++ b/tests/v3/compatibility_suite/test_v4_message_provider.py
@@ -0,0 +1,88 @@
+"""
+Basic HTTP provider feature test.
+"""
+
+from __future__ import annotations
+
+import logging
+import sys
+
+import pytest
+from pytest_bdd import scenario
+
+from tests.v3.compatibility_suite.util.provider import (
+    a_pact_file_for_message_is_to_be_verified,
+    a_pact_file_for_message_is_to_be_verified_with_comments,
+    a_provider_is_started_that_can_generate_the_message,
+    the_comment_will_have_been_printed_to_the_console,
+    the_name_of_the_test_will_be_displayed_as_the_original_test_name,
+    the_verification_is_run,
+    the_verification_results_will_contain_a_error,
+    the_verification_will_be_successful,
+    there_will_be_a_pending_error,
+)
+
+logger = logging.getLogger(__name__)
+
+
+################################################################################
+## Scenario
+################################################################################
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V4/message_provider.feature",
+    "Verifying a pending message interaction",
+)
+def test_verifying_a_pending_message_interaction() -> None:
+    """
+    Verifying a pending message interaction.
+    """
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith("win"),
+    reason="See pact-foundation/pact-python#639",
+)
+@scenario(
+    "definition/features/V4/message_provider.feature",
+    "Verifying a message interaction with comments",
+)
+def test_verifying_a_message_interaction_with_comments() -> None:
+    """
+    Verifying a message interaction with comments.
+    """
+
+
+################################################################################
+## Given
+################################################################################
+
+
+a_provider_is_started_that_can_generate_the_message()
+a_pact_file_for_message_is_to_be_verified("V4")
+a_pact_file_for_message_is_to_be_verified_with_comments("V4")
+
+
+################################################################################
+## When
+################################################################################
+
+
+the_verification_is_run()
+
+
+################################################################################
+## Then
+################################################################################
+
+
+the_comment_will_have_been_printed_to_the_console()
+the_name_of_the_test_will_be_displayed_as_the_original_test_name()
+the_verification_results_will_contain_a_error()
+the_verification_will_be_successful()
+there_will_be_a_pending_error()
diff --git a/tests/v3/compatibility_suite/test_v4_provider.py b/tests/v3/compatibility_suite/test_v4_provider.py
index bffc68f743..5f3ba44823 100644
--- a/tests/v3/compatibility_suite/test_v4_provider.py
+++ b/tests/v3/compatibility_suite/test_v4_provider.py
@@ -10,9 +10,9 @@
 import pytest
 from pytest_bdd import given, parsers, scenario
 
-from tests.v3.compatibility_suite.util import (
+from tests.v3.compatibility_suite.util import parse_markdown_table
+from tests.v3.compatibility_suite.util.interaction_definition import (
     InteractionDefinition,
-    parse_markdown_table,
 )
 from tests.v3.compatibility_suite.util.provider import (
     a_pact_file_for_interaction_is_to_be_verified,
@@ -104,7 +104,7 @@ def the_following_http_interactions_have_been_defined(
     # Parse the table into a more useful format
     interactions: dict[int, InteractionDefinition] = {}
     for row in content:
-        interactions[int(row["No"])] = InteractionDefinition(**row)
+        interactions[int(row["No"])] = InteractionDefinition(**row)  # type: ignore[arg-type]
     return interactions
 
 
diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py
index 91b5871d15..c2ba356e32 100644
--- a/tests/v3/compatibility_suite/util/__init__.py
+++ b/tests/v3/compatibility_suite/util/__init__.py
@@ -22,23 +22,15 @@ def _():
 from __future__ import annotations
 
 import base64
-import contextlib
 import hashlib
-import json
 import logging
-import sys
 import typing
 from collections.abc import Collection, Mapping
 from datetime import date, datetime, time
 from pathlib import Path
 from typing import Any, Generic, TypeVar
-from xml.etree import ElementTree
 
-import flask
-from flask import request
 from multidict import MultiDict
-from typing_extensions import Self
-from yarl import URL
 
 if typing.TYPE_CHECKING:
     from pact.v3.pact import Pact
@@ -130,7 +122,7 @@ def truncate(data: str | bytes) -> str:
 
     This is useful for printing large strings or bytes objects in tests.
     """
-    if len(data) <= 32:
+    if len(data) <= 256:
         if isinstance(data, str):
             return f"{data}"
         return data.decode("utf-8", "backslashreplace")
@@ -140,9 +132,9 @@ def truncate(data: str | bytes) -> str:
         checksum = hashlib.sha256(data.encode()).hexdigest()
         return (
             '"'
-            + data[:6]
+            + data[:128]
             + "⋯"
-            + data[-6:]
+            + data[-128:]
             + '"'
             + f" ({length} bytes, sha256={checksum[:7]})"
         )
@@ -185,6 +177,31 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]:
     return [dict(zip(rows[0], row)) for row in rows[1:]]
 
 
+def parse_horizontal_markdown_table(content: str) -> dict[str, str]:
+    """
+    Parse a Markdown table into a list of dictionaries.
+
+    The table is expected to be in the following format:
+
+    ```markdown
+    | key1 | val1 |
+    | key2 | val2 |
+    | key3 | val3 |
+    ```
+    """
+    rows = [
+        list(map(str.strip, row.split("|")))[1:-1]
+        for row in content.split("\n")
+        if row.strip()
+    ]
+
+    if len(rows[0]) > 2:
+        msg = f"Expected at most two columns in the table, got {len(rows[0])}"
+        raise ValueError(msg)
+
+    return {row[0]: row[1] for row in rows}
+
+
 def serialize(obj: Any) -> Any:  # noqa: ANN401, PLR0911
     """
     Convert an object to a dictionary.
@@ -269,459 +286,3 @@ def parse_matching_rules(matching_rules: str) -> str:
 
     with (FIXTURES_ROOT / matching_rules).open("r") as file:
         return file.read()
-
-
-class InteractionDefinition:
-    """
-    Interaction definition.
-
-    This is a dictionary that represents a single interaction. It is used to
-    parse the HTTP interactions table into a more useful format.
-    """
-
-    class Body:
-        """
-        Interaction body.
-
-        The interaction body can be one of:
-
-        - A file
-        - An arbitrary string
-        - A JSON document
-        - An XML document
-        """
-
-        def __init__(self, data: str) -> None:
-            """
-            Instantiate the interaction body.
-            """
-            self.string: str | None = None
-            self.bytes: bytes | None = None
-            self.mime_type: str | None = None
-
-            if data.startswith("file: ") and data.endswith("-body.xml"):
-                self.parse_fixture(FIXTURES_ROOT / data[6:])
-                return
-
-            if data.startswith("file: "):
-                self.parse_file(FIXTURES_ROOT / data[6:])
-                return
-
-            if data.startswith("JSON: "):
-                self.string = data[6:]
-                self.bytes = self.string.encode("utf-8")
-                self.mime_type = "application/json"
-                return
-
-            if data.startswith("XML: "):
-                self.string = data[5:]
-                self.bytes = self.string.encode("utf-8")
-                self.mime_type = "application/xml"
-                return
-
-            self.bytes = data.encode("utf-8")
-            self.string = data
-
-        def __repr__(self) -> str:
-            """
-            Debugging representation.
-            """
-            return "<Body: {}>".format(
-                ", ".join(
-                    str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()
-                ),
-            )
-
-        def parse_fixture(self, fixture: Path) -> None:
-            """
-            Parse a fixture file.
-
-            This is used to parse the fixture files that contain additional
-            metadata about the body (such as the content type).
-            """
-            etree = ElementTree.parse(fixture)  # noqa: S314
-            root = etree.getroot()
-            if not root or root.tag != "body":
-                msg = "Invalid XML fixture document"
-                raise ValueError(msg)
-
-            contents = root.find("contents")
-            content_type = root.find("contentType")
-            if contents is None:
-                msg = "Invalid XML fixture document: no contents"
-                raise ValueError(msg)
-            if content_type is None:
-                msg = "Invalid XML fixture document: no contentType"
-                raise ValueError(msg)
-            self.string = typing.cast(str, contents.text)
-
-            if eol := contents.attrib.get("eol", None):
-                if eol == "CRLF":
-                    self.string = self.string.replace("\r\n", "\n")
-                    self.string = self.string.replace("\n", "\r\n")
-                elif eol == "LF":
-                    self.string = self.string.replace("\r\n", "\n")
-
-            self.bytes = self.string.encode("utf-8")
-            self.mime_type = content_type.text
-
-        def parse_file(self, file: Path) -> None:
-            """
-            Load the contents of a file.
-
-            The mime type is inferred from the file extension, and the contents
-            are loaded as a byte array, and optionally as a string.
-            """
-            self.bytes = file.read_bytes()
-            with contextlib.suppress(UnicodeDecodeError):
-                self.string = file.read_text()
-
-            if file.suffix == ".xml":
-                self.mime_type = "application/xml"
-            elif file.suffix == ".json":
-                self.mime_type = "application/json"
-            elif file.suffix == ".jpg":
-                self.mime_type = "image/jpeg"
-            elif file.suffix == ".pdf":
-                self.mime_type = "application/pdf"
-            else:
-                msg = "Unknown file type"
-                raise ValueError(msg)
-
-    class State:
-        """
-        Provider state.
-        """
-
-        def __init__(
-            self,
-            name: str,
-            parameters: str | dict[str, Any] | None = None,
-        ) -> None:
-            """
-            Instantiate the provider state.
-            """
-            self.name = name
-            self.parameters: dict[str, Any]
-            if isinstance(parameters, str):
-                self.parameters = json.loads(parameters)
-            else:
-                self.parameters = parameters or {}
-
-        def __repr__(self) -> str:
-            """
-            Debugging representation.
-            """
-            return "<State: {}>".format(
-                ", ".join(
-                    str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()
-                ),
-            )
-
-        def as_dict(self) -> dict[str, str | dict[str, Any]]:
-            """
-            Convert the provider state to a dictionary.
-            """
-            return {"name": self.name, "parameters": self.parameters}
-
-        @classmethod
-        def from_dict(cls, data: dict[str, Any]) -> Self:
-            """
-            Convert a dictionary to a provider state.
-            """
-            return cls(**data)
-
-    def __init__(self, **kwargs: str) -> None:
-        """Initialise the interaction definition."""
-        self.id: int | None = None
-
-        self.states: list[InteractionDefinition.State] = []
-        self.pending: bool = False
-        self.text_comments: list[str] = []
-        self.comments: dict[str, str] = {}
-        self.test_name: str | None = None
-
-        self.method: str = kwargs.pop("method")
-        self.path: str = kwargs.pop("path")
-        self.response: int = int(kwargs.pop("response", 200))
-        self.query: str | None = None
-        self.headers: MultiDict[str] = MultiDict()
-        self.body: InteractionDefinition.Body | None = None
-
-        self.response_headers: MultiDict[str] = MultiDict()
-        self.response_body: InteractionDefinition.Body | None = None
-        self.matching_rules: str | None = None
-        self.response_matching_rules: str | None = None
-
-        self.update(**kwargs)
-
-    def update(self, **kwargs: str) -> None:  # noqa: C901, PLR0912
-        """
-        Update the interaction definition.
-
-        This is a convenience method that allows the interaction definition to
-        be updated with new values.
-        """
-        if interaction_id := kwargs.pop("No", None):
-            self.id = int(interaction_id)
-
-        if method := kwargs.pop("method", None):
-            self.method = method
-
-        if path := kwargs.pop("path", None):
-            self.path = path
-
-        if query := kwargs.pop("query", None):
-            self.query = query
-
-        if headers := kwargs.pop("headers", None):
-            self.headers = parse_headers(headers)
-
-        if headers := (
-            kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None)
-        ):
-            self.headers = parse_headers(headers)
-
-        if body := kwargs.pop("body", None):
-            # When updating the body, we _only_ update the body content, not
-            # the content type.
-            orig_content_type = self.body.mime_type if self.body else None
-            self.body = InteractionDefinition.Body(body)
-            self.body.mime_type = self.body.mime_type or orig_content_type
-
-        if content_type := (
-            kwargs.pop("content_type", None) or kwargs.pop("content type", None)
-        ):
-            if self.body is None:
-                self.body = InteractionDefinition.Body("")
-            self.body.mime_type = content_type
-
-        if response := kwargs.pop("response", None) or kwargs.pop("status", None):
-            self.response = int(response)
-
-        if response_headers := (
-            kwargs.pop("response_headers", None) or kwargs.pop("response headers", None)
-        ):
-            self.response_headers = parse_headers(response_headers)
-
-        if response_content := (
-            kwargs.pop("response_content", None) or kwargs.pop("response content", None)
-        ):
-            if self.response_body is None:
-                self.response_body = InteractionDefinition.Body("")
-            self.response_body.mime_type = response_content
-
-        if response_body := (
-            kwargs.pop("response_body", None) or kwargs.pop("response body", None)
-        ):
-            orig_content_type = (
-                self.response_body.mime_type if self.response_body else None
-            )
-            self.response_body = InteractionDefinition.Body(response_body)
-            self.response_body.mime_type = (
-                self.response_body.mime_type or orig_content_type
-            )
-
-        if matching_rules := (
-            kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None)
-        ):
-            self.matching_rules = parse_matching_rules(matching_rules)
-
-        if matching_rules := (
-            kwargs.pop("response_matching_rules", None)
-            or kwargs.pop("response matching rules", None)
-        ):
-            self.response_matching_rules = parse_matching_rules(matching_rules)
-
-        if len(kwargs) > 0:
-            msg = f"Unexpected arguments: {kwargs.keys()}"
-            raise TypeError(msg)
-
-    def __repr__(self) -> str:
-        """
-        Debugging representation.
-        """
-        return "<Body: {}>".format(
-            ", ".join(f"{k}={v!r}" for k, v in vars(self).items()),
-        )
-
-    def add_to_pact(self, pact: Pact, name: str) -> None:  # noqa: C901, PLR0912, PLR0915
-        """
-        Add the interaction to the pact.
-
-        This is a convenience method that allows the interaction definition to
-        be added to the pact, defining the "upon receiving ... with ... will
-        respond with ...".
-
-        Args:
-            pact:
-                The pact being defined.
-
-            name:
-                Name for this interaction. Must be unique for the pact.
-        """
-        interaction = pact.upon_receiving(name)
-        logger.info("with_request(%s, %s)", self.method, self.path)
-        interaction.with_request(self.method, self.path)
-
-        for state in self.states or []:
-            if state.parameters:
-                logger.info("given(%s, parameters=%s)", state.name, state.parameters)
-                interaction.given(state.name, parameters=state.parameters)
-            else:
-                logger.info("given(%s)", state.name)
-                interaction.given(state.name)
-
-        if self.pending:
-            logger.info("set_pending(True)")
-            interaction.set_pending(pending=True)
-
-        if self.text_comments:
-            logger.info("set_comment(text, %s)", self.text_comments)
-            interaction.set_comment("text", self.text_comments)
-
-        for key, value in self.comments.items():
-            logger.info("set_comment(%s, %s)", key, value)
-            interaction.set_comment(key, value)
-
-        if self.test_name:
-            logger.info("test_name(%s)", self.test_name)
-            interaction.test_name(self.test_name)
-
-        if self.query:
-            query = URL.build(query_string=self.query).query
-            logger.info("with_query_parameters(%s)", query.items())
-            interaction.with_query_parameters(query.items())
-
-        if self.headers:
-            logger.info("with_headers(%s)", self.headers.items())
-            interaction.with_headers(self.headers.items())
-
-        if self.body:
-            if self.body.string:
-                logger.info(
-                    "with_body(%s, %s)",
-                    truncate(self.body.string),
-                    self.body.mime_type,
-                )
-                interaction.with_body(
-                    self.body.string,
-                    self.body.mime_type,
-                )
-            elif self.body.bytes:
-                logger.info(
-                    "with_binary_file(%s, %s)",
-                    truncate(self.body.bytes),
-                    self.body.mime_type,
-                )
-                interaction.with_binary_body(
-                    self.body.bytes,
-                    self.body.mime_type,
-                )
-            else:
-                msg = "Unexpected body definition"
-                raise RuntimeError(msg)
-
-        if self.matching_rules:
-            logger.info("with_matching_rules(%s)", self.matching_rules)
-            interaction.with_matching_rules(self.matching_rules)
-
-        if self.response:
-            logger.info("will_respond_with(%s)", self.response)
-            interaction.will_respond_with(self.response)
-
-        if self.response_headers:
-            logger.info("with_headers(%s)", self.response_headers)
-            interaction.with_headers(self.response_headers.items())
-
-        if self.response_body:
-            if self.response_body.string:
-                logger.info(
-                    "with_body(%s, %s)",
-                    truncate(self.response_body.string),
-                    self.response_body.mime_type,
-                )
-                interaction.with_body(
-                    self.response_body.string,
-                    self.response_body.mime_type,
-                )
-            elif self.response_body.bytes:
-                logger.info(
-                    "with_binary_file(%s, %s)",
-                    truncate(self.response_body.bytes),
-                    self.response_body.mime_type,
-                )
-                interaction.with_binary_body(
-                    self.response_body.bytes,
-                    self.response_body.mime_type,
-                )
-            else:
-                msg = "Unexpected body definition"
-                raise RuntimeError(msg)
-
-        if self.response_matching_rules:
-            logger.info("with_matching_rules(%s)", self.response_matching_rules)
-            interaction.with_matching_rules(self.response_matching_rules)
-
-    def add_to_flask(self, app: flask.Flask) -> None:
-        """
-        Add an interaction to a Flask app.
-
-        Args:
-            app:
-                The Flask app to add the interaction to.
-        """
-        sys.stderr.write(
-            f"Adding interaction to Flask app: {self.method} {self.path}\n"
-        )
-        sys.stderr.write(f"  Query: {self.query}\n")
-        sys.stderr.write(f"  Headers: {self.headers}\n")
-        sys.stderr.write(f"  Body: {self.body}\n")
-        sys.stderr.write(f"  Response: {self.response}\n")
-        sys.stderr.write(f"  Response headers: {self.response_headers}\n")
-        sys.stderr.write(f"  Response body: {self.response_body}\n")
-
-        def route_fn() -> flask.Response:
-            sys.stderr.write(f"Received request: {self.method} {self.path}\n")
-            if self.query:
-                query = URL.build(query_string=self.query).query
-                # Perform a two-way check to ensure that the query parameters
-                # are present in the request, and that the request contains no
-                # unexpected query parameters.
-                for k, v in query.items():
-                    assert request.args[k] == v
-                for k, v in request.args.items():
-                    assert query[k] == v
-
-            if self.headers:
-                # Perform a one-way check to ensure that the expected headers
-                # are present in the request, but don't check for any unexpected
-                # headers.
-                for k, v in self.headers.items():
-                    assert k in request.headers
-                    assert request.headers[k] == v
-
-            if self.body:
-                assert request.data == self.body.bytes
-
-            return flask.Response(
-                response=self.response_body.bytes or self.response_body.string or None
-                if self.response_body
-                else None,
-                status=self.response,
-                headers=dict(**self.response_headers),
-                content_type=self.response_body.mime_type
-                if self.response_body
-                else None,
-                direct_passthrough=True,
-            )
-
-        # The route function needs to have a unique name
-        clean_name = self.path.replace("/", "_").replace("__", "_")
-        route_fn.__name__ = f"{self.method.lower()}_{clean_name}"
-
-        app.add_url_rule(
-            self.path,
-            view_func=route_fn,
-            methods=[self.method],
-        )
diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py
index 66033cd50b..76887b6367 100644
--- a/tests/v3/compatibility_suite/util/consumer.py
+++ b/tests/v3/compatibility_suite/util/consumer.py
@@ -29,7 +29,9 @@
 
     from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction
     from pact.v3.pact import PactServer
-    from tests.v3.compatibility_suite.util import InteractionDefinition
+    from tests.v3.compatibility_suite.util.interaction_definition import (
+        InteractionDefinition,
+    )
 
 logger = logging.getLogger(__name__)
 
@@ -90,7 +92,7 @@ def _(
         pact.with_specification(version)
         for iid in ids:
             definition = interaction_definitions[iid]
-            logging.info("Adding interaction %s", iid)
+            logger.info("Adding interaction %s", iid)
             definition.add_to_pact(pact, f"interaction {iid}")
 
         with pact.serve(raises=False) as srv:
@@ -121,8 +123,8 @@ def _(
         pact = Pact("consumer", "provider")
         pact.with_specification(version)
         definition = interaction_definitions[iid]
-        definition.update(**content[0])
-        logging.info("Adding modified interaction %s", iid)
+        definition.update(**content[0])  # type: ignore[arg-type]
+        logger.info("Adding modified interaction %s", iid)
         definition.add_to_pact(pact, f"interaction {iid}")
 
         with pact.serve(raises=False) as srv:
@@ -154,6 +156,9 @@ def _(
         ):
             definition.headers.add("Content-Type", definition.body.mime_type)
 
+        assert definition.method is not None, "Method not defined"
+        assert definition.path is not None, "Path not defined"
+
         return requests.request(
             definition.method,
             str(srv.url.with_path(definition.path)),
@@ -195,7 +200,7 @@ def _(
         """
         definition = interaction_definitions[request_id]
         assert len(content) == 1, "Expected exactly one row in the table"
-        definition.update(**content[0])
+        definition.update(**content[0])  # type: ignore[arg-type]
 
         if (
             definition.body
@@ -204,6 +209,9 @@ def _(
         ):
             definition.headers.add("Content-Type", definition.body.mime_type)
 
+        assert definition.method is not None, "Method not defined"
+        assert definition.path is not None, "Path not defined"
+
         return requests.request(
             definition.method,
             str(srv.url.with_path(definition.path)),
@@ -263,7 +271,7 @@ def _(
         """
         A response is returned.
         """
-        logging.info(
+        logger.info(
             "Request Information:\n%s",
             json.dumps(
                 {
@@ -277,7 +285,7 @@ def _(
                 indent=2,
             ),
         )
-        logging.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2))
+        logger.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2))
         assert response.status_code == code
 
 
@@ -310,7 +318,8 @@ def _(
         response: requests.Response,
         content_type: str,
     ) -> None:
-        assert response.headers["Content-Type"] == content_type
+        assert "Content-Type" in response.headers, "Content-Type not set"
+        assert response.headers["Content-Type"] == content_type, "Content-Type mismatch"
 
 
 def the_mock_server_status_will_be(stacklevel: int = 1) -> None:
diff --git a/tests/v3/compatibility_suite/util/interaction_definition.py b/tests/v3/compatibility_suite/util/interaction_definition.py
new file mode 100644
index 0000000000..19fd5ce051
--- /dev/null
+++ b/tests/v3/compatibility_suite/util/interaction_definition.py
@@ -0,0 +1,759 @@
+"""
+Interaction definition.
+
+This module defines the `InteractionDefinition` class, which is used to help
+parse the interaction definitions from the compatibility suite, and interact
+with the `Pact` and `Interaction` classes.
+"""
+
+from __future__ import annotations
+
+import base64
+import contextlib
+import json
+import logging
+import sys
+import typing
+from typing import Any, Literal
+from xml.etree import ElementTree
+
+import flask
+from flask import request
+from multidict import MultiDict
+from typing_extensions import Self
+from yarl import URL
+
+from pact.v3.interaction import HttpInteraction, Interaction
+from tests.v3.compatibility_suite.util import (
+    FIXTURES_ROOT,
+    parse_headers,
+    parse_matching_rules,
+    truncate,
+)
+
+if typing.TYPE_CHECKING:
+    from pathlib import Path
+
+    from pact.v3.interaction import Interaction
+    from pact.v3.pact import Pact
+    from tests.v3.compatibility_suite.util.provider import Provider
+
+logger = logging.getLogger(__name__)
+
+
+class InteractionBody:
+    """
+    Interaction body.
+
+    The interaction body can be one of:
+
+    - A file
+    - An arbitrary string
+    - A JSON document
+    - An XML document
+    """
+
+    def __init__(self, data: str) -> None:
+        """
+        Instantiate the interaction body.
+        """
+        self.string: str | None = None
+        self.bytes: bytes | None = None
+        self.mime_type: str | None = None
+
+        if data.startswith("file: ") and data.endswith("-body.xml"):
+            self.parse_fixture(FIXTURES_ROOT / data[6:])
+            return
+
+        if data.startswith("file: "):
+            self.parse_file(FIXTURES_ROOT / data[6:])
+            return
+
+        if data.startswith("JSON: "):
+            self.string = data[6:]
+            self.bytes = self.string.encode("utf-8")
+            self.mime_type = "application/json"
+            return
+
+        if data.startswith("XML: "):
+            self.string = data[5:]
+            self.bytes = self.string.encode("utf-8")
+            self.mime_type = "application/xml"
+            return
+
+        self.bytes = data.encode("utf-8")
+        self.string = data
+
+    def __repr__(self) -> str:
+        """
+        Debugging representation.
+        """
+        return "<Body: {}>".format(
+            ", ".join(str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()),
+        )
+
+    def parse_fixture(self, fixture: Path) -> None:
+        """
+        Parse a fixture file.
+
+        This is used to parse the fixture files that contain additional
+        metadata about the body (such as the content type).
+        """
+        etree = ElementTree.parse(fixture)  # noqa: S314
+        root = etree.getroot()
+        if not root or root.tag != "body":
+            msg = "Invalid XML fixture document"
+            raise ValueError(msg)
+
+        contents = root.find("contents")
+        content_type = root.find("contentType")
+        if contents is None:
+            msg = "Invalid XML fixture document: no contents"
+            raise ValueError(msg)
+        if content_type is None:
+            msg = "Invalid XML fixture document: no contentType"
+            raise ValueError(msg)
+        self.string = typing.cast(str, contents.text)
+
+        if eol := contents.attrib.get("eol", None):
+            if eol == "CRLF":
+                self.string = self.string.replace("\r\n", "\n")
+                self.string = self.string.replace("\n", "\r\n")
+            elif eol == "LF":
+                self.string = self.string.replace("\r\n", "\n")
+
+        self.bytes = self.string.encode("utf-8")
+        self.mime_type = content_type.text
+
+    def parse_file(self, file: Path) -> None:
+        """
+        Load the contents of a file.
+
+        The mime type is inferred from the file extension, and the contents
+        are loaded as a byte array, and optionally as a string.
+        """
+        self.bytes = file.read_bytes()
+        with contextlib.suppress(UnicodeDecodeError):
+            self.string = file.read_text()
+
+        if file.suffix == ".xml":
+            self.mime_type = "application/xml"
+        elif file.suffix == ".json":
+            self.mime_type = "application/json"
+        elif file.suffix == ".jpg":
+            self.mime_type = "image/jpeg"
+        elif file.suffix == ".pdf":
+            self.mime_type = "application/pdf"
+        else:
+            msg = "Unknown file type"
+            raise ValueError(msg)
+
+    def add_to_interaction(
+        self,
+        interaction: Interaction,
+    ) -> None:
+        """
+        Add a body to the interaction.
+
+        This is a helper method that adds the body to the interaction. This
+        relies on Pact's intelligent understanding of whether it is dealing with
+        a request or response (which is determined through the use of
+        `will_respond_with`).
+
+        Args:
+            body:
+                The body to add to the interaction.
+
+            interaction:
+                The interaction to add the body to.
+
+        """
+        if self.string:
+            logger.info(
+                "with_body(%r, %r)",
+                truncate(self.string),
+                self.mime_type,
+            )
+            interaction.with_body(
+                self.string,
+                self.mime_type,
+            )
+        elif self.bytes:
+            logger.info(
+                "with_binary_body(%r, %r)",
+                truncate(self.bytes),
+                self.mime_type,
+            )
+            interaction.with_binary_body(
+                self.bytes,
+                self.mime_type,
+            )
+        else:
+            msg = "Unexpected body definition"
+            raise RuntimeError(msg)
+
+        if self.mime_type and isinstance(interaction, HttpInteraction):
+            logger.info('set_header("Content-Type", %r)', self.mime_type)
+            interaction.set_header("Content-Type", self.mime_type)
+
+
+class InteractionState:
+    """
+    Provider state.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        parameters: str | dict[str, Any] | None = None,
+    ) -> None:
+        """
+        Instantiate the provider state.
+        """
+        self.name = name
+        self.parameters: dict[str, Any]
+        if isinstance(parameters, str):
+            self.parameters = json.loads(parameters)
+        else:
+            self.parameters = parameters or {}
+
+    def __repr__(self) -> str:
+        """
+        Debugging representation.
+        """
+        return "<State: {}>".format(
+            ", ".join(str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()),
+        )
+
+    def as_dict(self) -> dict[str, str | dict[str, Any]]:
+        """
+        Convert the provider state to a dictionary.
+        """
+        return {"name": self.name, "parameters": self.parameters}
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> Self:
+        """
+        Convert a dictionary to a provider state.
+        """
+        return cls(**data)
+
+
+class InteractionDefinition:
+    """
+    Interaction definition.
+
+    This is a dictionary that represents a single interaction. It is used to
+    parse the HTTP interactions table into a more useful format.
+    """
+
+    def __init__(
+        self,
+        *,
+        metadata: dict[str, Any] | None = None,
+        **kwargs: str,
+    ) -> None:
+        """
+        Initialise the interaction definition.
+
+        As the interaction definitions are parsed from a Markdown table,
+        values are expected to be strings and must be converted to the correct
+        type.
+
+        The _only_ exception to this is the `metadata` key, which expects
+        a dictionary.
+        """
+        # A common pattern used in the tests is to have a table with the 'base'
+        # definitions, and have tests modify these definitions as need be. As a
+        # result, the `__init__` method is designed to set all the values to
+        # defaults, and the `update` method is used to update the values.
+        if type_ := kwargs.pop("type", "HTTP"):
+            if type_ not in ("HTTP", "Sync", "Async"):
+                msg = f"Invalid value for 'type': {type_}"
+                raise ValueError(msg)
+            self.type: Literal["HTTP", "Sync", "Async"] = type_  # type: ignore[assignment]
+
+        # General properties shared by all interaction types
+        self.id: int | None = None
+        self.description: str | None = None
+        self.states: list[InteractionState] = []
+        self.metadata: dict[str, Any] | None = None
+        self.pending: bool = False
+        self.is_pending: bool = False
+        self.test_name: str | None = None
+        self.text_comments: list[str] = []
+        self.comments: dict[str, str] = {}
+
+        # Request properties
+        self.method: str | None = None
+        self.path: str | None = None
+        self.response: int | None = None
+        self.query: str | None = None
+        self.headers: MultiDict[str] = MultiDict()
+        self.body: InteractionBody | None = None
+        self.matching_rules: str | None = None
+
+        # Response properties
+        self.response_headers: MultiDict[str] = MultiDict()
+        self.response_body: InteractionBody | None = None
+        self.response_matching_rules: str | None = None
+
+        self.update(metadata=metadata, **kwargs)
+
+    def update(self, *, metadata: dict[str, Any] | None = None, **kwargs: str) -> None:
+        """
+        Update the interaction definition.
+
+        This is a convenience method that allows the interaction definition to
+        be updated with new values.
+        """
+        kwargs = self._update_shared(metadata, **kwargs)
+        kwargs = self._update_request(**kwargs)
+        kwargs = self._update_response(**kwargs)
+
+        if len(kwargs) > 0:
+            msg = f"Unexpected arguments: {kwargs.keys()}"
+            raise TypeError(msg)
+
+    def _update_shared(
+        self,
+        metadata: dict[str, Any] | None = None,
+        **kwargs: str,
+    ) -> dict[str, str]:
+        """
+        Update the shared properties of the interaction.
+
+        Note that the following properties are not supported and must be
+        modified directly:
+
+        -   `states`
+        -   `text_comments`
+        -   `comments`
+
+        Args:
+            metadata:
+                Metadata for the interaction.
+
+            kwargs:
+                Remaining keyword arguments, which are:
+
+                -   `No`: Interaction ID. Used purely for debugging purposes.
+                -   `description`: Description of the interaction (used by
+                    asynchronous messages)
+                -   `pending`: Whether the interaction is pending.
+                -   `test_name`: Test name for the interaction.
+
+        Returns:
+            The remaining keyword arguments.
+        """
+        if interaction_id := kwargs.pop("No", None):
+            self.id = int(interaction_id)
+
+        if description := kwargs.pop("description", None):
+            self.description = description
+
+        if "states" in kwargs:
+            msg = "Unsupported. Modify the 'states' property directly."
+            raise ValueError(msg)
+
+        if metadata:
+            self.metadata = metadata
+
+        if "pending" in kwargs:
+            self.pending = kwargs.pop("pending") == "true"
+
+        if test_name := kwargs.pop("test_name", None):
+            self.test_name = test_name
+
+        if "text_comments" in kwargs:
+            msg = "Unsupported. Modify the 'text_comments' property directly."
+            raise ValueError(msg)
+
+        if "comments" in kwargs:
+            msg = "Unsupported. Modify the 'comments' property directly."
+            raise ValueError(msg)
+
+        return kwargs
+
+    def _update_request(self, **kwargs: str) -> dict[str, str]:
+        """
+        Update the request properties of the interaction.
+
+        Args:
+            kwargs:
+                Remaining keyword arguments, which are:
+
+                -   `method`: Request method.
+                -   `path`: Request path.
+                -   `query`: Query parameters.
+                -   `headers`: Request headers.
+                -   `raw_headers`: Request headers.
+                -   `body`: Request body.
+                -   `content_type`: Request content type.
+                -   `matching_rules`: Request matching rules.
+
+        """
+        if method := kwargs.pop("method", None):
+            self.method = method
+
+        if path := kwargs.pop("path", None):
+            self.path = path
+
+        if query := kwargs.pop("query", None):
+            self.query = query
+
+        if headers := kwargs.pop("headers", None):
+            self.headers = parse_headers(headers)
+
+        if headers := (
+            kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None)
+        ):
+            self.headers = parse_headers(headers)
+
+        if body := kwargs.pop("body", None):
+            # When updating the body, we _only_ update the body content, not
+            # the content type.
+            orig_content_type = self.body.mime_type if self.body else None
+            self.body = InteractionBody(body)
+            self.body.mime_type = self.body.mime_type or orig_content_type
+
+        if content_type := (
+            kwargs.pop("content_type", None) or kwargs.pop("content type", None)
+        ):
+            if self.body is None:
+                self.body = InteractionBody("")
+            self.body.mime_type = content_type
+
+        if matching_rules := (
+            kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None)
+        ):
+            self.matching_rules = parse_matching_rules(matching_rules)
+
+        return kwargs
+
+    def _update_response(self, **kwargs: str) -> dict[str, str]:
+        """
+        Update the response properties of the interaction.
+
+        Args:
+            kwargs:
+                Remaining keyword arguments, which are:
+
+                -  `response`: Response status code.
+                -  `response_headers`: Response headers.
+                -  `response_content`: Response content type.
+                -  `response_body`: Response body.
+                -  `response_matching_rules`: Response matching rules.
+
+        Returns:
+            The remaining keyword arguments.
+        """
+        if response := kwargs.pop("response", None) or kwargs.pop("status", None):
+            self.response = int(response)
+
+        if response_headers := (
+            kwargs.pop("response_headers", None) or kwargs.pop("response headers", None)
+        ):
+            self.response_headers = parse_headers(response_headers)
+
+        if response_content := (
+            kwargs.pop("response_content", None) or kwargs.pop("response content", None)
+        ):
+            if self.response_body is None:
+                self.response_body = InteractionBody("")
+            self.response_body.mime_type = response_content
+
+        if response_body := (
+            kwargs.pop("response_body", None) or kwargs.pop("response body", None)
+        ):
+            orig_content_type = (
+                self.response_body.mime_type if self.response_body else None
+            )
+            self.response_body = InteractionBody(response_body)
+            self.response_body.mime_type = (
+                self.response_body.mime_type or orig_content_type
+            )
+
+        if matching_rules := (
+            kwargs.pop("response_matching_rules", None)
+            or kwargs.pop("response matching rules", None)
+        ):
+            self.response_matching_rules = parse_matching_rules(matching_rules)
+
+        return kwargs
+
+    def __repr__(self) -> str:
+        """
+        Debugging representation.
+        """
+        return "<Body: {}>".format(
+            ", ".join(f"{k}={v!r}" for k, v in vars(self).items()),
+        )
+
+    def add_to_pact(  # noqa: C901, PLR0912, PLR0915
+        self,
+        pact: Pact,
+        name: str,
+    ) -> None:
+        """
+        Add the interaction to the pact.
+
+        This is a convenience method that allows the interaction definition to
+        be added to the pact, defining the "upon receiving ... with ... will
+        respond with ...".
+
+        Args:
+            pact:
+                The pact being defined.
+
+            name:
+                Name for this interaction. Must be unique for the pact.
+        """
+        interaction = pact.upon_receiving(name, self.type)
+        if isinstance(interaction, HttpInteraction):
+            assert self.method, "Method must be defined"
+            assert self.path, "Path must be defined"
+
+            logger.info("with_request(%r, %r)", self.method, self.path)
+            interaction.with_request(self.method, self.path)
+
+        for state in self.states or []:
+            if state.parameters:
+                logger.info("given(%r, parameters=%r)", state.name, state.parameters)
+                interaction.given(state.name, parameters=state.parameters)
+            else:
+                logger.info("given(%r)", state.name)
+                interaction.given(state.name)
+
+        if self.pending:
+            logger.info("set_pending(True)")
+            interaction.set_pending(pending=True)
+
+        if self.text_comments:
+            for comment in self.text_comments:
+                logger.info("add_text_comment(%r)", comment)
+                interaction.add_text_comment(comment)
+
+        for key, value in self.comments.items():
+            logger.info("set_comment(%r, %r)", key, value)
+            interaction.set_comment(key, value)
+
+        if self.test_name:
+            logger.info("test_name(%r)", self.test_name)
+            interaction.test_name(self.test_name)
+
+        if self.query:
+            assert isinstance(
+                interaction, HttpInteraction
+            ), "Query parameters require an HTTP interaction"
+            query = URL.build(query_string=self.query).query
+            logger.info("with_query_parameters(%r)", query.items())
+            interaction.with_query_parameters(query.items())
+
+        if self.headers:
+            assert isinstance(
+                interaction, HttpInteraction
+            ), "Headers require an HTTP interaction"
+            logger.info("with_headers(%r)", self.headers.items())
+            interaction.with_headers(self.headers.items())
+
+        if self.body:
+            self.body.add_to_interaction(interaction)
+
+        if self.matching_rules:
+            logger.info("with_matching_rules(%r)", self.matching_rules)
+            interaction.with_matching_rules(self.matching_rules)
+
+        if self.response:
+            assert isinstance(
+                interaction, HttpInteraction
+            ), "Response requires an HTTP interaction"
+            logger.info("will_respond_with(%r)", self.response)
+            interaction.will_respond_with(self.response)
+
+        if self.response_headers:
+            assert isinstance(
+                interaction, HttpInteraction
+            ), "Response headers require an HTTP interaction"
+            logger.info("with_headers(%r)", self.response_headers)
+            interaction.with_headers(self.response_headers.items())
+
+        if self.response_body:
+            assert isinstance(
+                interaction, HttpInteraction
+            ), "Response body requires an HTTP interaction"
+            self.response_body.add_to_interaction(interaction)
+
+        if self.response_matching_rules:
+            logger.info("with_matching_rules(%r)", self.response_matching_rules)
+            interaction.with_matching_rules(self.response_matching_rules)
+
+        if self.metadata:
+            for key, value in self.metadata.items():
+                if isinstance(value, str):
+                    interaction.with_metadata({key: value})
+                else:
+                    interaction.with_metadata({key: json.dumps(value)})
+
+    def add_to_provider(self, provider: Provider) -> None:
+        """
+        Add an interaction to a Flask app.
+
+        Args:
+            provider:
+                The test provider to add the interaction to.
+        """
+        logger.debug("Adding %s interaction to Flask app", self.type)
+        if self.type == "HTTP":
+            self._add_http_to_provider(provider)
+        elif self.type == "Sync":
+            self._add_sync_to_provider(provider)
+        elif self.type == "Async":
+            self._add_async_to_provider(provider)
+        else:
+            msg = f"Unknown interaction type: {self.type}"
+            raise ValueError(msg)
+
+    def _add_http_to_provider(self, provider: Provider) -> None:
+        """
+        Add a HTTP interaction to a Flask app.
+
+        Ths function works by defining a new function to handle the request and
+        produce the response. This function is then added to the Flask app as a
+        route.
+
+        Args:
+            provider:
+                The test provider to add the interaction to.
+        """
+        assert isinstance(self.method, str), "Method must be a string"
+        assert isinstance(self.path, str), "Path must be a string"
+
+        logger.info(
+            "Adding HTTP '%s %s' interaction to Flask app",
+            self.method,
+            self.path,
+        )
+        logger.debug("-> Query: %s", self.query)
+        logger.debug("-> Headers: %s", self.headers)
+        logger.debug("-> Body: %s", self.body)
+        logger.debug("-> Response Status: %s", self.response)
+        logger.debug("-> Response Headers: %s", self.response_headers)
+        logger.debug("-> Response Body: %s", self.response_body)
+
+        def route_fn() -> flask.Response:
+            if self.query:
+                query = URL.build(query_string=self.query).query
+                # Perform a two-way check to ensure that the query parameters
+                # are present in the request, and that the request contains no
+                # unexpected query parameters.
+                for k, v in query.items():
+                    assert request.args[k] == v
+                for k, v in request.args.items():
+                    assert query[k] == v
+
+            if self.headers:
+                # Perform a one-way check to ensure that the expected headers
+                # are present in the request, but don't check for any unexpected
+                # headers.
+                for k, v in self.headers.items():
+                    assert k in request.headers
+                    assert request.headers[k] == v
+
+            if self.body:
+                assert request.data == self.body.bytes
+
+            return flask.Response(
+                response=self.response_body.bytes or self.response_body.string or None
+                if self.response_body
+                else None,
+                status=self.response,
+                headers=dict(**self.response_headers),
+                content_type=self.response_body.mime_type
+                if self.response_body
+                else None,
+                direct_passthrough=True,
+            )
+
+        # The route function needs to have a unique name
+        clean_name = self.path.replace("/", "_").replace("__", "_")
+        route_fn.__name__ = f"{self.method.lower()}_{clean_name}"
+
+        provider.app.add_url_rule(
+            self.path,
+            view_func=route_fn,
+            methods=[self.method],
+        )
+
+    def _add_sync_to_provider(self, provider: Provider) -> None:
+        """
+        Add a synchronous message interaction to a Flask app.
+
+        Args:
+            provider:
+                The test provider to add the interaction to.
+        """
+        raise NotImplementedError
+
+    def _add_async_to_provider(self, provider: Provider) -> None:
+        """
+        Add a synchronous message interaction to a Flask app.
+
+        Args:
+            provider:
+                The test provider to add the interaction to.
+        """
+        assert self.description, "Description must be set for async messages"
+        provider.messages[self.description] = self
+
+        # All messages are handled by the same route. So we just need to check
+        # whether the route has been defined, and if not, define it.
+        for rule in provider.app.url_map.iter_rules():
+            if rule.rule == "/_pact/message":
+                sys.stderr.write("Async message route already defined\n")
+                return
+
+        sys.stderr.write("Adding async message route\n")
+
+        @provider.app.post("/_pact/message")
+        def post_message() -> flask.Response:
+            body: dict[str, Any] = json.loads(request.data)
+            description: str = body["description"]
+
+            if description not in provider.messages:
+                return flask.Response(
+                    response=json.dumps({
+                        "error": f"Message {description} not found",
+                    }),
+                    status=404,
+                    headers={"Content-Type": "application/json"},
+                    content_type="application/json",
+                )
+
+            interaction: InteractionDefinition = provider.messages[description]
+            return interaction.create_async_message_response()
+
+    def create_async_message_response(self) -> flask.Response:
+        """
+        Convert the interaction to a Flask response.
+
+        When an async message needs to be produced, Pact expects the response
+        from the special `/_pact/message` endppoint to generate the expected
+        message.
+
+        Whilst this is a Response from Flask's perspective, the attributes
+        returned
+        """
+        assert self.type == "Async", "Only async messages are supported"
+
+        if self.metadata:
+            self.headers["Pact-Message-Metadata"] = base64.b64encode(
+                json.dumps(self.metadata).encode("utf-8")
+            ).decode("utf-8")
+
+        return flask.Response(
+            response=self.body.bytes or self.body.string or None if self.body else None,
+            headers=((k, v) for k, v in self.headers.items()),
+            content_type=self.body.mime_type if self.body else None,
+            direct_passthrough=True,
+        )
diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py
index f9ab72b901..923de54b20 100644
--- a/tests/v3/compatibility_suite/util/provider.py
+++ b/tests/v3/compatibility_suite/util/provider.py
@@ -49,10 +49,14 @@
 import pact.constants  # type: ignore[import-untyped]
 from pact.v3.pact import Pact
 from tests.v3.compatibility_suite.util import (
-    InteractionDefinition,
     parse_headers,
     parse_markdown_table,
     serialize,
+    truncate,
+)
+from tests.v3.compatibility_suite.util.interaction_definition import (
+    InteractionDefinition,
+    InteractionState,
 )
 
 if TYPE_CHECKING:
@@ -87,6 +91,13 @@
 start of the scenario.
 """
 
+VERIFIER_ERROR_MAP: dict[str, str] = {
+    "Response status did not match": "StatusMismatch",
+    "Headers had differences": "HeaderMismatch",
+    "Body had differences": "BodyMismatch",
+    "Metadata had differences": "MetadataMismatch",
+}
+
 
 def next_version() -> str:
     """
@@ -120,12 +131,35 @@ def _find_free_port() -> int:
         return s.getsockname()[1]
 
 
+def _setup_logging(log_level: int) -> None:
+    """
+    Set up logging for the provider.
+
+    Pytest is responsible for setting up the logging for the main Python
+    process, but the provider runs in a subprocess and does not automatically
+    inherit the logging configuration.
+
+    This function sets up the logging within the provider subprocess, provided
+    that it wasn't already set up (in case any logging configuration is
+    inherited).
+    """
+    if logging.getLogger().handlers:
+        return
+
+    logging.basicConfig(
+        level=log_level,
+        format="%(asctime)s.%(msec)03d [%(levelname)s] %(name)s: %(message)s",
+        datefmt="%H:%M:%S",
+    )
+    logger.debug("Debug logging enabled")
+
+
 class Provider:
     """
     HTTP Provider.
     """
 
-    def __init__(self, provider_dir: Path | str) -> None:
+    def __init__(self, provider_dir: Path | str, log_level: int) -> None:
         """
         Instantiate a new provider.
 
@@ -135,31 +169,37 @@ def __init__(self, provider_dir: Path | str) -> None:
                 provider. At a minimum, this directory must contain a file
                 called `interactions.pkl`. This file must contain a list of
                 [`InteractionDefinition`] objects.
+
+            log_level:
+                The log level for the provider.
         """
+        _setup_logging(log_level)
+
+        self.messages: dict[str, InteractionDefinition] = {}
         self.provider_dir = Path(provider_dir)
         if not self.provider_dir.is_dir():
             msg = f"Directory {self.provider_dir} does not exist"
             raise ValueError(msg)
 
         self.app: flask.Flask = flask.Flask("provider")
-        self._add_ping(self.app)
-        self._add_callback(self.app)
-        self._add_after_request(self.app)
-        self._add_interactions(self.app)
+        self._add_ping()
+        self._add_callback()
+        self._add_after_request()
+        self._add_interactions()
 
-    def _add_ping(self, app: flask.Flask) -> None:
+    def _add_ping(self) -> None:
         """
         Add a ping endpoint to the provider.
 
         This is used to check that the provider is running.
         """
 
-        @app.get("/_test/ping")
+        @self.app.get("/_test/ping")
         def ping() -> str:
             """Simple ping endpoint for testing."""
             return "pong"
 
-    def _add_callback(self, app: flask.Flask) -> None:
+    def _add_callback(self) -> None:
         """
         Add a callback endpoint to the provider.
 
@@ -176,20 +216,24 @@ def _add_callback(self, app: flask.Flask) -> None:
         contents of the file.
         """
 
-        @app.route("/_test/callback", methods=["GET", "POST"])
+        @self.app.route("/_pact/callback", methods=["GET", "POST"])
         def callback() -> tuple[str, int] | str:
             if (self.provider_dir / "fail_callback").exists():
                 return "Provider state not found", 404
 
             provider_states_path = self.provider_dir / "provider_states"
             if provider_states_path.exists():
+                logger.debug("Provider states file found")
                 with provider_states_path.open() as f:
-                    states = [InteractionDefinition.State(**s) for s in json.load(f)]
+                    states = [InteractionState(**s) for s in json.load(f)]
+                logger.debug("Provider states: %s", states)
                 for state in states:
                     if request.args["state"] == state.name:
+                        logger.debug("State found: %s", state)
                         for k, v in state.parameters.items():
                             assert k in request.args
                             assert str(request.args[k]) == str(v)
+                        logger.debug("State parameters match")
                         break
                 else:
                     msg = "State not found"
@@ -208,7 +252,7 @@ def callback() -> tuple[str, int] | str:
                         "query_params": serialize(request.args),
                         "headers_list": serialize(request.headers),
                         "headers_dict": serialize(dict(request.headers)),
-                        "body": request.data.decode("utf-8"),
+                        "body": request.data.decode("utf-8", errors="backslashreplace"),
                         "form": serialize(request.form),
                     },
                     f,
@@ -216,7 +260,7 @@ def callback() -> tuple[str, int] | str:
 
             return ""
 
-    def _add_after_request(self, app: flask.Flask) -> None:
+    def _add_after_request(self) -> None:
         """
         Add a handler to log requests and responses.
 
@@ -224,14 +268,13 @@ def _add_after_request(self, app: flask.Flask) -> None:
         application (both to the logger as well as to files).
         """
 
-        @app.after_request
+        @self.app.after_request
         def log_request(response: flask.Response) -> flask.Response:
-            sys.stderr.write(f"START REQUEST: {request.method} {request.path}\n")
-            sys.stderr.write(f"Query string: {request.query_string.decode('utf-8')}\n")
-            sys.stderr.write(f"Header: {serialize(request.headers)}\n")
-            sys.stderr.write(f"Body: {request.data.decode('utf-8')}\n")
-            sys.stderr.write(f"Form: {serialize(request.form)}\n")
-            sys.stderr.write("END REQUEST\n")
+            logger.debug("Received request: %s %s", request.method, request.path)
+            logger.debug("-> Query string: %s", request.query_string.decode("utf-8"))
+            logger.debug("-> Headers: %s", serialize(request.headers))
+            logger.debug("-> Body: %s", truncate(request.get_data().decode("utf-8")))
+            logger.debug("-> Form: %s", serialize(request.form))
 
             with (
                 self.provider_dir
@@ -245,21 +288,23 @@ def log_request(response: flask.Response) -> flask.Response:
                         "query_params": serialize(request.args),
                         "headers_list": serialize(request.headers),
                         "headers_dict": serialize(dict(request.headers)),
-                        "body": request.data.decode("utf-8"),
+                        "body": request.data.decode("utf-8", errors="backslashreplace"),
                         "form": serialize(request.form),
                     },
                     f,
                 )
             return response
 
-        @app.after_request
+        @self.app.after_request
         def log_response(response: flask.Response) -> flask.Response:
-            sys.stderr.write(f"START RESPONSE: {response.status_code}\n")
-            sys.stderr.write(f"Headers: {serialize(response.headers)}\n")
-            sys.stderr.write(
-                f"Body: {response.get_data().decode('utf-8', errors='replace')}\n"
+            logger.debug("Returning response: %d", response.status_code)
+            logger.debug("-> Headers: %s", serialize(response.headers))
+            logger.debug(
+                "-> Body: %s",
+                truncate(
+                    response.get_data().decode("utf-8", errors="backslashreplace")
+                ),
             )
-            sys.stderr.write("END RESPONSE\n")
 
             with (
                 self.provider_dir
@@ -270,13 +315,15 @@ def log_response(response: flask.Response) -> flask.Response:
                         "status_code": response.status_code,
                         "headers_list": serialize(response.headers),
                         "headers_dict": serialize(dict(response.headers)),
-                        "body": response.get_data().decode("utf-8", errors="replace"),
+                        "body": response.get_data().decode(
+                            "utf-8", errors="backslashreplace"
+                        ),
                     },
                     f,
                 )
             return response
 
-    def _add_interactions(self, app: flask.Flask) -> None:
+    def _add_interactions(self) -> None:
         """
         Add the interactions to the provider.
         """
@@ -284,7 +331,7 @@ def _add_interactions(self, app: flask.Flask) -> None:
             interactions: list[InteractionDefinition] = pickle.load(f)  # noqa: S301
 
         for interaction in interactions:
-            interaction.add_to_flask(app)
+            interaction.add_to_provider(self)
 
     def run(self) -> None:
         """
@@ -489,11 +536,11 @@ def latest_verification_results(self) -> requests.Response | None:
 if __name__ == "__main__":
     import sys
 
-    if len(sys.argv) != 2:
-        sys.stderr.write(f"Usage: {sys.argv[0]} <dir>")
+    if len(sys.argv) != 3:
+        sys.stderr.write(f"Usage: {sys.argv[0]} <dir> <log_level>\n")
         sys.exit(1)
 
-    Provider(sys.argv[1]).run()
+    Provider(sys.argv[1], int(sys.argv[2])).run()
 
 
 ################################################################################
@@ -564,7 +611,7 @@ def _(
         defns: list[InteractionDefinition] = []
         for interaction in interactions:
             defn = copy.deepcopy(interaction_definitions[interaction])
-            defn.update(**changes[0])
+            defn.update(**changes[0])  # type: ignore[arg-type]
             defns.append(defn)
             logger.debug(
                 "Updated interaction %d: %s",
@@ -578,6 +625,45 @@ def _(
         yield from start_provider(temp_dir)
 
 
+def a_provider_is_started_that_can_generate_the_message(
+    stacklevel: int = 1,
+) -> None:
+    @given(
+        parsers.re(
+            r"a provider is started"
+            r' that can generate the "(?P<name>[^"]+)" message'
+            r' with "(?P<body>.+)"$'
+        ),
+        target_fixture="provider_url",
+        stacklevel=stacklevel + 1,
+    )
+    def _(
+        temp_dir: Path,
+        name: str,
+        body: str,
+    ) -> Generator[URL, None, None]:
+        interactions: list[InteractionDefinition] = []
+        interactions_pkl = temp_dir / "interactions.pkl"
+        if interactions_pkl.exists():
+            with interactions_pkl.open("rb") as f:
+                interactions = pickle.load(f)  # noqa: S301
+
+        interaction = InteractionDefinition(
+            type="Async",
+            description=name,
+            body=body.replace(r"\"", '"'),
+        )
+        # If there's no content type, then it is a `text/plain` message
+        if interaction.body and not interaction.body.mime_type:
+            interaction.body.mime_type = "text/plain"
+        interactions.append(interaction)
+
+        with (temp_dir / "interactions.pkl").open("wb") as pkl_file:
+            pickle.dump(interactions, pkl_file)
+
+        yield from start_provider(temp_dir)
+
+
 def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]:  # noqa: C901
     """Start the provider app with the given interactions."""
     process = subprocess.Popen(  # noqa: S603
@@ -585,6 +671,7 @@ def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]:  # n
             sys.executable,
             Path(__file__),
             str(provider_dir),
+            str(logger.getEffectiveLevel()),
         ],
         cwd=Path.cwd(),
         stdout=subprocess.PIPE,
@@ -630,10 +717,10 @@ def redirect() -> NoReturn:
         while True:
             if process.stdout:
                 while line := process.stdout.readline():
-                    logger.debug("Provider stdout: %s", line.strip())
+                    logger.debug("Provider stdout: %s", line.rstrip())
             if process.stderr:
                 while line := process.stderr.readline():
-                    logger.debug("Provider stderr: %s", line.strip())
+                    logger.debug("Provider stderr: %s", line.rstrip())
 
     thread = Thread(target=redirect, daemon=True)
     thread.start()
@@ -690,6 +777,45 @@ def _(
         verifier.add_source(temp_dir / "pacts")
 
 
+def a_pact_file_for_message_is_to_be_verified(
+    version: str,
+    stacklevel: int = 1,
+) -> None:
+    @given(
+        parsers.re(
+            r'a Pact file for "(?P<name>[^"]+)":"(?P<fixture>[^"]+)" is to be verified'
+            r"(?P<pending>(, but is marked pending)?)",
+        ),
+        converters={"pending": lambda x: x != ""},
+        stacklevel=stacklevel + 1,
+    )
+    def _(
+        verifier: Verifier,
+        temp_dir: Path,
+        name: str,
+        fixture: str,
+        pending: bool,  # noqa: FBT001
+    ) -> None:
+        defn = InteractionDefinition(
+            type="Async",
+            description=name,
+            body=fixture,
+        )
+        defn.pending = pending
+        logger.debug("Adding message interaction: %s", defn)
+
+        pact = Pact("consumer", "provider")
+        pact.with_specification(version)
+        defn.add_to_pact(pact, name)
+        (temp_dir / "pacts").mkdir(exist_ok=True, parents=True)
+        pact.write_file(temp_dir / "pacts")
+
+        with (temp_dir / "pacts" / "consumer-provider.json").open() as f:
+            logger.debug("Pact file contents: %s", f.read())
+
+        verifier.add_source(temp_dir / "pacts")
+
+
 def a_pact_file_for_interaction_is_to_be_verified_with_comments(
     version: str,
     stacklevel: int = 1,
@@ -745,6 +871,52 @@ def _(
         verifier.add_source(temp_dir / "pacts")
 
 
+def a_pact_file_for_message_is_to_be_verified_with_comments(
+    version: str,
+    stacklevel: int = 1,
+) -> None:
+    @given(
+        parsers.re(
+            r'a Pact file for "(?P<name>[^"]+)":"(?P<fixture>[^"]+)" is to be verified'
+            r" with the following comments:\n(?P<comments>.+)",
+            re.DOTALL,
+        ),
+        converters={"comments": parse_markdown_table},
+        stacklevel=stacklevel + 1,
+    )
+    def _(
+        verifier: Verifier,
+        temp_dir: Path,
+        name: str,
+        fixture: str,
+        comments: list[dict[str, str]],
+    ) -> None:
+        defn = InteractionDefinition(
+            type="Async",
+            description=name,
+            body=fixture,
+        )
+        for comment in comments:
+            if comment["type"] == "text":
+                defn.text_comments.append(comment["comment"])
+            elif comment["type"] == "testname":
+                defn.test_name = comment["comment"]
+            else:
+                defn.comments[comment["type"]] = comment["comment"]
+        logger.info("Updated interaction: %s", defn)
+
+        pact = Pact("consumer", "provider")
+        pact.with_specification(version)
+        defn.add_to_pact(pact, name)
+        (temp_dir / "pacts").mkdir(exist_ok=True, parents=True)
+        pact.write_file(temp_dir / "pacts")
+
+        with (temp_dir / "pacts" / "consumer-provider.json").open() as f:
+            logger.debug("Pact file contents: %s", f.read())
+
+        verifier.add_source(temp_dir / "pacts")
+
+
 def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker(
     version: str,
     stacklevel: int = 1,
@@ -832,7 +1004,7 @@ def _(
                 f.write("true")
 
         verifier.set_state(
-            provider_url / "_test" / "callback",
+            provider_url / "_pact" / "callback",
             teardown=True,
         )
 
@@ -866,7 +1038,7 @@ def _(
         )
 
         defn = interaction_definitions[interaction]
-        defn.states = [InteractionDefinition.State(state)]
+        defn.states = [InteractionState(state)]
 
         pact = Pact("consumer", "provider")
         pact.with_specification(version)
@@ -912,8 +1084,7 @@ def _(
 
         defn = interaction_definitions[interaction]
         defn.states = [
-            InteractionDefinition.State(s["State Name"], s.get("Parameters", None))
-            for s in states
+            InteractionState(s["State Name"], s.get("Parameters", None)) for s in states
         ]
 
         pact = Pact("consumer", "provider")
@@ -978,6 +1149,11 @@ def _(
         logger.debug("Running verification on %r", verifier)
 
         verifier.set_info("provider", url=provider_url)
+        verifier.add_transport(
+            protocol="message",
+            port=provider_url.port,
+            path="/_pact/message",
+        )
         try:
             verifier.verify()
         except Exception as e:  # noqa: BLE001
@@ -1030,18 +1206,13 @@ def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None:
         verifier = verifier_result[0]
         logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2))
 
-        if error == "Response status did not match":
-            mismatch_type = "StatusMismatch"
-        elif error == "Headers had differences":
-            mismatch_type = "HeaderMismatch"
-        elif error == "Body had differences":
-            mismatch_type = "BodyMismatch"
-        elif error == "State change request failed":
-            assert "One or more of the setup state change handlers has failed" in [
-                error["mismatch"]["message"] for error in verifier.results["errors"]
-            ]
-            return
-        else:
+        mismatch_type = VERIFIER_ERROR_MAP.get(error)
+        if not mismatch_type:
+            if error == "State change request failed":
+                assert "One or more of the setup state change handlers has failed" in [
+                    error["mismatch"]["message"] for error in verifier.results["errors"]
+                ]
+                return
             msg = f"Unknown error type: {error}"
             raise ValueError(msg)
 
diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py
index 1c76b44cea..6e56ecd79b 100644
--- a/tests/v3/test_http_interaction.py
+++ b/tests/v3/test_http_interaction.py
@@ -13,6 +13,7 @@
 import pytest
 
 from pact.v3 import Pact
+from pact.v3.pact import MismatchesError
 
 if TYPE_CHECKING:
     from pathlib import Path
@@ -362,7 +363,7 @@ async def test_with_body_response(pact: Pact, method: str) -> None:
                 json={"test": True},
             ) as resp:
                 assert resp.status == 200
-                assert await resp.json() == {"test": True}
+                assert json.loads(await resp.content.read()) == {"test": True}
 
 
 @pytest.mark.asyncio()
@@ -382,7 +383,7 @@ async def test_with_body_explicit(pact: Pact) -> None:
                 json={"request": True},
             ) as resp:
                 assert resp.status == 200
-                assert await resp.json() == {"response": True}
+                assert json.loads(await resp.content.read()) == {"response": True}
 
 
 def test_with_body_invalid(pact: Pact) -> None:
@@ -444,10 +445,10 @@ async def test_binary_file_request(pact: Pact) -> None:
         async with aiohttp.ClientSession(srv.url) as session:
             async with session.post("/", data=payload) as resp:
                 assert resp.status == 200
+
+    with pytest.raises(MismatchesError), pact.serve() as srv:  # noqa: PT012
+        async with aiohttp.ClientSession(srv.url) as session:
             async with session.post("/", data=payload[:2]) as resp:
-                # The match _only_ checks the content type, not the content
-                # itself. See
-                # https://pact-foundation.slack.com/archives/C02BXLDJ7JR/p1697032990681329
                 assert resp.status == 200
 
 
diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py
index cedb665119..aab32d1b92 100644
--- a/tests/v3/test_verifier.py
+++ b/tests/v3/test_verifier.py
@@ -157,7 +157,7 @@ def test_broker_source_selector(verifier: Verifier) -> None:
         verifier.broker_source("http://localhost:8080", selector=True)
         .consumer_tags("main", "test")
         .provider_tags("main", "test")
-        .consumer_versions("1.2.3")
+        .consumer_versions('{"latest": true}')
         .build()
     )