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

# Authorize a Directory update

> A beginner-friendly guide to nonces, canonical payloads, and signatures

Read-only requests do not require your private key. Updates do, because a Directory must distinguish the DID controller from somebody merely claiming to be the controller.

Authorization happens entirely on your device:

1. Read the current nonce.
2. Build the operation's canonical payload.
3. Sign the exact payload bytes with an authorized Ed25519 key.
4. Send the public fields, encoded payload, and signature.

The Directory receives no private key.

## Nonces prevent replay

A nonce is a counter associated with a DID authorization domain. Once a nonce is used successfully, another request cannot reuse it.

Fetch current values with:

```http theme={null}
GET /dids/{did}/nonces
```

Use the nonce named for the operation you are building. If another update is submitted first, fetch the values again and rebuild the signature.

| Response field | Operations |
| - | - |
| `alias_nonce` | Add, update, or remove an alias |
| `document_nonce` | Verification methods, authentication, key agreement, and services |
| `root_pubkey_nonce` | Rotate the root public key |
| `control_nonce` | Devices and DID deactivation |
| `policy_nonce` | Create, replace, or delete the DID policy and set DID delivery constraints |

Existing Persona operations have separate counters. Fetch them with:

```http theme={null}
GET /personas/{persona}/nonces
```

The nonce route returns `404` before initial registration. For registration, use the `challenge_nonce` and `operator_nonce` returned by the DNS-challenge response. For an existing Persona, use `persona_nonce` for DNS challenge and attestation replay protection, `operator_nonce` for signed Persona registration-state operations, and `policy_nonce` for that Persona's policy and delivery constraints. `operator_nonce` belongs to the operator DID rather than one Persona, so another Persona operation under the same operator can make it stale. A Persona transfer challenge binds both the current `persona_nonce` and the current operator's `operator_nonce`.

## Expiration limits stolen requests

`valid_until` is an unsigned 64-bit Unix timestamp in **milliseconds** for SCALE authorization payloads. Use a short future window and make sure the machine signing the request has an accurate clock.

## Canonical payloads are bytes

For each signed chain-state mutation that carries a proof, `canonical_payload` is the exact SCALE-encoded authorization structure. DNS challenge creation and verification prepare these operations but do not themselves carry a caller signature. Send a canonical payload as:

* Hex beginning with `0x`, or
* Standard or URL-safe Base64

The Directory decodes the value, reconstructs the expected structure from the other request fields, and compares the bytes. Signing a JSON object or a visually similar string will not work.

The document authorization field order is:

| Position | SCALE value |
| - | - |
| 1 | DID as `Vec<u8>` UTF-8 |
| 2 | action discriminant as `u8` |
| 3 | optional verification method |
| 4 | optional key-agreement method |
| 5 | optional service |
| 6 | optional component ID |
| 7 | nonce as `u64` |
| 8 | `valid_until` as `u64` milliseconds |
| 9 | signer key ID as `Vec<u8>` UTF-8 |

Each endpoint page identifies which optional item is populated.

Alias payloads use this field order:

| Position | SCALE value |
| - | - |
| 1 | DID as `Vec<u8>` UTF-8 |
| 2 | action enum: add `0`, update `1`, remove `2` |
| 3 | optional alias |
| 4 | optional old alias |
| 5 | optional new alias |
| 6 | `alias_nonce` as `u64` |
| 7 | `valid_until` as `u64` milliseconds |
| 8 | signer key ID as `Vec<u8>` UTF-8 |

Root-public-key rotation uses a distinct payload:

| Position | SCALE value |
| - | - |
| 1 | DID as `Vec<u8>` UTF-8 |
| 2 | New 32-byte root public key as `Vec<u8>` |
| 3 | `root_pubkey_nonce` as `u64` |
| 4 | `valid_until` as `u64` milliseconds |
| 5 | signer key ID as `Vec<u8>` UTF-8 |

Device and deactivation payloads use:

| Position | SCALE value |
| - | - |
| 1 | DID as `Vec<u8>` UTF-8 |
| 2 | control action enum and its fields |
| 3 | `control_nonce` as `u64` |
| 4 | `valid_until` as `u64` milliseconds |
| 5 | signer key ID as `Vec<u8>` UTF-8 |

The operation schema identifies add, tombstone, update, and remove device actions and DID deactivation. Use the schema version returned by the Directory or current SDK; do not reuse the removed policy-link action encoding.

Delivery-policy payloads bind the complete replacement or deletion to its scope:

| Position | SCALE value |
| - | - |
| 1 | Operator DID as `Vec<u8>` UTF-8 |
| 2 | Policy operation enum: complete policy, scoped deletion, or scoped delivery-constraint update |
| 3 | `policy_nonce` as `u64` |
| 4 | `valid_until` as `u64` milliseconds |
| 5 | Authorized signer key ID as `Vec<u8>` UTF-8 |

The operation value carries the canonical DID or Persona scope and the complete policy or requested constraints where applicable. The current Policy V2 actions set or delete one DID- or Persona-scoped policy or update DID or Persona delivery constraints. Tag-scoped actions have been removed; a tag is a rule condition.

A Persona registration payload binds the canonical DNS name, action, proposed controller set and threshold, optional delivery constraints, DNS validation attestation, challenge nonce, operator nonce, and `valid_until`. Renewal binds the same fields except a replacement controller set: it preserves the finalized controllers, which can change only through the rotation endpoint. Persona v1 requires `controller_threshold: 1`. Initial registration is signed by one proposed controller that is already a signing key on the operator DID; renewal is signed by one active Persona controller.

A Persona transfer has two nested authorization layers. The new operator signs the Directory-returned `acceptance_payload` with a proposed new controller. The Directory then binds that signature and the DNS attestation into `canonical_payload`, which an active current Persona controller signs. The chain verifies both signatures and the attestation before replacing the operator and controller set. Use the returned bytes exactly; neither payload is JSON.

<Note>
  The Directory submits only DID-key proof calls. Account-owner pallet calls remain available to developers who
  interact with the chain directly, but they are not Directory HTTP endpoints.
</Note>

## Sign the bytes, not their printed encoding

If `canonical_payload` is Base64, decode it and sign the decoded bytes. Do not sign the Base64 characters. The resulting Ed25519 signature is sent as Base64 in `signature`.

## Signer selection

Set `signer_key_id` to:

* `root` to use the record's root public key, or
* A complete authorized verification-method ID such as `did:openpayload:...#signing`

The key must already be authorized by the DID record.

## Why the body repeats the DID and action

The path selects the resource. The signed body binds the authorization to the same DID and one exact operation. The Directory rejects a mismatch rather than guessing what the caller intended.

## Straight to the point

```text theme={null}
nonce = GET /dids/{did}/nonces
payload = SCALE(operation fields in documented order)
signature = Ed25519.sign(private_key, payload_bytes)
canonical_payload = "0x" + hex(payload_bytes)
```

Never send `private_key`, a mnemonic, or a seed phrase.


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