Inbox Protocol Specification

Version 1.0Secure Asynchronous Messaging for the Decentralized Web

Built on Dap Identity and neue.cash Economics


1. Introduction

1.1 Purpose

The Inbox Protocol defines a secure, asynchronous messaging system designed for the decentralized web. It provides end‑to‑end encrypted communication built on cryptographic identity through the Dap naming system, with spam resistance achieved through economic signals via neue.cash micropayments.

This specification establishes the message formats, cryptographic operations, delivery mechanisms, and policy enforcement rules that enable trustless peer‑to‑peer messaging without reliance on centralized intermediaries.

1.2 Goals

The Inbox Protocol is designed to achieve the following objectives:

1.3 Non‑Goals (Version 1)

The following features are explicitly out of scope for version 1.0 of this specification:

1.4 Terminology

The following terms are used throughout this specification:

Dap name: A human‑readable identifier registered on the Dap network that resolves to cryptographic keys and service endpoints.

Endpoint: An HTTPS URL where a recipient’s inbox server accepts incoming messages.

Envelope: The unencrypted metadata portion of a message containing routing and verification information.

Payload: The encrypted content portion of a message containing the actual message body and attachments.

Allowlist: A recipient‑maintained list of senders permitted to deliver messages without payment.

Blocklist: A recipient‑maintained list of senders whose messages are rejected or silently dropped.

Stranger fee: A neue.cash payment required from senders not on the recipient’s allowlist.

1.5 Notational Conventions

This specification uses the following conventions:

2. Addressing

2.1 Address Format

Inbox addresses are derived from Dap names and support optional subaddressing for routing and policy purposes.

Primary address format: <name>

Subaddress format: <name>/<subaddress>

Examples of valid addresses:

Subaddresses are arbitrary strings chosen by the recipient. They allow a single identity to present multiple contact points with independent policies.

2.2 Name Resolution

To send a message, the sender must resolve the recipient’s Dap name to obtain their inbox configuration. Resolution is performed through the Dap network and returns an InboxRecord containing:

Implementations SHOULD cache resolved records to reduce network overhead. A cache TTL of 24 hours is RECOMMENDED, with immediate refresh on delivery errors indicating key changes.

2.3 Subaddresses

Subaddresses enable recipients to compartmentalize their communications. Each subaddress may define policy overrides that take precedence over the base policy.

When a message is addressed to name/subaddress, the recipient’s server:

  1. Resolves the base name to obtain the InboxRecord
  2. Checks if the subaddress exists in the policy.subaddresses map
  3. Applies any policy overrides defined for that subaddress
  4. Falls back to base policy for unspecified fields

Unknown subaddresses are delivered using the base policy unless the recipient has configured explicit rejection of unknown subaddresses.

3. Dap Record Schema

3.1 InboxRecord Structure

The InboxRecord is stored in the Dap network and contains all information needed to send messages to an identity.


| Field    | Type        | Description                                        |
|----------|-------------|----------------------------------------------------|
| endpoint | string      | HTTPS URL where messages are delivered             |
| keys     | InboxKeys   | Cryptographic keys for encryption and verification |
| policy   | InboxPolicy | Delivery policy configuration                      |
      

3.2 InboxKeys Structure

The keys object contains the recipient’s current and historical cryptographic keys.


| Field    | Type   | Description                                               |
|----------|--------|-----------------------------------------------------------|
| current  | object | Active keypair: id, encrypt, verify, created              |
| previous | array  | Optional array of expired keys with expiration timestamps |
| revoked  | array  | Optional array of revoked key IDs                         |
      

Each key entry contains:

3.3 InboxPolicy Structure

The policy object defines the recipient’s message acceptance rules.


| Field         | Type    | Default  | Description                                           |
|---------------|---------|----------|-------------------------------------------------------|
| allowlistOnly | boolean | false    | If true, reject messages from non‑allowlisted senders |
| blockBehavior | string  | "reject" | "reject" returns error, "silent" drops quietly        |
| blocklist     | array   | []       | Dap names whose messages are rejected                 |
| maxSize       | integer | 1048576  | Maximum message size in bytes                         |
| strangerFee   | string  | "0"      | neue.cash amount required from strangers              |
| subaddresses  | object  | null     | Map of subaddress to policy overrides                 |
      

3.4 Example Record

A complete InboxRecord example:


{
  "endpoint": "https://inbox.grace.dap/messages",
  "keys": {
    "current": {
      "created": 1706812800000,
      "encrypt": "x25519:MCowBQYDK2VuAyEA...",
      "id": "k1706812800",
      "verify": "ed25519:MCowBQYDK2VwAyEA..."
    },
    "previous": [{
      "created": 1698969600000,
      "encrypt": "x25519:MCowBQYDK2VuAyEA...",
      "expires": 1714521600000,
      "id": "k1698969600",
      "verify": "ed25519:MCowBQYDK2VwAyEA..."
    }],
    "revoked": []
  },
  "policy": {
    "allowlistOnly": false,
    "blockBehavior": "reject",
    "blocklist": ["spammer"],
    "maxSize": 1048576,
    "strangerFee": "0.001",
    "subaddresses": {
      "business": { "strangerFee": "0.01" }
    }
  }
}
      

4. Cryptography

4.1 Algorithms

The Inbox Protocol uses the following cryptographic primitives:


| Purpose              | Algorithm          | Reference               |
|----------------------|--------------------|-------------------------|
| Digital Signatures   | Ed25519            | RFC 8032                |
| Hashing              | BLAKE3             | BLAKE3 Specification    |
| Key Agreement        | X25519             | RFC 7748                |
| Symmetric Encryption | XChaCha20‑Poly1305 | draft‑irtf‑cfrg‑xchacha |
      

These algorithms were selected for their security properties, performance characteristics, and broad implementation availability.

4.2 Key Encoding

Public keys are encoded using the format:


<algorithm>:<base64-encoded-key>
      

Examples:

4.3 Key IDs

Key identifiers follow the format:


k<unix-timestamp>
      

Where the timestamp is the Unix time (in seconds) when the key was generated. Key IDs MUST be unique within an identity’s key history.

4.4 Encryption Process

Message encryption follows a hybrid encryption scheme:

  1. Generate a random 256‑bit symmetric key
  2. Encrypt the serialized payload using XChaCha20‑Poly1305 with the symmetric key
  3. Generate an ephemeral X25519 keypair
  4. Perform X25519 key agreement between the ephemeral private key and recipient’s public key
  5. Derive an encryption key from the shared secret using BLAKE3 in keyed mode, with the context string inbox.v1.key-encapsulation
  6. Encrypt the symmetric key using XChaCha20‑Poly1305 with the derived key
  7. Concatenate the ephemeral public key, encrypted symmetric key, and encrypted payload

The encrypted output format is:


encrypted:<base64(ephemeral_pubkey || encrypted_symkey || ciphertext)>
      

4.5 Signature Process

Message signatures are computed over a canonical serialization of the envelope:

  1. Extract signing fields: from, to, keyId, ts, hash (and parent, payment, recipients if present)
  2. Serialize as canonical JSON (keys sorted alphabetically, no whitespace)
  3. Compute Ed25519 signature over the UTF‑8 bytes of the serialized JSON
  4. Encode signature as: ed25519:<base64-encoded-signature>

The canonical JSON ensures signature verification produces consistent results across implementations.

5. Message Format

5.1 InboxMessage Structure

A complete Inbox message consists of a version indicator, metadata envelope, and encrypted payload.


| Field    | Type          | Description                                   |
|----------|---------------|-----------------------------------------------|
| envelope | InboxEnvelope | Unencrypted routing and verification metadata |
| payload  | string        | Encrypted message content                     |
| v        | integer       | Protocol version (currently 1)                |
      

5.2 InboxEnvelope Structure

The envelope contains all information needed for routing, verification, and policy enforcement.


| Field      | Type    | Required | Description                                 |
|------------|---------|----------|---------------------------------------------|
| from       | string  | Yes      | Sender’s Dap name                           |
| hash       | string  | Yes      | BLAKE3 hash of encrypted payload            |
| keyId      | string  | Yes      | ID of recipient’s key used for encryption   |
| parent     | string  | No       | Hash of parent message for threading        |
| payment    | object  | No       | neue.cash payment proof (amount, tx)        |
| recipients | array   | No       | Other recipients’ Dap names (multi‑send)    |
| signature  | string  | Yes      | Sender’s Ed25519 signature                  |
| to         | string  | Yes      | Recipient address (name or name/subaddress) |
| ts         | integer | Yes      | Unix timestamp in milliseconds              |
      

5.3 InboxPayload Structure

The decrypted payload contains the actual message content.


| Field       | Type   | Required | Description                                 |
|-------------|--------|----------|---------------------------------------------|
| attachments | array  | No       | Array of InboxAttachment references         |
| body        | array  | Yes*     | Array of InboxBody content blocks           |
| editHistory | array  | No       | Previous version hashes for edits           |
| editOf      | string | No*      | Hash of message being edited                |
| emoji       | string | No*      | Emoji content for reactions                 |
| reactTo     | string | No*      | Hash of message being reacted to            |
| subject     | string | No       | Message subject line                        |
| type        | string | Yes      | "message", "reaction", "edit", or "retract" |
      

* Required fields depend on payload type. Messages require body; reactions require reactTo and emoji; edits require editOf and body.

5.4 InboxBody Structure

Message body content supports multiple representations for client compatibility.


| Field   | Type   | Description                                              |
|---------|--------|----------------------------------------------------------|
| content | string | The actual text content                                  |
| type    | string | MIME type: "text/plain", "text/html", or "text/markdown" |
      

A message body SHOULD include at least a text/plain representation for maximum compatibility. Additional representations (HTML, Markdown) may be included for rich display.

5.5 InboxAttachment Structure

Attachments are stored externally and referenced by hash.


| Field | Type    | Description                                        |
|-------|---------|----------------------------------------------------|
| hash  | string  | BLAKE3 hash of attachment content                  |
| mime  | string  | MIME type of the attachment                        |
| name  | string  | Original filename                                  |
| ref   | string  | URI where attachment can be retrieved              |
| size  | integer | File size in bytes                                 |
      

The hash allows recipients to verify attachment integrity after download. Attachments are not counted against message size limits since they are stored externally.

5.6 Multi‑Recipient Messages

To send a message to multiple recipients, the sender uses a fan‑out model:

  1. Generate a single symmetric key and encrypt the payload once
  2. For each recipient, encrypt the symmetric key with their public key
  3. Send individual messages to each recipient’s endpoint

Each message has its own envelope (different to address, keyId, signature) but shares the same encrypted payload content. This approach does not create a persistent group identity—each recipient sees the message independently.

Recipient visibility: When sending to multiple recipients, the sender MUST include the recipients field in the envelope listing all other recipients’ Dap names (excluding the current to address). This allows recipients to see who else received the message and reply‑all if desired.

Example envelope for a message sent to ada, grace, and turing (as received by grace):


{
  "from": "hopper",
  "hash": "blake3:Ynx7...",
  "keyId": "k1706812800",
  "recipients": ["ada", "turing"],
  "signature": "ed25519:Mx9a...",
  "to": "grace",
  "ts": 1707000000000
}
      

The recipients field is omitted for single‑recipient messages.

5.7 Attachment Retrieval

Attachments are stored externally and referenced by URI in the ref field. The protocol does not mandate a specific storage mechanism—implementations may use any URI scheme appropriate for their deployment.

Retrieval process:

  1. Client extracts the ref URI from the attachment metadata
  2. Client fetches the content from the URI using the appropriate protocol
  3. Client computes the BLAKE3 hash of the retrieved content
  4. Client verifies the hash matches the hash field in the attachment metadata
  5. If hashes do not match, the attachment MUST be rejected as corrupted or tampered

Authentication: When attachments are stored on authenticated endpoints, the sender SHOULD provide retrieval credentials through a secure channel (e.g., within the encrypted payload). The mechanism for credential exchange is implementation‑defined.

Recommended approach: Senders may host attachments on their own infrastructure with time‑limited signed URLs, or use a shared storage service agreed upon by communicating parties.

6. Delivery

6.1 Endpoint Discovery

Before sending a message, the sender must discover the recipient’s inbox endpoint:

  1. Parse the recipient address to extract the Dap name (portion before any "/")
  2. Resolve the Dap name through the Dap network
  3. Extract the endpoint URL from the InboxRecord
  4. Cache the record for future messages (recommended TTL: 24 hours)

6.2 Sending a Message

The complete message sending process:

  1. Resolve recipient’s Dap name to obtain InboxRecord
  2. Check recipient’s policy for allowlist and fee requirements
  3. If stranger fee is required, obtain neue.cash payment proof
  4. Select the recipient’s current encryption key
  5. Construct and encrypt the payload
  6. Compute the BLAKE3 hash of the encrypted payload
  7. Construct the envelope with all required fields
  8. Sign the canonical envelope using sender’s Ed25519 key
  9. POST the complete InboxMessage to the recipient’s endpoint

6.3 Endpoint API

The recipient’s endpoint MUST implement the following HTTP endpoint:

POST /messages
Request body: A complete InboxMessage as JSON
Content type: application/json

The endpoint processes the message and returns an InboxDeliveryResponse indicating acceptance, queuing, or rejection.

6.4 Validation Order

Recipients MUST validate incoming messages in the following order:

  1. Timestamp window: Reject if ts is more than 5 minutes in the future or 7 days in the past
  2. Duplicate check: Reject if the message hash has been seen before
  3. Signature verification: Resolve sender’s Dap name and verify signature
  4. Key validity: Check that keyId is current, in previous (not expired), and not revoked
  5. Policy checks: Verify sender is allowlisted or payment is provided
  6. Size limit: Verify total message size does not exceed maxSize

This order ensures efficient rejection of invalid messages before expensive operations.

6.5 Response Format

The endpoint returns one of three response types:

Accepted:


{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "status": "accepted" }
      

Queued (for delayed processing):


{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "reason": "rate limited", "status": "queued" }
      

Rejected:


{ "error": "PAYMENT_REQUIRED", "message": "Stranger fee of 0.001 required", "status": "rejected" }
      

6.6 Message ID Generation

Upon accepting a message, the recipient’s endpoint generates a unique message ID for tracking and reference purposes.

Message IDs use the following format:


msg:<nanoid>
      

Where <nanoid> is a 21‑character identifier generated using the Nano ID algorithm with the default alphabet (A-Za-z0-9_-).

Example: msg:V1StGXR8_Z5jdHi6B-myT

The message ID is returned in the delivery response and may be used for:

Message IDs are opaque to senders and SHOULD NOT be used for cryptographic purposes. The canonical identifier for a message remains its content hash.

7. Replay Protection

7.1 Timestamp Validation

Message timestamps are validated against the following bounds:

Messages with timestamps outside this window are rejected with TIMESTAMP_INVALID. This prevents replay attacks using old messages while accommodating reasonable clock skew between systems.

7.2 Hash Deduplication

Recipients MUST maintain a record of seen message hashes to prevent duplicate delivery:

The combination of hash and timestamp in the signature prevents attackers from modifying timestamps on captured messages.

7.3 Signature Binding

The sender’s signature covers both the timestamp and the payload hash, creating a binding that prevents manipulation:

An attacker who captures a message cannot replay it after the MAX_AGE window expires, and cannot modify the timestamp because it would invalidate the signature.

8. Key Management

8.1 Key Rotation

Recipients SHOULD rotate their keys periodically (recommended: every 6-12 months). The rotation process:

  1. Generate a new X25519/Ed25519 keypair
  2. Create a new key entry with fresh id, created timestamp
  3. Move the current key to the previous array with an expires timestamp
  4. Update the Dap record with the new key configuration
  5. Retain the old private key until its expiration to decrypt in‑flight messages

The overlap period (typically 30 days) ensures messages encrypted with the old key can still be decrypted during the transition.

8.2 Key Revocation

If a private key is compromised, the recipient MUST revoke it immediately:

  1. Add the compromised key’s ID to the revoked array
  2. Securely delete the compromised private key material
  3. Update the Dap record

Messages encrypted with a revoked key are rejected with KEY_REVOKED. Senders receiving this error MUST refresh the recipient’s record and retry with the current key.

8.3 Sender Key Caching

Senders SHOULD cache recipient key information to reduce resolution overhead:

8.4 Sender Retry Logic

On receiving a key‑related error, senders should implement the following retry behavior:


| Error       | Action                                           |
|-------------|--------------------------------------------------|
| KEY_EXPIRED | Refresh recipient record, retry with current key |
| KEY_REVOKED | Refresh recipient record, retry with current key |
| KEY_UNKNOWN | Refresh recipient record, retry with current key |
      

After refreshing, the sender should retry the message once. Repeated failures indicate a persistent problem and should be reported to the user.

9. Policy Enforcement

9.1 Allowlists

Recipients may maintain an allowlist of trusted senders. Allowlist behavior:

The allowlist API (authenticated):

Authentication for these management endpoints is implementation‑defined and outside the scope of this specification.

9.2 Blocklists

Recipients may block specific senders. Blocklist behavior depends on blockBehavior:

Silent blocking prevents senders from confirming they are blocked. The blocklist API follows the same pattern as allowlists (GET/POST/DELETE /blocklist).

Authentication for these management endpoints is implementation‑defined and outside the scope of this specification.

9.3 Stranger Fees

The stranger fee mechanism provides economic spam resistance:

  1. Sender checks if they are on the recipient’s allowlist (if known)
  2. If not allowlisted and strangerFee > 0, sender must include payment
  3. Payment proof is included in envelope.payment: { amount, tx }
  4. Recipient validates payment through neue.cash before accepting message

The payment amount MUST be at least equal to the strangerFee specified in the policy. Subaddresses may override the fee (e.g., higher fee for business inquiries).

9.4 Size Limits

The maxSize policy field specifies the maximum total size of a message in bytes:

Messages exceeding the size limit are rejected with SIZE_EXCEEDED.

10. Threading

10.1 Parent References

Messages may reference a parent message by including the parent’s hash in the envelope:


"parent": "blake3:Ynx7..."
      

Parent references form a directed acyclic graph (DAG), similar to a Merkle tree. A message may only have one direct parent, but the graph allows branching when multiple parties reply to the same message.

10.2 Thread Reconstruction

Clients reconstruct conversation threads locally:

  1. Fetch all messages from a conversation
  2. Index messages by their hash
  3. Build a tree structure following parent references
  4. Display messages in chronological or threaded order

No server‑side thread state is required—threads are implicit in the message graph.

10.3 Reactions

Reactions are lightweight responses that reference another message:


{
  "emoji": "👍",
  "reactTo": "blake3:Ynx7...",
  "type": "reaction"
}
      

The reactTo field contains the hash of the message being reacted to. Multiple reactions from the same sender replace previous reactions (last one wins).

10.4 Edits

Edits allow senders to modify previously sent messages:


{
  "body": [{ "content": "Updated content", "type": "text/plain" }],
  "editHistory": ["blake3:Ynx7..."],
  "editOf": "blake3:Ynx7...",
  "type": "edit"
}
      

The editHistory array contains hashes of all previous versions, allowing clients to show edit history if desired. Only the original sender may edit a message.

10.5 Retractions

Retractions signal that the sender wishes to withdraw a message:


{
  "editOf": "blake3:Ynx7...",
  "type": "retract"
}
      

Recipients MAY honor retractions by hiding the original message, but are not required to delete it. The retraction serves as a signal of sender intent, not a guarantee of removal.

11. Errors

11.1 Error Codes

The following error codes are defined for message rejection:


| Code              | Description                                                |
|-------------------|------------------------------------------------------------|
| BLOCKED           | Sender is on recipient’s blocklist                         |
| DUPLICATE         | Message hash has been seen before                          |
| KEY_EXPIRED       | Encryption key found in previous array but past expiration |
| KEY_REVOKED       | Encryption key is in revoked list                          |
| KEY_UNKNOWN       | Encryption key ID not recognized                           |
| PAYMENT_INVALID   | Payment proof failed validation                            |
| PAYMENT_REQUIRED  | Stranger fee required but not provided                     |
| POLICY_VIOLATION  | Generic policy failure (unspecified reason)                |
| SIGNATURE_INVALID | Sender signature verification failed                       |
| SIZE_EXCEEDED     | Message exceeds `maxSize` limit                            |
| TIMESTAMP_INVALID | Timestamp outside acceptable window                        |
      

11.2 Error Response Format

Error responses follow this structure:


{
  "error": "<code>",
  "message": "<optional human-readable detail>",
  "status": "rejected"
}
      

The message field is optional and provides additional context for debugging. Implementations SHOULD NOT expose internal details that could aid attackers.

11.3 Retry Guidance

Errors are classified as retryable or terminal:

Retryable after refresh:

Terminal (do not retry):

12. Security Considerations

12.1 Threat Model

The Inbox Protocol is designed to protect against the following adversaries:

Passive network observers: Attackers who can observe network traffic but cannot modify it. They may attempt to learn message contents or metadata.

Active MITM attackers: Attackers who can intercept and modify network traffic. They may attempt to impersonate senders or recipients.

Compromised endpoints: Recipient servers that may be malicious or compromised. They have access to metadata but should not be able to read encrypted content.

Malicious senders: Attackers who attempt to send spam, phishing, or abusive content to recipients.

12.2 Mitigations

The protocol provides the following protections:


| Threat            | Mitigation                                          |
|-------------------|-----------------------------------------------------|
| Passive observers | End-to-end encryption (XChaCha20-Poly1305)          |
| MITM attacks      | Signatures + Dap resolution for sender verification |
| Key compromise    | Key rotation + revocation mechanisms                |
| Spam/abuse        | Allowlists + stranger fees + blocklists             |
      

12.3 Known Limitations

Version 1.0 of the protocol has the following known limitations:

12.4 Recommendations

Implementations SHOULD follow these security practices:

13. Extensibility

13.1 Protocol Versioning

The v field in InboxMessage indicates the protocol version. Version handling rules:

13.2 Payload Type Extensions

New payload types may be introduced beyond the core set (edit, message, reactionretract):

13.3 Reserved Fields

Fields prefixed with x- are reserved for experimental and extension use:

14. References

14.1 Normative References

The following specifications are required for implementation:

14.2 Informative References

The following specifications informed the design of this protocol:

Appendix A: Type Definitions (TypeScript)

The following TypeScript definitions provide a normative reference for implementers:

A.1 Dap Record Types


interface InboxRecord {
  endpoint: string;
  keys: InboxKeys;
  policy: InboxPolicy;
}

interface InboxKeys {
  current: {
    created: number;
    encrypt: string;
    id: string;
    verify: string;
  };
  previous?: Array<{
    created: number;
    encrypt: string;
    expires: number;
    id: string;
    verify: string;
  }>;
  revoked?: string[];
}

interface InboxPolicy {
  allowlistOnly: boolean;
  blockBehavior?: "reject" | "silent";
  blocklist?: string[];
  maxSize: number;
  strangerFee: string;
  subaddresses?: Record>;
}
      

A.2 Message Types


interface InboxMessage {
  envelope: InboxEnvelope;
  payload: string;
  v: 1;
}

interface InboxEnvelope {
  from: string;
  hash: string;
  keyId: string;
  parent?: string;
  payment?: {
    amount: string;
    tx: string;
  };
  recipients?: string[];
  signature: string;
  to: string;
  ts: number;
}

interface InboxPayload {
  attachments?: InboxAttachment[];
  body: InboxBody[];
  editHistory?: string[];
  editOf?: string;
  emoji?: string;
  reactTo?: string;
  subject?: string;
  type: "edit" | "message" | "reaction" | "retract";
}

interface InboxBody {
  content: string;
  type: "text/html" | "text/markdown" | "text/plain";
}

interface InboxAttachment {
  hash: string;
  mime: string;
  name: string;
  ref: string;
  size: number;
}
      

A.3 Response Types


type InboxDeliveryResponse =
  | { id: string; status: "accepted" }
  | { id: string; reason: string; status: "queued" }
  | { error: InboxError; message?: string; status: "rejected" };

type InboxError =
  | "BLOCKED"
  | "DUPLICATE"
  | "KEY_EXPIRED"
  | "KEY_REVOKED"
  | "KEY_UNKNOWN"
  | "PAYMENT_INVALID"
  | "PAYMENT_REQUIRED"
  | "POLICY_VIOLATION"
  | "SIGNATURE_INVALID"
  | "SIZE_EXCEEDED"
  | "TIMESTAMP_INVALID";
      

Appendix B: Example Messages

B.1 Simple Message


{
  "envelope": {
    "from": "ada",
    "hash": "blake3:Ynx7K9mB2vP...",
    "keyId": "k1706812800",
    "signature": "ed25519:Mx9aQ2kL7nR...",
    "to": "grace",
    "ts": 1707000000000
  },
  "payload": "encrypted:Qm9x4WvR8mN...",
  "v": 1
}
      

B.2 Reply with Payment


{
  "envelope": {
    "from": "ada",
    "hash": "blake3:7FabX3nK2wQ...",
    "keyId": "k1706812800",
    "parent": "blake3:Ynx7K9mB2vP...",
    "payment": {
      "amount": "0.01",
      "tx": "nc:tx:8QmzR4kL9vB..."
    },
    "signature": "ed25519:Kp2bM7nR4wX...",
    "to": "grace/business",
    "ts": 1707000060000
  },
  "payload": "encrypted:Rm4yK8vN2qP...",
  "v": 1
}
      

B.3 Multi‑Recipient Message


{
  "envelope": {
    "from": "hopper",
    "hash": "blake3:Pq8rN2mK4vX...",
    "keyId": "k1706812800",
    "recipients": ["ada", "turing"],
    "signature": "ed25519:Wx7bN4kR2mQ...",
    "to": "grace",
    "ts": 1707000120000
  },
  "payload": "encrypted:Kn5xR8vM2pL...",
  "v": 1
}
      

B.4 Decrypted Payload Example


{
  "attachments": [
    {
      "hash": "blake3:9BncX4mR2kL...",
      "mime": "application/pdf",
      "name": "agenda.pdf",
      "ref": "https://files.hopper.dap/a/9BncX4mR2kL?token=eyJhbG...",
      "size": 24576
    }
  ],
  "body": [
    {
      "content": "Are we still on for 3pm?",
      "type": "text/plain"
    }
  ],
  "subject": "Meeting tomorrow",
  "type": "message"
}
      

Appendix C: Endpoint API Reference

C.1 POST /messages

Deliver a message to the recipient.

Request:


POST /messages HTTP/1.1
Content-Type: application/json

<InboxMessage JSON>
      

Response (accepted):


HTTP/1.1 200 OK
Content-Type: application/json

{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "status": "accepted" }
      

Response (rejected):


HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "PAYMENT_REQUIRED",
  "message": "Stranger fee of 0.001 required",
  "status": "rejected"
}
      

C.2 Allowlist Management (Authenticated)

GET /allowlist
Retrieve the recipient’s allowlist entries.

POST /allowlist
Add a name to the allowlist.


{ "name": "ada" }
      

DELETE /allowlist
Remove a name from the allowlist.


{ "name": "ada" }
      

C.3 Blocklist Management (Authenticated)

The blocklist API follows the same pattern as allowlist management:

Appendix D: Canonical JSON Serialization

For signature generation and verification, envelope fields MUST be serialized in canonical form to ensure consistent results across implementations.

D.1 Serialization Rules

  1. Object keys MUST be sorted alphabetically (lexicographic UTF‑8 order)
  2. No whitespace between tokens
  3. No trailing commas
  4. UTF‑8 encoding throughout
  5. Optional fields (parent, payment, recipients) included only when present
  6. Numbers serialized without unnecessary precision

D.2 Signed Fields

The following envelope fields are included in the signature (when present):

D.3 Example

Given an envelope with all fields present:
Canonical form:


{"from":"ada","hash":"blake3:Ynx7...","keyId":"k1706812800","parent":"blake3:Abc1...","payment":{"amount":"0.01","tx":"nc:tx:8Qmz..."},"recipients":["grace","turing"],"to":"hopper","ts":1707000000000}
      

Note: The above is shown on one line. The actual canonical form contains no whitespace or line breaks.