> ## 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.

# Build a DDN message envelope

> Learn the encrypted message format accepted by Relay and Cache nodes

A DDN envelope is the package an OpenPayload client sends through Relay and Cache nodes. Think of it as an addressed, sealed parcel:

* The outer fields tell nodes where and how to handle it.
* The encrypted header carries protected routing or application metadata.
* The payload carries encrypted message data.

Nodes validate the outer shape but do not need to decrypt your message.

## Smallest practical shape

```json theme={null}
{
  "message_id": "9f8d6f08-bce5-4df5-b22b-bf8c6bd4e143",
  "to": "did:openpayload:1111111111111111111111@example.com",
  "tag": "Legal",
  "timestamp": "<current-RFC3339-timestamp>",
  "ttl": "24h",
  "payload": {
    "ciphertext_b64": "<encrypted-message>"
  },
  "encrypted_header": "<base64-encrypted-header>",
  "header_nonce": "<base64-nonce>",
  "header_key_id": "recipient-key-1",
  "schema_version": "1"
}
```

## Required fields

| Field | Meaning |
| - | - |
| `message_id` | A UUID generated once for this message |
| `to` | Recipient DID or alias, optionally followed by `@persona`; `recipient` is accepted as an alternate name |
| `timestamp` | ISO-8601, epoch milliseconds, or epoch seconds |
| `payload` | The application payload, normally encrypted |

An encrypted header is strongly recommended for real data, but it is not required by the transport envelope. If using flat fields, supply `encrypted_header`, `header_nonce`, `schema_version`, and either `header_key_id` or `header_key_epoch`.

## Common optional fields

| Field | Purpose |
| - | - |
| `from` | Sender DID; `sender` and `source` are accepted alternates |
| `signature` | Base64 sender signature; when supplied, a sender DID is also required |
| `x_key_id` | Recipient encryption-key or target-device reference |
| `tag` | Public operator-defined message category such as `Legal` or `Confidential` |
| `application_id` | Optional public registered application identifier, such as `talaria`; separate from `tag` |
| `ttl` | Requested clock-time retention such as `15m`, `1h`, `24h`, or `48h` |
| `delivery_hint` | `standard`, `chunked`, `direct-to-cache`, or `realtime` |
| `context` | Client-defined JSON object; it may be `null` or omitted, and Relay does not interpret its application schema |

Unknown outer fields are rejected. This protects interoperability by preventing different clients from silently inventing incompatible headers.

## Network-managed policy provenance

An original sender should omit `expires_at` and must not submit `policy_context`, `policy_id`, `policy_revision`, `policy_rule_id`, `policy_block_hash`, or `policy_block_number`. These are reserved network fields, not client authorization inputs. The public Relay routes reject client-supplied policy provenance with `400 client_policy_context_forbidden`.

The winning rule comes from finalized state and remains fixed for the delivery attempt. Network-managed provenance is not part of the compatible-client envelope contract, and copied client fields never grant policy authority.

When no rule applies, the Relay uses ordinary DID services. A lookup failure is not equivalent to policy-free fall-through.

## Recipient, Persona, and tag

The target syntax is:

```text theme={null}
did-or-alias[@persona]
```

Examples:

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

`@persona` is optional. A Persona is a registered policy name. Domain-shaped Persona names require DNS verification; simple names do not. An alias cannot contain `@` or `#`, so the separator is unambiguous.

Resolution does not rewrite the flat `to` value. When the envelope carries a sender signature, that signature continues to bind the exact DID-or-alias selector the sender supplied.

### Persona release profile

For a recipient whose Persona policy requires gated delivery, first call [delivery resolution](/api-reference/directory/dids/delivery-resolution) with the complete `recipientDid@persona` target. The Directory returns the recipient's DID document, the selected policy, and the public recipient and processor keys. Seal the payload with the returned `openpayload:persona-release:v1` profile, sign the envelope, and send that same envelope to the Relay. The Relay checks its public shape and policy binding before following the delivery plan. The recipient must verify a processor unlock before opening the payload.

The gated envelope must include a sender DID and a valid sender signature. Its `payload` is a JSON object with `profile`, `id` matching `message_id`, `recipient_did`, `persona`, selected `policy_id` and `policy_revision`, `recipient_key_id`, `processor_key_id`, Base64 `nonce` and `ciphertext`, and `recipient_package` and `processor_package` objects. Each package has Base64 `enc` and `ct` values. The Relay checks that the nonce decodes to 12 bytes, both `enc` values decode to 32 bytes, and the ciphertext fields meet the profile's size bounds. It does not decrypt or validate the cryptographic construction.

The network does not inspect the plaintext or prove that the client applied the profile correctly. Applications still choose their own content format inside the encrypted payload.

Do not append `#tag` to `to`. Keep the optional tag in the top-level `tag` field. Tags, labels, and categories are synonymous policy concepts, but `tag` is the only envelope field. Tags are public, nonblank, case-sensitive, and operator-defined, with a maximum of 96 UTF-8 bytes; they cannot contain control characters or NUL. Leading and trailing spaces are preserved rather than stripped. Do not place secret classifications or plaintext message content in a tag.

Policy resolution uses finalized chain state and includes an optional Persona and tag as selectors. Relay and Cache nodes may reuse a resolved policy for its `policy_ttl_seconds`. This ensures DID policies still apply without either selector. A Persona rule has precedence over a DID rule. If no rule applies, delivery falls through to the recipient DID's ordinary services.

`application_id` identifies a separate application control policy. It does not select the recipient delivery route or change message retention. A sender signature that includes `application_id` binds that identifier to the message. The [V3 policy format](/concepts/policy-v3) lets an applicable Application rule react to a Cache store or local Relay delivery with a templated `Send` or `Call` step. The application must verify any proof it places in `context`; the network does not interpret it.

## TTL and size limits

`ttl` is the message's requested retention duration. It is not the scope operator's chain setting. The DID or Persona publishes `requested_cache_seconds`, and the chain derives `effective_ttl_seconds` no greater than 48 hours. The Relay applies the lesser of the envelope request and the chain-derived effective TTL, then produces one absolute `expires_at` timestamp for every Cache replica.

The network rejects an envelope outside these ceilings:

| Item | Maximum |
| - | -: |
| Raw HTTP envelope | 25 MiB |
| Non-payload JSON and header metadata | 64 KiB |
| Decoded unchunked encrypted payload | 16 MiB |
| Decoded chunk | 2 MiB |

A DID, Persona, or matching policy rule can publish lower delivery constraints. The 64 KiB non-payload metadata ceiling is fixed by the network and has no operator constraint field. For larger files, use out-of-band object storage and send an encrypted reference in the payload.

## Chunked messages

For a large encrypted object, send independently deliverable chunks. Each chunk has its own `message_id` and shares a UUID `message_group_id`.

```json theme={null}
{
  "delivery_hint": "chunked",
  "message_group_id": "e93e6c49-d280-4078-81bd-16f09be99e7e",
  "sequence_number": 0,
  "total_chunks": 3
}
```

`sequence_number` begins at `0` and must be less than `total_chunks`. Relay and Cache nodes do not reassemble or decrypt the content.

Each decoded chunk is at most 2 MiB. OpenPayload does not define a network-wide maximum chunk count or aggregate message size. A Relay can still reject an individual envelope with `413` or reject assured staging with `429`/`507` when its advertised size or operational capacity is exceeded.

Supplying `message_group_id`, `sequence_number`, and `total_chunks` opts into assured chunk submission. The Relay durably stages the opaque envelopes, without decrypting or reassembling them, until every declared sequence is present. An identical chunk may be retried. A different envelope that reuses an occupied sequence is rejected with `409`. A `202 chunk_staged` response means the set is still incomplete; `202 chunk_set_delivered` means the Relay completed delivery of the declared set. If the set remains incomplete past the Relay's staging window, it is purged.

## Sender signatures

Sender identity is optional at the envelope-validation layer. If you provide either the sender DID or `signature`, provide both. Nodes that verify the sender resolve the DID and check the signature against its authorized public key.

When `application_id` is present, the signed envelope uses the `openpayload:ddn-envelope:v2` canonical form and includes that exact identifier. Envelopes without `application_id` retain the v1 form.

## Straight to the point

* JSON object only
* UUID `message_id`
* Valid recipient DID or alias with an optional `@persona` suffix
* Timestamp plus payload
* Optional top-level `tag`; never put a tag in `to`
* Optional top-level `application_id`, independent of `tag`
* Optional flat encrypted header or client-defined `context`; a non-null `context` must be a JSON object
* No unknown top-level fields
* Maximum effective retention: 48 hours; a longer request is clamped
* Encrypt before sending


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