diff --git a/payjoin-cli/src/app/v2/ohttp.rs b/payjoin-cli/src/app/v2/ohttp.rs
index e438540ec..b4a227284 100644
--- a/payjoin-cli/src/app/v2/ohttp.rs
+++ b/payjoin-cli/src/app/v2/ohttp.rs
@@ -1,5 +1,7 @@
//! OHTTP relay and payjoin directory selection / key bootstrapping for the payjoin-cli.
//!
+//! See the `payjoin::io` module for relay-selection and health-check guidance.
+//!
//! [`MailroomManager`] tracks relays and directories that have failed,
//! excluding them from future selections for the lifetime of the [`MailroomManager`].
//!
diff --git a/payjoin-ffi/csharp/Payjoin.Http.cs b/payjoin-ffi/csharp/Payjoin.Http.cs
index 0ce5136ca..f32e631fc 100644
--- a/payjoin-ffi/csharp/Payjoin.Http.cs
+++ b/payjoin-ffi/csharp/Payjoin.Http.cs
@@ -6,6 +6,43 @@
namespace Payjoin.Http
{
+ ///
+ /// Fetch OHTTP keys from a payjoin directory through an OHTTP relay.
+ ///
+ ///
+ /// When multiple relays are configured, callers should pick one at random per request
+ /// to avoid a fixed contact pattern at the network layer.
+ ///
+ /// Random selection only helps if the relay list itself is not identifying: prefer a shared
+ /// relay list and discourage isolated infrastructure that other apps don't use, since a
+ /// distinctive list fingerprints the client regardless of how a relay is picked from it.
+ ///
+ /// Sender and receiver have distinct request patterns:
+ /// - Receiver: long-poll GETs, then a POST
+ /// - Sender: a POST, then long-poll GETs
+ ///
+ /// OHTTP does not hide the client IP from the relay. A relay that sees the same
+ /// client repeatedly can observe its access patterns to infer whether
+ /// the IP is associated with a sender or receiver, potentially linking to identity or
+ /// location. Based on when a session ends it may be easier to correctly guess
+ /// whether a transaction is a PayJoin. The IP address linked information may
+ /// additionally aid in cluster analysis, for example whether a cluster's temporal
+ /// patterns are consistent with a location guess for the IP address.
+ ///
+ /// Health checks: some clients call periodically to
+ /// verify that the directory and relay infrastructure is reachable. Given the threat
+ /// model above, this is acceptable only when:
+ /// - The call is not triggered on any deterministic, recurring event
+ /// (e.g. app startup, periodic timer). Prefer user-initiated actions
+ /// (e.g. opening a settings/status screen) or piggybacking on operations
+ /// the user already triggered (e.g. resuming an existing session).
+ /// - The caller throttles invocations so they don't produce a recurring
+ /// timing pattern observable by the relay.
+ ///
+ /// A health check has a distinct traffic pattern from a real payjoin request
+ /// and is not temporally tied to any onchain broadcast, but repeated calls
+ /// still expose the client IP to the relay.
+ ///
internal sealed class OhttpKeysClient : IDisposable
{
private readonly HttpClient _client;
diff --git a/payjoin-ffi/dart/lib/http.dart b/payjoin-ffi/dart/lib/http.dart
index f313f2d43..e6d10d991 100644
--- a/payjoin-ffi/dart/lib/http.dart
+++ b/payjoin-ffi/dart/lib/http.dart
@@ -1,3 +1,40 @@
+/// Fetch OHTTP keys from a payjoin directory through an OHTTP relay.
+///
+/// When multiple relays are configured, callers **should pick one at random per request**
+/// to avoid a fixed contact pattern at the network layer.
+///
+/// Random selection only helps if the relay list itself is not identifying: prefer a shared
+/// relay list and discourage isolated infrastructure that other apps don't use, since a
+/// distinctive list fingerprints the client regardless of how a relay is picked from it.
+///
+/// Sender and receiver have distinct request patterns:
+/// - Receiver: long-poll GETs, then a POST
+/// - Sender: a POST, then long-poll GETs
+///
+/// OHTTP does not hide the client IP from the relay. A relay that sees the same
+/// client repeatedly can observe its access patterns to infer whether
+/// the IP is associated with a sender or receiver, potentially linking to identity or
+/// location. Based on when a session ends it may be easier to correctly guess
+/// whether a transaction is a PayJoin. The IP address linked information may
+/// additionally aid in cluster analysis, for example whether a cluster's temporal
+/// patterns are consistent with a location guess for the IP address.
+///
+/// ## Health checks
+///
+/// Some clients call `fetchOhttpKeys` periodically to verify that the
+/// directory and relay infrastructure is reachable. Given the threat model
+/// above, this is acceptable only when:
+///
+/// - The call is **not** triggered on any deterministic, recurring event
+/// (e.g. app startup, periodic timer). Prefer user-initiated actions
+/// (e.g. opening a settings/status screen) or piggybacking on operations
+/// the user already triggered (e.g. resuming an existing session).
+/// - The caller throttles invocations so they don't produce a recurring
+/// timing pattern observable by the relay.
+///
+/// A health check has a distinct traffic pattern from a real payjoin request
+/// and is not temporally tied to any onchain broadcast, but repeated calls
+/// still expose the client IP to the relay.
library http;
import 'dart:async';
diff --git a/payjoin-ffi/python/src/payjoin/http.py b/payjoin-ffi/python/src/payjoin/http.py
index 70c391700..019d45c40 100644
--- a/payjoin-ffi/python/src/payjoin/http.py
+++ b/payjoin-ffi/python/src/payjoin/http.py
@@ -1,3 +1,42 @@
+"""Fetch OHTTP keys from a payjoin directory through an OHTTP relay.
+
+When multiple relays are configured, callers **should pick one at random per request**
+to avoid a fixed contact pattern at the network layer.
+
+Random selection only helps if the relay list itself is not identifying: prefer a shared
+relay list and discourage isolated infrastructure that other apps don't use, since a
+distinctive list fingerprints the client regardless of how a relay is picked from it.
+
+Sender and receiver have distinct request patterns:
+- Receiver: long-poll GETs, then a POST
+- Sender: a POST, then long-poll GETs
+
+OHTTP does not hide the client IP from the relay. A relay that sees the same
+client repeatedly can observe its access patterns to infer whether
+the IP is associated with a sender or receiver, potentially linking to identity or
+location. Based on when a session ends it may be easier to correctly guess
+whether a transaction is a PayJoin. The IP address linked information may
+additionally aid in cluster analysis, for example whether a cluster's temporal
+patterns are consistent with a location guess for the IP address.
+
+## Health checks
+
+Some clients call `fetch_ohttp_keys` periodically to verify that the
+directory and relay infrastructure is reachable. Given the threat model
+above, this is acceptable only when:
+
+- The call is **not** triggered on any deterministic, recurring event
+ (e.g. app startup, periodic timer). Prefer user-initiated actions
+ (e.g. opening a settings/status screen) or piggybacking on operations
+ the user already triggered (e.g. resuming an existing session).
+- The caller throttles invocations so they don't produce a recurring
+ timing pattern observable by the relay.
+
+A health check has a distinct traffic pattern from a real payjoin request
+and is not temporally tied to any onchain broadcast, but repeated calls
+still expose the client IP to the relay.
+"""
+
import ssl
from urllib.parse import urljoin
diff --git a/payjoin/src/core/io.rs b/payjoin/src/core/io.rs
index 2cbc0010b..2585434fa 100644
--- a/payjoin/src/core/io.rs
+++ b/payjoin/src/core/io.rs
@@ -1,4 +1,40 @@
//! IO-related types and functions. Specifically, fetching OHTTP keys from a payjoin directory.
+//!
+//! When multiple relays are configured, callers **should pick one at random per request**
+//! to avoid a fixed contact pattern at the network layer.
+//!
+//! Random selection only helps if the relay list itself is not identifying: prefer a shared
+//! relay list and discourage isolated infrastructure that other apps don't use, since a
+//! distinctive list fingerprints the client regardless of how a relay is picked from it.
+//!
+//! Sender and receiver have distinct request patterns:
+//! - Receiver: long-poll GETs, then a POST
+//! - Sender: a POST, then long-poll GETs
+//!
+//! OHTTP does not hide the client IP from the relay. A relay that sees the same
+//! client repeatedly can observe its access patterns to infer whether
+//! the IP is associated with a sender or receiver, potentially linking to identity or
+//! location. Based on when a session ends it may be easier to correctly guess
+//! whether a transaction is a PayJoin. The IP address linked information may
+//! additionally aid in cluster analysis, for example whether a cluster's temporal
+//! patterns are consistent with a location guess for the IP address.
+//!
+//! ## Health checks
+//!
+//! Some clients call [`fetch_ohttp_keys`] periodically to verify that the
+//! directory and relay infrastructure is reachable. Given the threat model
+//! above, this is acceptable only when:
+//!
+//! - The call is **not** triggered on any deterministic, recurring event
+//! (e.g. app startup, periodic timer). Prefer user-initiated actions
+//! (e.g. opening a settings/status screen) or piggybacking on operations
+//! the user already triggered (e.g. resuming an existing session).
+//! - The caller throttles invocations so they don't produce a recurring
+//! timing pattern observable by the relay.
+//!
+//! A health check has a distinct traffic pattern from a real payjoin request
+//! and is not temporally tied to any onchain broadcast, but repeated calls
+//! still expose the client IP to the relay.
use std::time::Duration;
use http::header::ACCEPT;