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

# Your first OpenPayload requests

> Resolve a public DID, create an identity locally, and send a live Hello World message

This guide begins with a read-only request. It then creates an OpenPayload identity entirely on your machine and finishes by sending a minimal live message through a Relay.

For OpenPayload-operated services, use the [public alpha endpoints](/public-alpha-endpoints). The examples below keep placeholder hostnames so you can also follow this guide with another operator.

## Choose your starting point

| Your goal | Start here | Private key needed? |
| - | - | - |
| See what an OpenPayload API response looks like | Read a public DID below | No |
| Build an identity and keys | Continue to **Generate the identity** | Yes, generated locally |
| Send someone a `Hello World!` | Continue to **Send a live Relay message** | No, for this unsigned smoke test |
| Send an encrypted message | Read [Build a DDN message envelope](/guides/ddn-envelope) | Recipient public keys only |

## Read a public DID first

A terminal is the text-based application where you type commands. On macOS it is named **Terminal**. On Windows you can use **PowerShell**. On Linux it may be named **Terminal**, **Console**, or **Shell**.

Ask your Directory operator for:

* Its HTTPS base URL
* A confirmed public OpenPayload DID you may use for testing

Then run:

```bash theme={null}
curl --request GET \
  --url "https://directory.example.com/resolve/did%3Aopenpayload%3A1111111111111111111111"
```

Replace both placeholders. This request is read-only. It does not create an identity, spend funds, or modify network state.

If you receive JSON containing `document`, `aliases`, `devices`, and `registration_status`, your connection is working. A `404` means the sample DID is not registered on that Directory; it does not necessarily mean your command is malformed.

## Create an identity locally

The remaining steps generate an Ed25519 keypair, build a public DID document, and sign a registration payload. A **public key** may be shared. A **private key** proves control of the identity and must remain secret.

<Note>
  `POST /register-did` creates an ownerless DID using the root-key registration proof. Later Directory updates
  use the root or another authorized DID key; they do not require a chain account.
</Note>

<Warning>
  Your private key controls the identity. Never print it, commit it, include it in a request, or send it to the Directory.
</Warning>

## Prerequisites

Choose either the Bash or Python workflow.

<Tabs>
  <Tab title="Bash and OpenSSL">
    Install the following command-line tools:

    * Bash or another POSIX-compatible shell
    * OpenSSL 3 with Ed25519 support
    * Python 3 for Base58 encoding using only the standard library
    * `jq` for constructing JSON
    * `curl` for submitting the request

    Confirm that OpenSSL exposes Ed25519:

    ```bash theme={null}
    openssl list -public-key-algorithms | grep -i ED25519
    ```

    If this command returns no match, install or select OpenSSL 3 before continuing. The LibreSSL version bundled with some operating systems may not support the commands in this guide.
  </Tab>

  <Tab title="Python">
    Install Python 3.9 or later, then create an isolated environment:

    ```bash theme={null}
    python3 -m venv .openpayload-venv
    source .openpayload-venv/bin/activate
    python -m pip install --upgrade pip
    python -m pip install cryptography
    ```

    The Python workflow uses `cryptography` for Ed25519 key generation and signing. Base58 encoding and JSON generation use the Python standard library.
  </Tab>
</Tabs>

## Set the Directory URL

Replace the placeholder with the HTTPS URL supplied by your Directory operator:

```bash theme={null}
export OPENPAYLOAD_DIRECTORY_URL="https://directory.example.com"
```

The generation steps do not contact the Directory. You use this URL only when you submit or resolve the DID.

## Generate the identity

<Tabs>
  <Tab title="Bash and OpenSSL">
    Create `generate-openpayload-identity.sh` with the following content:

    ```bash theme={null}
    #!/usr/bin/env bash
    set -euo pipefail

    umask 077
    IDENTITY_DIR="${1:-openpayload-identity}"

    for required_command in openssl jq python3; do
      if ! command -v "$required_command" >/dev/null 2>&1; then
        echo "Missing required command: $required_command" >&2
        exit 1
      fi
    done

    if ! openssl list -public-key-algorithms 2>/dev/null | grep -qi ED25519; then
      echo "OpenSSL does not report Ed25519 support. Install or select OpenSSL 3." >&2
      exit 1
    fi

    mkdir -p "$IDENTITY_DIR"
    chmod 700 "$IDENTITY_DIR"

    base58btc() {
      python3 -c '
    import sys

    alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    data = sys.stdin.buffer.read()
    number = int.from_bytes(data, "big")
    encoded = ""

    while number:
        number, remainder = divmod(number, 58)
        encoded = alphabet[remainder] + encoded

    leading_zeroes = len(data) - len(data.lstrip(b"\x00"))
    sys.stdout.write("1" * leading_zeroes + encoded)
    '
    }

    # Create the Ed25519 private and public keys.
    openssl genpkey \
      -algorithm ED25519 \
      -out "$IDENTITY_DIR/private-key.pem"

    openssl pkey \
      -in "$IDENTITY_DIR/private-key.pem" \
      -pubout \
      -out "$IDENTITY_DIR/public-key.pem"

    # An OpenSSL 3 Ed25519 SubjectPublicKeyInfo value ends with the raw 32-byte key.
    openssl pkey \
      -in "$IDENTITY_DIR/private-key.pem" \
      -pubout \
      -outform DER \
      | tail -c 32 > "$IDENTITY_DIR/public-key.raw"

    PUBLIC_KEY_BYTES="$(wc -c < "$IDENTITY_DIR/public-key.raw" | tr -d " ")"
    if [ "$PUBLIC_KEY_BYTES" != "32" ]; then
      echo "Expected a 32-byte Ed25519 public key, received $PUBLIC_KEY_BYTES bytes." >&2
      exit 1
    fi

    # Create a random 128-bit identifier and encode it with the Base58 alphabet.
    openssl rand 16 > "$IDENTITY_DIR/did-entropy.bin"
    DID_IDENTIFIER="$(base58btc < "$IDENTITY_DIR/did-entropy.bin")"
    DID="did:openpayload:$DID_IDENTIFIER"
    KEY_ID="$DID#root"
    PUBLIC_KEY_MULTIBASE="z$(base58btc < "$IDENTITY_DIR/public-key.raw")"

    # Build the public DID document.
    jq -n \
      --arg did "$DID" \
      --arg key_id "$KEY_ID" \
      --arg public_key "$PUBLIC_KEY_MULTIBASE" \
      '{
        "@context": "https://www.w3.org/ns/did/v1",
        id: $did,
        verificationMethod: [
          {
            id: $key_id,
            type: "Ed25519VerificationKey2020",
            controller: $did,
            publicKeyMultibase: $public_key
          }
        ],
        authentication: [$key_id],
        keyAgreement: [],
        service: []
      }' > "$IDENTITY_DIR/did-document.json"

    # Sign the public registration payload: DID, a pipe, and the exact timestamp.
    TIMESTAMP="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
    printf '%s' "$DID|$TIMESTAMP" > "$IDENTITY_DIR/registration-payload.txt"

    openssl pkeyutl \
      -sign \
      -rawin \
      -inkey "$IDENTITY_DIR/private-key.pem" \
      -in "$IDENTITY_DIR/registration-payload.txt" \
      -out "$IDENTITY_DIR/registration-signature.bin"

    SIGNATURE="$(openssl base64 -A -in "$IDENTITY_DIR/registration-signature.bin")"

    jq -n \
      --arg did "$DID" \
      --arg timestamp "$TIMESTAMP" \
      --arg signature "$SIGNATURE" \
      --arg root_pubkey "$PUBLIC_KEY_MULTIBASE" \
      --slurpfile document "$IDENTITY_DIR/did-document.json" \
      '{
        did: $did,
        timestamp: $timestamp,
        signature: $signature,
        root_pubkey: $root_pubkey,
        did_document: $document[0]
      }' > "$IDENTITY_DIR/registration.json"

    printf 'DID: %s\n' "$DID"
    printf 'DID document: %s/did-document.json\n' "$IDENTITY_DIR"
    printf 'Registration request: %s/registration.json\n' "$IDENTITY_DIR"
    printf 'Private key: %s/private-key.pem\n' "$IDENTITY_DIR"
    ```

    Make the script executable and run it:

    ```bash theme={null}
    chmod +x generate-openpayload-identity.sh
    ./generate-openpayload-identity.sh
    ```
  </Tab>

  <Tab title="Python">
    Create `generate_openpayload_identity.py` with the following content:

    ```python theme={null}
    #!/usr/bin/env python3
    import argparse
    import base64
    import json
    import os
    import secrets
    from datetime import datetime, timezone
    from pathlib import Path

    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey


    BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


    def base58btc(data: bytes) -> str:
        number = int.from_bytes(data, "big")
        encoded = ""

        while number:
            number, remainder = divmod(number, 58)
            encoded = BASE58_ALPHABET[remainder] + encoded

        leading_zeroes = len(data) - len(data.lstrip(b"\x00"))
        return "1" * leading_zeroes + encoded


    def write_json(path: Path, value: dict) -> None:
        path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")


    def main() -> None:
        parser = argparse.ArgumentParser(
            description="Generate an OpenPayload DID, Ed25519 keypair, and registration request."
        )
        parser.add_argument(
            "--output",
            default="openpayload-identity",
            help="Directory for generated files (default: openpayload-identity)",
        )
        args = parser.parse_args()

        output = Path(args.output)
        output.mkdir(mode=0o700, parents=True, exist_ok=True)
        os.chmod(output, 0o700)

        private_key = Ed25519PrivateKey.generate()
        public_key = private_key.public_key()

        private_pem = private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        )
        public_pem = public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        public_raw = public_key.public_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PublicFormat.Raw,
        )

        private_path = output / "private-key.pem"
        private_path.write_bytes(private_pem)
        os.chmod(private_path, 0o600)
        (output / "public-key.pem").write_bytes(public_pem)
        (output / "public-key.raw").write_bytes(public_raw)

        did_identifier = base58btc(secrets.token_bytes(16))
        did = f"did:openpayload:{did_identifier}"
        key_id = f"{did}#root"
        public_key_multibase = "z" + base58btc(public_raw)

        document = {
            "@context": "https://www.w3.org/ns/did/v1",
            "id": did,
            "verificationMethod": [
                {
                    "id": key_id,
                    "type": "Ed25519VerificationKey2020",
                    "controller": did,
                    "publicKeyMultibase": public_key_multibase,
                }
            ],
            "authentication": [key_id],
            "keyAgreement": [],
            "service": [],
        }

        timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
        payload = f"{did}|{timestamp}".encode("utf-8")
        signature = base64.b64encode(private_key.sign(payload)).decode("ascii")

        registration = {
            "did": did,
            "timestamp": timestamp,
            "signature": signature,
            "root_pubkey": public_key_multibase,
            "did_document": document,
        }

        write_json(output / "did-document.json", document)
        write_json(output / "registration.json", registration)
        (output / "registration-payload.txt").write_bytes(payload)

        print(f"DID: {did}")
        print(f"DID document: {output / 'did-document.json'}")
        print(f"Registration request: {output / 'registration.json'}")
        print(f"Private key: {private_path}")


    if __name__ == "__main__":
        main()
    ```

    Run the CLI:

    ```bash theme={null}
    python generate_openpayload_identity.py
    ```

    To choose a different output directory:

    ```bash theme={null}
    python generate_openpayload_identity.py --output my-openpayload-identity
    ```
  </Tab>
</Tabs>

## Review the generated files

<Note>
  This quickstart creates a minimal identity with empty `keyAgreement` and `service` arrays. That identity can be registered, but it does not yet advertise an encryption key, Relay, or Cache. Before using it for messaging, publish the components shown in [A complete messaging DID document](/concepts/complete-did-document).
</Note>

Both workflows create the same artifacts:

<Tree>
  <Folder name="openpayload-identity" defaultOpen>
    <File name="private-key.pem" />

    <File name="public-key.pem" />

    <File name="public-key.raw" />

    <File name="did-document.json" />

    <File name="registration-payload.txt" />

    <File name="registration.json" />
  </Folder>
</Tree>

| File | Purpose | Safe to publish? |
| - | - | - |
| `private-key.pem` | Signs requests that control the DID. | No |
| `public-key.pem` | Standard PEM representation of the public key. | Yes |
| `public-key.raw` | Raw 32-byte Ed25519 public key. | Yes |
| `did-document.json` | Public DID document submitted to the Directory. | Yes |
| `registration-payload.txt` | Exact public payload signed for registration. | Yes |
| `registration.json` | Complete public registration request and signature. | Yes |

Inspect the DID document and registration body before submission:

```bash theme={null}
jq . openpayload-identity/did-document.json
jq . openpayload-identity/registration.json
```

Confirm that these values agree:

* `registration.json.did` equals `did_document.id`.
* The first `verificationMethod.id` starts with the same DID.
* `controller` equals the DID.
* `root_pubkey` equals the first `publicKeyMultibase` value.
* `registration.json` does not contain the private key.

## Register the DID

Submit the generated request:

```bash theme={null}
curl --fail-with-body \
  --request POST \
  --url "$OPENPAYLOAD_DIRECTORY_URL/register-did" \
  --header "Content-Type: application/json" \
  --data @openpayload-identity/registration.json
```

A successful submission returns `202 Accepted` with a transaction identifier and a pending registration status:

```json theme={null}
{
  "status": "did registration pending",
  "registration_status": "pending",
  "tx_id": "<transaction-id>",
  "did": "did:openpayload:<identifier>",
  "did_document": {
    "id": "did:openpayload:<identifier>"
  }
}
```

<Note>
  `202 Accepted` means the Directory accepted the registration for processing. It does not mean the registration is confirmed.
</Note>

## Wait for confirmation

Read the DID from the registration file and URL-encode it for the path:

```bash theme={null}
DID="$(jq -r '.did' openpayload-identity/registration.json)"
ENCODED_DID="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$DID")"
```

Check the registration state:

```bash theme={null}
curl --fail-with-body \
  --url "$OPENPAYLOAD_DIRECTORY_URL/registration-status/$ENCODED_DID" \
  | jq .
```

The response reports one of these states:

| State | What to do |
| - | - |
| `confirmed` | Continue to resolve the DID. |
| `pending` | Wait briefly, then check again. |
| `missing` | The DID is not registered on chain. Review the registration response before resubmitting. |

A `502` response means the Directory could not verify chain state. Retry the status request; do not interpret it as `missing`.

## Resolve the DID

Resolve the public record:

```bash theme={null}
curl --fail-with-body \
  --url "$OPENPAYLOAD_DIRECTORY_URL/resolve/$ENCODED_DID" \
  | jq .
```

If resolution returns `425` with `recipient_registration_pending`, check registration status again. A confirmed record returns `200` and includes:

```json theme={null}
{
  "document": {
    "id": "did:openpayload:<identifier>"
  },
  "registration_status": "confirmed"
}
```

## Send a live Relay message

You can now use the confirmed DID as both the sender's test destination and the WebSocket recipient. This keeps the first message focused on the live Relay path:

1. The recipient opens a WebSocket session with the Relay.
2. You compose a minimal `Hello World!` envelope.
3. You submit the envelope to `POST /relay`.
4. The Relay delivers it to the connected WebSocket.

<Note>
  WebSocket is the live receiving path in this example. Envelope submission uses `POST /relay`; the Relay does not accept outbound envelopes as WebSocket frames.
</Note>

### Set the Relay URLs

Ask your Relay operator for its HTTPS and WebSocket base URLs, then set both values:

```bash theme={null}
export OPENPAYLOAD_RELAY_URL="https://relay.example.com"
export OPENPAYLOAD_RELAY_WS_URL="wss://relay.example.com"
```

This example does not configure or call a Cache service. Keep the recipient WebSocket connected while you submit the message.

### Open the recipient WebSocket

Open a second terminal and connect the DID you registered above. The device identifier `hello-world` is local to this live session.

This command uses `npx`, which is included with Node.js. You can use another WebSocket client if you prefer.

```bash theme={null}
DID="$(jq -r '.did' openpayload-identity/registration.json)"
ENCODED_DID="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$DID")"

npx --yes wscat \
  --connect "$OPENPAYLOAD_RELAY_WS_URL/ws/$ENCODED_DID/hello-world"
```

The Relay sends a welcome frame after the connection opens:

```json theme={null}
{
  "type": "welcome",
  "status": "ok",
  "connectionKey": "did:openpayload:<identifier>::hello-world"
}
```

Leave this terminal open.

### Compose the smallest test envelope

Return to your first terminal. Generate a new message identifier and current timestamp, then write the envelope:

```bash theme={null}
jq -n \
  --arg message_id "$(python3 -c 'import uuid; print(uuid.uuid4())')" \
  --arg to "$DID" \
  --arg timestamp "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
  '{
    message_id: $message_id,
    to: $to,
    timestamp: $timestamp,
    payload: {text: "Hello World!"},
    context: null
  }' > hello-world-envelope.json

jq . hello-world-envelope.json
```

The resulting envelope contains only the fields needed for this live transport smoke test:

```json theme={null}
{
  "message_id": "<generated-uuid>",
  "to": "did:openpayload:<identifier>",
  "timestamp": "<current-utc-time>",
  "payload": {
    "text": "Hello World!"
  },
  "context": null
}
```

<Warning>
  This intentionally readable payload is only for a public, non-sensitive smoke test. Base64 encoding is not encryption. Encrypt the payload and protected context before sending real data; continue with [Build a DDN message envelope](/guides/ddn-envelope) for that flow.
</Warning>

### Submit the message

Send the envelope to the Relay:

```bash theme={null}
curl --fail-with-body \
  --request POST \
  --url "$OPENPAYLOAD_RELAY_URL/relay" \
  --header "Content-Type: application/json" \
  --data @hello-world-envelope.json \
  | jq .
```

A successful live delivery reports the WebSocket mode and confirms that no Cache fallback was attempted:

```json theme={null}
{
  "success": true,
  "delivery_mode": "local_websocket",
  "http_status": 200,
  "code": "delivered",
  "message": "Delivered via local WebSocket.",
  "retry_count": 0,
  "cache_attempted": false
}
```

Your WebSocket terminal receives the same DDN envelope, including:

```json theme={null}
{
  "payload": {
    "text": "Hello World!"
  }
}
```

If the Relay does not return `local_websocket`, confirm that the WebSocket is still connected under the same DID and that the DID registration is finalized.

## Protect the private key

After registration:

1. Move `private-key.pem` into your secret manager or encrypted key store.
2. Keep at least one secure recovery copy.
3. Do not commit the generated directory to source control.
4. Delete unneeded binary intermediates after you confirm your backup and signing workflow.

Add the generated directory to `.gitignore`:

```gitignore theme={null}
openpayload-identity/
```

<Card title="Explore the Directory API" icon="book-open" href="/openpayload-directory-api">
  Continue with alias, device, DID document, and delivery policy endpoints.
</Card>


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