diff --git a/domain/abri/__init__.py b/domain/abri/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/domain/abri/models.py b/domain/abri/models.py
new file mode 100644
index 000000000..e160c3e3b
--- /dev/null
+++ b/domain/abri/models.py
@@ -0,0 +1,32 @@
+from dataclasses import dataclass
+from datetime import date
+from typing import Literal, NewType, Union
+
+PlaceRef = NewType("PlaceRef", str)
+
+SlotCode = Literal["AM", "PM", "AD"]
+
+
+@dataclass(frozen=True)
+class LogJobRequest:
+ client_ref: str
+ place_ref: PlaceRef
+ appointment_date: date
+ appointment_time: SlotCode
+ short_description: str
+ long_description: str
+
+
+@dataclass(frozen=True)
+class JobLogged:
+ job_no: str
+ logged_info: str
+
+
+@dataclass(frozen=True)
+class AbriRequestRejected:
+ code: str
+ message: str
+
+
+LogJobResult = Union[JobLogged, AbriRequestRejected]
diff --git a/infrastructure/abri/__init__.py b/infrastructure/abri/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py
new file mode 100644
index 000000000..2579ba341
--- /dev/null
+++ b/infrastructure/abri/abri_client.py
@@ -0,0 +1,13 @@
+import requests
+
+from domain.abri.models import LogJobRequest, LogJobResult
+from infrastructure.abri.config import AbriConfig
+
+
+class AbriClient:
+ def __init__(self, config: AbriConfig) -> None:
+ self._config = config
+ self._session = requests.Session()
+
+ def log_job(self, request: LogJobRequest) -> LogJobResult:
+ raise NotImplementedError
diff --git a/infrastructure/abri/config.py b/infrastructure/abri/config.py
new file mode 100644
index 000000000..fe9fa6d8c
--- /dev/null
+++ b/infrastructure/abri/config.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+from typing import Mapping
+
+
+@dataclass(frozen=True)
+class AbriConfig:
+ endpoint_url: str
+ username: str
+ password: str
+ default_resource: str
+
+ @classmethod
+ def from_env(cls, env: Mapping[str, str]) -> "AbriConfig":
+ raise NotImplementedError
diff --git a/tests/abri/abri_relay_logjob_relayerror_response.xml b/tests/abri/abri_relay_logjob_relayerror_response.xml
new file mode 100644
index 000000000..ac6647828
--- /dev/null
+++ b/tests/abri/abri_relay_logjob_relayerror_response.xml
@@ -0,0 +1,6 @@
+
+
+ false
+ RelayError
+ No property was found with UPRN/place_ref '1007176aa', street name '', house number '0', house name '', post code ''
+
diff --git a/tests/abri/abri_relay_logjob_request_example.xml b/tests/abri/abri_relay_logjob_request_example.xml
new file mode 100644
index 000000000..9fe160990
--- /dev/null
+++ b/tests/abri/abri_relay_logjob_request_example.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/abri/abri_relay_logjob_success_response.xml b/tests/abri/abri_relay_logjob_success_response.xml
new file mode 100644
index 000000000..aabd14b40
--- /dev/null
+++ b/tests/abri/abri_relay_logjob_success_response.xml
@@ -0,0 +1,6 @@
+
+
+
+ AD0226519
+
+
diff --git a/tests/infrastructure/abri/__init__.py b/tests/infrastructure/abri/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/infrastructure/abri/test_abri_client.py b/tests/infrastructure/abri/test_abri_client.py
new file mode 100644
index 000000000..40eaed7a2
--- /dev/null
+++ b/tests/infrastructure/abri/test_abri_client.py
@@ -0,0 +1,77 @@
+from datetime import date
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from domain.abri.models import JobLogged, LogJobRequest, PlaceRef
+from infrastructure.abri.abri_client import AbriClient
+from infrastructure.abri.config import AbriConfig
+
+FIXTURE_DIR = Path(__file__).parents[2] / "abri"
+ENDPOINT_URL = "https://relay.example.test/api/DomnaRelay?code=test-function-key"
+
+CONFIG = AbriConfig(
+ endpoint_url=ENDPOINT_URL,
+ username="DomnaWeb",
+ password="",
+ default_resource="NAULKH",
+)
+
+
+def _load_fixture(name: str) -> bytes:
+ return (FIXTURE_DIR / name).read_bytes()
+
+
+def _make_client(mock_session: MagicMock) -> AbriClient:
+ with patch(
+ "infrastructure.abri.abri_client.requests.Session",
+ return_value=mock_session,
+ ):
+ return AbriClient(config=CONFIG)
+
+
+def _spec_example_request() -> LogJobRequest:
+ return LogJobRequest(
+ client_ref="DOM51111",
+ place_ref=PlaceRef("1007165"),
+ appointment_date=date(2026, 6, 18),
+ appointment_time="PM",
+ short_description="Christian's SCS External Test.",
+ long_description="Christian's SCS External Test.",
+ )
+
+
+@pytest.fixture()
+def mock_session() -> MagicMock:
+ return MagicMock()
+
+
+@pytest.fixture()
+def client(mock_session: MagicMock) -> AbriClient:
+ return _make_client(mock_session)
+
+
+# --- log_job: success ---
+
+
+def test_log_job_returns_job_logged_with_openhousing_job_number(
+ client: AbriClient, mock_session: MagicMock
+) -> None:
+ # Arrange
+ mock_session.post.return_value.content = _load_fixture(
+ "abri_relay_logjob_success_response.xml"
+ )
+
+ # Act
+ result = client.log_job(_spec_example_request())
+
+ # Assert
+ assert result == JobLogged(
+ job_no="AD0226519",
+ logged_info=(
+ "Job number AD0226519 has been successfully logged for "
+ "49 Admers Crescent, Liphook, Midsomer, XX99 IOP - "
+ "Short Description example job."
+ ),
+ )