> ## Documentation Index
> Fetch the complete documentation index at: https://www.offlineprotocol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These docs target Mesh SDK v0.29.0. Match the installed package and binding before generating code. Start at /getting-started/agents for task-specific reading paths.
> Call the company and product Offline Protocol, never Offline alone. Current packages: @offline-protocol/mesh-sdk 0.29.0 (React Native), offline-protocol-sdk 0.29.0 on PyPI (Python 3.10 to 3.13; wheels for macOS 14+ arm64, Linux x86_64 and aarch64 with glibc 2.34+, Windows x86_64; other hosts build from source; extras lan and http add LAN discovery, the HTTP front and offline-protocol-verify), the offline-protocol crates 0.29.0 on crates.io, the OfflineProtocolSDK Swift package 0.29.0 (preview, iOS 13+ only) and Android bindings from the GitHub release zip (preview, not on Maven Central), @offline-protocol/id-react 0.2.0, @offline-protocol/id-react-native 0.3.3, @offline-protocol/pol 0.1.2 and @offline-protocol/cli 0.2.6. Canonical docs URLs start with https://www.offlineprotocol.com/docs.
> The Mesh SDK runs in a native app or gateway. A browser OfflineID SDK integration does not provide browser mesh transport. Local mesh operation does not require a portal account or API key. The Mesh SDK appId is a mesh app identifier the developer chooses, not the portal App ID (app_...) that OfflineID and Proof of Location require.
> In v0.29.0 service RPC bodies are sealed in the 1:1 MLS session once the recipient runs 0.29 and the session is confirmed; discovery, the first request to a provider never met, and requests to older peers are signed plaintext, so keep secrets out of service bodies unless the event says encrypted: true. The HTTP front (offline-protocol-service --http) always carries calls inside encrypted direct messages but does not authorize callers. Message delivery, durable local acceptance and backend commit are distinct outcomes. Use the workflow guide for the required application logic.
> Offline Protocol CLI 0.2.6 is on npm (@offline-protocol/cli, command offline). Its local MCP server, named offline-protocol, runs with npx -y @offline-protocol/cli@0.2.6 mcp serve and requires no login or key. Hosted MCP is at https://mcp.offlineprotocol.com/mcp with an app API key in Authorization: Bearer and the matching App ID in x-app-id; organization keys are rejected, and Claude Desktop and Claude.ai cannot send these headers; there, use the public read-only endpoint https://mcp.offlineprotocol.com/public/mcp as a custom connector, or local MCP to scaffold or edit projects. Follow /tools/overview for setup and do not invent commands beyond it. MCP provides integration context and planning, not mesh execution; file-writing tools are local only.
> In v0.29.0 phone Wi-Fi peer streams carry data: Android over Wi-Fi Direct (the SDK forms the group with autoAccept on Android 10+) and over the Wi-Fi network it is on (DNS-SD), iOS over Network framework (LAN or AWDL). Android and iOS meet over that slot only on a shared Wi-Fi network that allows multicast; iOS 0.28 and later does not see iOS 0.27 or earlier over it. Otherwise use BLE or a provisioned relay across platforms. The Swift and Android packages are previews whose public names may change; never present a Gradle Maven Central dependency for the Android library. The receiver core ACKs before application persistence; use application acceptance for durable workflows.
> Proof of Location is Sepolia testnet witness evidence, not zero-knowledge proof or proof of presence. The geohash is public onchain. Read /proof-of-location/security before integration.

# HTTP front

> Let programs that do not use the SDK call a service on another device with one plain HTTP request, carried end to end encrypted by the Mesh SDK v0.29.0 service.

The HTTP front lets an application that does not use the SDK call a service on another device the way it calls any HTTP server. It is part of the Python package from v0.29.0 and runs beside the [local API service](/docs/mesh-sdk/local-api) on each host. A provider registers an ordinary HTTP endpoint with the front on its own host. A requester on another host sends an ordinary request to its own front, with a host name that names the service and the device. The fronts carry the request and the response inside end-to-end encrypted messages, over whichever carrier reaches the other host, including through a host in between. Nothing in either application imports the SDK.

## Install and start

```bash theme={null}
pip install 'offline-protocol-sdk[lan,http]==0.29.0'
export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)"   # once per host; keep it
offline-protocol-service --config config.json \
    --mls-root /var/lib/op/keys --state-root /var/lib/op/state \
    --listen 0.0.0.0:7878 --lan --http 127.0.0.1:8080
```

`--http` starts the front in the same process, as a client of the local API under the application ID `offline-protocol-http-front`. `offline-protocol-http-front` runs it as a separate process instead. Run one front per service: two fronts on one host both receive every request and call the callback twice. `GET /health` on the front returns its name, version, connection state and `local_address`, the host's own `off1...` address.

## Register a provider

A provider is any HTTP endpoint on the providing host. Register it with the front on that host:

```bash theme={null}
curl -X PUT http://127.0.0.1:8080/services/timeofday \
  -H 'content-type: application/json' \
  -d '{"callback": "http://127.0.0.1:9000"}'
```

The registration is idempotent, kept in a file so a restart registers it again, and also registered with the engine, so [service discovery](/docs/guides/nearby-service) finds it. The call answers `409` when another local application owns the name. The callback is an `http` or `https` URL with no query or fragment; the request's path is appended to it.

| Endpoint | Does |
| - | - |
| `PUT /services/<service>` | Registers `{"callback": "http://…", "version": "", "capabilities": {}}` |
| `DELETE /services/<service>` | Unregisters |
| `GET /services` | Lists this front's registrations |
| `GET /browse/<service>` | Server-sent events: which devices offer the service, with `hops` and `source` (`mesh` or `lan`) |
| `GET /health` | Name, version, connection state and `local_address` |

## Call a service on another device

```bash theme={null}
curl -H "Host: timeofday.<device address>.offline.protocol.internal" \
  http://127.0.0.1:8080/now
```

The host name is `<service>.<device>.<domain>`. The device is the provider host's `off1...` address, which is a DNS label as it stands, or an alias from the operator's `--http-aliases` file (`{"aliases": {"bob": "off1..."}}`). An alias is never learned from the network. The domain defaults to `offline.protocol.internal` and can be changed with `--http-domain`. The front routes on the host name, so a resolver entry or a wildcard DNS rule that points the domain at the front removes the need for a manual `Host` header.

The providing front calls the callback with the method, the path, the carried headers and the body, plus three headers of its own:

| Header | Value |
| - | - |
| `X-Offline-Protocol-Sender` | The caller's address, as MLS authenticated it |
| `X-Offline-Protocol-Request-Id` | A fresh ID for this request |
| `X-Offline-Protocol-Service` | The service name |

A requester can ask for a shorter or longer deadline with `X-Offline-Protocol-Timeout: <seconds>`.

## Who can call

The front does not authorize. Any device whose message the engine decrypts can call every registered service, and with `auto_key_exchange` on that is any device running the SDK that can reach the host. The provider decides who it serves, for example by comparing `X-Offline-Protocol-Sender` with an allowlist of addresses it received out of band.

That header cannot be forged through the front, but it is unsigned on the callback request. Trust it only when nothing but the front can reach the callback, such as a callback on loopback in a network namespace the front alone shares. A callback on a shared container network or a published port receives whatever header any program sends.

Off loopback, the front requires a per-launch token: `--http-token-file PATH` writes a new 32-byte token at every start, and clients present it in `X-Offline-Protocol-Token`. On any binding the front refuses a request that carries an `Origin` header, so a web page cannot send a body or register a callback through it by DNS rebinding. A page can still make a blind `GET` by loading a resource, so a host where a browser runs should use a token on loopback too. The SDK threat model records this as R23, "The HTTP front trusts every process that can reach it".

## Limits

| Limit | Value | Refused with |
| - | - | - |
| Body, either direction | 128 KiB | `body_too_large` (413) or `response_too_large` (502) |
| Carried headers | 8 KiB | `bad_request` (400) |
| Path and query | 2 KiB | `bad_request` (400) |
| Deadline | 30 s by default, at most 300 s | `deadline_exceeded` (504) |
| Callbacks running at once, per provider | 32 | `callback_failed` (502) |

Only listed headers cross: `accept`, `accept-language`, `authorization`, `cache-control`, `content-language`, `content-type`, `etag`, `if-match`, `if-none-match`, `last-modified`, `location`, and `x-` names other than `x-offline-protocol-*`, `x-forwarded-*`, `x-real-ip`, `x-original-url` and `x-rewrite-url`. A path with a `.` or `..` segment is refused.

The body limit does not fit every carrier. A sealed envelope over about 71 KB does not cross Bluetooth LE at the 185-byte MTU floor, nor one over 64 KiB a Nostr relay; a request whose only route is such a carrier ends as `deadline_exceeded`.

**A `504` does not mean the request did not run.** The requester stops waiting, but the message is not withdrawn: the engine keeps retrying it, and a provider that receives it within 60 seconds past its deadline still calls the callback. A request that changes state should carry an idempotency key of the application's own in an `x-` header, because `X-Offline-Protocol-Request-Id` is new on every retry.

## Errors

| Token | Status | Means |
| - | - | - |
| `bad_host` | 400 | The host is under the domain but not `<service>.<device>`, or, without a token, names neither the domain nor loopback |
| `bad_request` | 400 | Headers or path over the limit, a header value that is not UTF-8, a path with a dot segment, or a timeout that is not a number |
| `unknown_device` | 404 | The label is neither an address nor an alias |
| `unknown_service` | 404 | Nothing registered under that name on the provider |
| `body_too_large` | 413 | The request body is over the limit |
| `deadline_exceeded` | 504 | No response before the deadline; the request may still run |
| `recipient_unreachable` | 502 | The engine reported the device unreachable |
| `send_failed` | 502 | The engine gave up on the request message |
| `callback_failed` | 502 | The callback failed, timed out, answered something a requester would refuse, or 32 callbacks were already running |
| `response_too_large` | 502 | The callback's body is over the limit |
| `not_connected` | 503 | The front has no connection to the local API |
| `unauthorized` | 401 | The token is missing or wrong, or the request carries `Origin` |
| `unsupported_version` | 400 | The envelope version is not 1 |

An error response carries its token in `X-Offline-Protocol-Error`.

## Run it in a container

The SDK repository's [`bindings/python/docker`](https://github.com/Offline-Protocol/offline-protocol-sdk/tree/v0.29.0/bindings/python/docker) builds one service per host on `python:3.12-slim`, with the `lan` and `http` extras installed from PyPI and the front on `127.0.0.1:8080`. No image is published to a registry; build it from that directory. It is configured by environment variables (`OP_LISTEN`, `OP_PEERS`, `OP_LAN`, `OP_CONFIG`, `OP_HTTP`, `OP_HTTP_TOKEN_FILE`, `OP_HTTP_ALIASES`, `OP_RELAY`, `OP_GATEWAY`), and its README lists what the host must grant:

| Grant | Why |
| - | - |
| The host's network namespace | Multicast DNS does not leave a container bridge, and the front then sits on the host's loopback. Set `OP_LISTEN` to the host's reserved LAN address. |
| Inbound TCP 7878 | Other hosts open their peer streams here. |
| A persistent volume at `/var/lib/offline-protocol` | The identity and every queued message. Without it the host has a new address after every recreate. |
| `OFFLINE_PROTOCOL_STORE_KEY` from a secret store | Everything on the volume is sealed under it. Losing it loses the identity. |
| The system D-Bus socket | Bluetooth LE through BlueZ, with `config-ble.json`. |
| A clock | A provider drops a request more than 60 s past its deadline. |

[`examples/http-front`](https://github.com/Offline-Protocol/offline-protocol-sdk/tree/v0.29.0/examples/http-front) runs two boxes on one machine with Docker Compose, a provider with no SDK import on box B and a client on box A, each sharing its box's network namespace:

```bash theme={null}
cd examples/http-front
printf 'BOX_A_STORE_KEY=%s\nBOX_B_STORE_KEY=%s\n' \
    "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env   # once; keep it
docker compose up -d --build
docker compose logs provider          # {"registered": "timeofday", ..., "address": "off1..."}
docker compose run --rm client off1...   # box B's address
```

The provider accepts `--allow off1...` to serve only listed callers. When box B's container is recreated, recreate the provider too: a container that joined the old network namespace stays in it.

## Reference

* [HTTP front chapter](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.29.0/docs/spec/http-front.md): the hostname, envelope, limits, errors and authorization.
* [Local API guide, the HTTP front](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.29.0/docs/local-api.md#the-http-front).
* [Threat model](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.29.0/docs/security/threat-model.md), R23.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.