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

# Delivery policies

> Understand optional DID and Persona routing rules, constraints, and deterministic policy resolution

Delivery policies let a decentralized identifier (DID) operator or Persona operator publish public routing instructions for encrypted DDN envelopes. A policy can select Relay, inspection, and Cache services without exposing the encrypted payload.

The [shared V3 policy structure](/concepts/policy-v3) is available for DID, Persona, and Application scopes, including event-driven `Send` and `Call` actions. This page describes the earlier V2 routing contract, which remains available for existing integrations.

Policies are optional. When no policy applies, a Relay uses the recipient DID's ordinary service entries. A missing policy is normal fall-through behavior, not an error.

## Policy scopes

OpenPayload stores at most one policy for each supported scope. JSON uses the exact enum values `DID` and `PERSONA`:

| Scope | Identifier | Purpose |
| - | - | - |
| DID | `did:openpayload:...` | Default delivery rules for one recipient DID |
| Persona | Canonical DNS name such as `example.com` | Rules for a registered organization or collective |

A tag is not a policy scope. Tags, labels, and categories mean the same thing in the policy model: an operator-defined message classification such as `Legal`, `Confidential`, or `Secret`. The DDN envelope always uses the public top-level field `tag`, and a rule may match that value.

## Address a Persona

Put the optional Persona after the DID or alias:

```text theme={null}
did:openpayload:1111111111111111111111@example.com
river.stone@example.com
```

Keep the message tag separate:

```json theme={null}
{
  "to": "river.stone@example.com",
  "tag": "Legal"
}
```

Aliases cannot contain `@` or `#`. This makes the single `@` separator unambiguous. The Directory resolves an alias to its DID before policy resolution.

## Resolution order

The chain selects one winning rule from finalized state:

1. Validate and resolve the recipient DID.
2. When the target contains a Persona, require that Persona to be active and registered.
3. Evaluate rules in the Persona policy that match the recipient and tag.
4. If no Persona rule matches, evaluate the DID policy.
5. If no DID rule matches, return no policy and use the DID's ordinary service entries.

Within one policy, the matching rule with the greatest numeric `priority` wins. Ruleset order breaks an equal-priority tie. A winning Persona rule replaces DID policy routing; a route failure follows that rule's own failure transitions and does not fall back to the DID policy.

## Policy structure

Each policy is one versioned object with an ordered `ruleset`:

```json theme={null}
{
  "policy_id": "0x<64-hex-policy-id>",
  "scope": {
    "type": "PERSONA",
    "id": "example.com"
  },
  "revision": 4,
  "policy_ttl_seconds": 60,
  "operator_did": "did:openpayload:1111111111111111111111",
  "ruleset": [
    {
      "id": "legal-delivery",
      "priority": 100,
      "conditions": [
        {
          "type": "TagEquals",
          "value": "Legal"
        },
        {
          "type": "RecipientEquals",
          "value": "did:openpayload:1111111111111111111111"
        }
      ],
      "constraint_overrides": {
        "requested_cache_seconds": 7200,
        "max_replicas": 2
      },
      "action": {
        "type": "DeliveryPlan/v1",
        "entry_step": "inspect",
        "steps": [
          {
            "id": "inspect",
            "operation": "Forward",
            "targets": [
              {
                "service_did": "did:openpayload:1111111111111111111111",
                "service_id": "did:openpayload:1111111111111111111111#inspection",
                "priority": 100,
                "weight": 100
              }
            ]
          },
          {
            "id": "retain",
            "operation": "Store",
            "targets": [
              {
                "service_did": "did:openpayload:1111111111111111111111",
                "service_id": "did:openpayload:1111111111111111111111#cache-primary",
                "priority": 100,
                "weight": 80
              },
              {
                "service_did": "did:openpayload:1111111111111111111111",
                "service_id": "did:openpayload:1111111111111111111111#cache-failover",
                "priority": 50,
                "weight": 20
              }
            ],
            "placement": {
              "desired_replicas": 2,
              "required_replicas": 1,
              "selection": "Weighted"
            }
          }
        ],
        "transitions": [
          {
            "from": "inspect",
            "to": "retain",
            "trigger": "Always",
            "mode": "Next"
          }
        ]
      }
    }
  ]
}
```

Rule IDs must be unique within the policy. A Directory accepts only action and condition versions supported by the network. It rejects unknown commands, invalid service references, duplicate transition edges, graph cycles, unreachable steps, and plans that exceed network bounds.

For a DID scope, `operator_did` must equal the scope DID. For a Persona scope, it must equal the active operator DID recorded on that Persona. The field identifies the controller whose signed authorization can replace or remove the policy; it is not a routing target.

Set `revision` to `1` when creating a scope's first policy. A complete replacement uses the finalized revision plus one. The Directory returns `409 policy_revision_conflict` instead of overwriting a concurrently updated policy.

`policy_ttl_seconds` bounds how long Relay and Cache nodes can reuse a resolved policy lookup. It accepts 1 through 300 seconds and defaults to 60 when omitted from a new policy request. It does not control message retention; `requested_cache_seconds` controls that separately. Existing policies receive a 60-second lookup TTL during the chain storage upgrade.

`policy_id` is a deterministic 32-byte identifier represented as `0x` followed by 64 hexadecimal characters:

```text theme={null}
Blake2b-256(
  UTF-8("openpayload:delivery-policy:v2:") ||
  SCALE(scope_type_u8, scope_id_as_Vec<u8>)
)
```

Use scope discriminant `0` for `DID` and `1` for `PERSONA`. Initial condition shapes are:

| Type | Operand |
| - | - |
| `TagEquals` | `value`: exact tag |
| `TagPrefix` | `value`: tag prefix |
| `TagAbsent` | No `value` or `values` |
| `RecipientEquals` | `value`: one canonical DID |
| `RecipientIn` | `values`: one through 16 canonical DIDs |

All conditions in a rule must match. An empty `conditions` array matches every message in that policy scope. Tags are nonblank, case-sensitive, operator-defined UTF-8 strings of at most 96 bytes; control characters and NUL are prohibited. Leading or trailing spaces are preserved rather than stripped, so operators should avoid spellings that are easy to confuse.

## Delivery plans

`DeliveryPlan/v1` is a bounded directed graph:

* `Forward` sends an envelope to a named next-hop service.
* `Store` selects named Cache services and a replica placement requirement.
* `Archive` routes an envelope to a compatible archive service without changing the network TTL.
* A transition uses `trigger` to select the next step after `Success`, `Failure`, or `Always`.
* `Next` advances one branch. Multiple transitions with the same `from` and `trigger`, different `to` step IDs, and `mode: "Fork"` start parallel branches.

For each source step, `Success` and `Failure` may each have at most one `Next` transition. An `Always` `Next` transition is exclusive with every other `Next` transition from that source. Use `Fork` transitions for fan-out; the Directory and chain reject ambiguous `Next` combinations.

Each target contains `service_did`, `service_id`, `priority`, and `weight`. It cannot contain an arbitrary URL. The DID and service ID must resolve to the exact compatible public service type: `Forward` uses `OpenPayloadRelayService`, `Store` uses `OpenPayloadCacheService`, and `Archive` uses `OpenPayloadArchiveService`. An inspection DID participates as a next hop by publishing a Relay service. One physical endpoint can publish distinct Cache and Archive service entries when it supports both roles. `priority` defines failover tiers. `Priority` selection orders greater priorities first and uses weight as a stable tie-breaker. `Weighted` Store selection uses the message ID to produce deterministic weighted ordering within one priority tier; it never moves a lower-priority target ahead of a higher-priority tier. `Forward` and `Archive` use priority ordering because they do not declare placement.

Every endpoint published by a service referenced from a policy must be a public HTTPS base URI. Credentials, query strings, and fragments are prohibited. The Directory rejects localhost names, `.localhost`, `.local`, `.internal`, and `.home.arpa` names, plus known private, loopback, link-local, multicast, carrier-grade NAT, documentation, and other special-use IP literals. A noncompliant endpoint is not eligible for policy delivery. This check validates the published URI and literal host; it does not pin future DNS answers or guarantee protection from DNS rebinding.

A plan can contain at most one `Forward` step. Its successor steps run afterward under the same pinned policy revision; a policy cannot create a multi-hop chain of Forward steps.

The finalized winning policy remains fixed for one delivery attempt. If a plan forwards once and then continues with successor steps, those steps use the same selected revision even when chain state changes during delivery. Compatible clients submit only the original envelope and must not add policy context or provenance. Network-managed provenance is outside the public client contract.

The policy action can also reject delivery or select the ordinary DID services explicitly:

```json theme={null}
{"type":"Reject/v1","reason_code":451}
```

```json theme={null}
{"type":"BaseDidDelivery/v1"}
```

`Reject/v1.reason_code` is a public unsigned integer from `0` through `65535`. The first protocol version requires an acyclic graph. Clients and nodes must reject a plan containing an unknown action or condition instead of attempting to interpret it as an operator command.

`BaseDidDelivery/v1` applies the recipient DID's ordinary service routing once. If that routing selects another Relay, the receiving Relay continues with the recipient's non-Relay delivery options instead of forwarding through another Relay again.

Policy V2 bounds each policy to 32 rules, each rule to 8 conditions, and each delivery plan to 32 steps and 64 transitions. A step can name at most 16 distinct service targets. Rule and step IDs, including the `from` and `to` references in transitions, use 1 through 64 ASCII letters, digits, `.`, `_`, `:`, or `-`. Priorities range from `0` through `65535`; weights range from `1` through `65535`.

A policy also has a maximum validation budget of 4096 units. Count one unit for each rule, condition, DID in a `RecipientIn` condition, delivery-plan step, route target, and transition. The Directory and chain reject a policy whose total exceeds 4096 even when every individual collection is within its own bound.

A `Store` step requires `placement`. `desired_replicas` is from 1 through 4 and cannot exceed the number of targets; `required_replicas` is from 1 through the desired count. `selection` is `Priority` or `Weighted`. `Forward` and `Archive` steps cannot declare Cache placement.

## Constraints and network ceilings

Message constraints belong to the DID or Persona record. A rule may only narrow the effective base constraints. It cannot increase an effective scope limit or a network ceiling.

For each field, the resolver takes the minimum of the network ceiling, the recipient DID's configured constraint when present, and the addressed Persona's constraint when present. Persona constraints still apply when no Persona rule matches and resolution falls through to a DID rule. The winning rule's optional overrides are then minimum-clamped against that merged base; an override can never cause a Persona winner to lose precedence.

| Limit | Network maximum |
| - | -: |
| Raw HTTP envelope | 25 MiB (`26214400` bytes) |
| Non-payload JSON and header metadata | 64 KiB (`65536` bytes) |
| Decoded unchunked encrypted message | 16 MiB (`16777216` bytes) |
| Decoded chunk | 2 MiB (`2097152` bytes) |
| Cache replicas | 4 |
| Effective retention TTL | 48 hours (`172800` seconds) |

A scope can publish `requested_cache_seconds`, `max_http_envelope_bytes`, `max_unchunked_message_bytes`, `max_chunk_bytes`, and `max_replicas`. The chain preserves the positive `requested_cache_seconds` value and derives the read-only `effective_ttl_seconds`:

`requested_cache_seconds` limits how long a Cache may retain a message. It does not control how long a Relay or Cache retains a policy lookup.

```text theme={null}
effective_ttl_seconds = min(requested_cache_seconds, 172800)
```

An operator may request more than 48 hours, but the effective value remains 48 hours. The Directory rejects an active byte or replica value above its network ceiling rather than silently changing it. The 64 KiB non-payload metadata ceiling is network-fixed and has no scope-constraint field.

Complete scope constraints must also be internally consistent: `max_chunk_bytes` cannot exceed `max_unchunked_message_bytes`, and the unchunked limit cannot exceed the raw HTTP-envelope limit. A rule can omit overrides it does not need.

`max_message_bytes` and `max_chunks` remain present in the Policy V2 SCALE shape only for backward wire compatibility. Current runtime responses normalize them to `4294967295` and `65535`, respectively. They are not operator policy controls, Relay admission limits, or Cache admission limits; clients should omit them from JSON requests. Nodes protect themselves with per-envelope and per-chunk limits plus bounded staging capacity.

When a sender supplies `message_group_id`, `sequence_number`, and `total_chunks`, a Relay stages the opaque envelopes until all declared sequence numbers are present. It does not inspect or reassemble their payloads. The staging manifest and completed-delivery progress are durable across a normal process restart. If the set does not complete within the Relay's staging window, the Relay purges that incomplete set. A client may retry an identical sequence; conflicting content for the same sequence is rejected.

The Relay derives the message TTL as `min(envelope requested ttl, resolved effective ttl)` and computes one absolute `expires_at`. Every stored copy uses that same hard upper bound, and forwarding or replication cannot extend it. A Cache can purge a copy earlier after acknowledgement or when current public authorization or placement no longer permits retention.

## Replicas and acknowledgement

A `Store` step can name eligible Cache services and request multiple replicas. `desired_replicas` is the target count; `required_replicas` is the minimum that must accept the envelope for the step to succeed.

`details.accepted_caches` in a Relay result reports Cache endpoints that accepted a `Store` step, while `details.accepted_archives` reports endpoints that accepted an `Archive` step. Both lists are limited to storage observed by the responding Relay; an upstream Relay does not aggregate later accepts after a `Forward` step. These lists are not embedded in the stored envelope for the recipient. After safely persisting or processing a retrieved Cache copy, the recipient acknowledges it at the Cache that returned it. A successful acknowledgement from one Cache does not delete another Cache's replica.

## Public policy metadata

Policy scopes, rule conditions, service references, priorities, and graph structure are public routing metadata. Do not put plaintext message contents, secrets, private keys, or confidential business logic in a policy or public tag.

## Removed legacy model

The current public contract replaces the earlier recipient/tag/Persona policy maps and DID policy-link endpoints:

* Recipient policies are now DID-scoped policies at `/delivery-policies/dids/{did}`.
* Persona policies use registered DNS Personas rather than Persona DIDs.
* Tag-scoped policy endpoints are removed; tags are rule conditions.
* `effective_ttl` in blocks is replaced by requested and effective clock-time seconds.
* Pricing, release-fee, `cache_eligible`, and policy lifecycle fields are removed.
* DID delivery-policy-link endpoints are removed because a scope owns its single policy directly.

Legacy recipient, tag, and DID policy-link routes are not part of the current public contract. Do not send a legacy policy object to a current endpoint.

| Removed route family | Current model |
| - | - |
| `/delivery-policies/recipients/{did}` | `/delivery-policies/dids/{did}` |
| `/delivery-policies/tags/{tagDid}` | A `TagEquals`, `TagPrefix`, or `TagAbsent` condition inside a DID or Persona policy |
| `/dids/{did}/delivery-policy-links` | No link object; the DID or Persona scope owns its one policy directly |

Current Directory servers do not register the removed routes; they return `404`.


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