---
title: "Publish the caller's own vault directory key (human or agent principal)"
method: POST
path: "/agent/v1/whoami/encryption-key"
tags: ["Org"]
---

# Publish the caller's own vault directory key (human or agent principal)

`POST /agent/v1/whoami/encryption-key`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/agent/v1/whoami/encryption-key`
- Auth: one of:
  - API key in header `SIGN-IN-WITH-X`
  - HTTP bearer

Self-only, custody-eligible principals only (`human` | `agent`; a `ci` caller is refused NOT_A_RECIPIENT_PRINCIPAL): publishes the caller's own X25519 public key + `ek_` fingerprint to the org-scoped encryption-key directory (vault-human-envelopes, widened by vault-agent-envelopes). The private key never reaches the gateway. `custody_scheme: keystore_v1` is the CLI keystore's own key (no wrappers — the keystore file is the custody): the row is created PENDING and the response carries `activation` (an ECDH possession challenge) — answer it on `POST /agent/v1/whoami/encryption-key/activate`; a pending key is a recipient nowhere. Republishing the principal's CURRENT key is an idempotent 200 (`deduplicated: true`). A DIFFERENT key is refused 409 KEY_ROTATION_REQUIRED — rotation is never automatic (a stolen wallet key must not replace a member's decryption identity); remedies: restore the keystore backup, have an org owner revoke the stale key (`DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key`), or — only for a principal that is the sole custody-eligible member of every org it belongs to — republish with `replace_current: true` (otherwise 403 KEY_ROTATION_NOT_SELF_AUTHORIZABLE). Credential-gated: SIWX or a control-plane session only — a grant key bearer, CI session, service key, or admin key never reaches this route. The SDK runs this automatically on the first vault operation of a keystore with no published key (`run402 repos …`, `git clone`/`push`); there is no enrollment verb.

## Headers

- `SIGN-IN-WITH-X` string, required

## Request body

- object
  - `public_key` string, required: Canonical base64url-encoded 32-byte X25519 public key.
  - `ek_fingerprint` string, required: `ek_<32 hex>` — first 16 bytes of SHA-256(raw pubkey).
  - `suite` string: Defaults to `r402s-1`.
  - `key_id` string, uuid: Client-minted key row UUID — required when `wrappers[]` is supplied (it is bound inside every wrapper's sealed context).
  - `custody_scheme` 'legacy_direct_prf_v1' | 'wrapped_legacy_scalar_v1' | 'wrapped_random_v1' | 'keystore_v1': EXPLICIT custody declaration (consult round 1: never inferred from wrapper count). `keystore_v1` — the CLI keystore's key, no wrappers, pending until the possession proof; `wrapped_*` require `wrappers[]`; omit for a legacy direct-PRF browser enrollment.
  - `wrappers` object[]: Wrappers created ATOMICALLY with the key row, all PENDING — the key activates only when its first wrapper passes the possession-proof activation. Same item shape as `POST /agent/v1/source-access/wrappers`.
    - `wrapper_id` string, uuid, required
    - `kind` 'webauthn_prf' | 'recovery_code', required
    - `wrapper_ciphertext` string, required
    - `elevation_id` string, uuid
  - `replace_current` boolean: vault-agent-envelopes D3: replace a DIFFERENT current key. Honored only when the caller is the sole custody-eligible member of every org it belongs to; otherwise 403 KEY_ROTATION_NOT_SELF_AUTHORIZABLE. Never implied.
  - `signing_pubkey` string: vault-multi-writer rev 47. Canonical base64url-encoded 32-byte Ed25519 public key — the vault-WRITER signing half, unrelated to `public_key` above (the encryption half). OPTIONAL, and ALL-OR-NONE with `signing_fingerprint`/`possession_signature`: omitting all three publishes only the encryption half, byte-identical to a pre-rev-47 client.
  - `signing_fingerprint` string: vault-multi-writer rev 47. `vk_<32 hex>` derived from `signing_pubkey` via the same `keyFingerprint` convention `add_writer_key`/p0062 use — a self-consistency check, not a separate derivation. Mismatched against a re-derivation from `signing_pubkey` → 400 VALIDATION_FAILED naming `possession_signature`.
  - `possession_signature` string: vault-multi-writer rev 47. Ed25519 signature over `JCS{domain:"r402s/v0/signing-key-possession/v1", principal_id, signing_pubkey, encryption_pubkey}`, verified SYNCHRONOUSLY under `signing_pubkey` — no challenge round-trip, since a valid signature already IS possession. The signing half is NEVER rotation-gated: publishing a new signing key grants nothing by itself (becoming a vault writer still needs an existing writer's cooperation or a handoff grant+acceptance), so there is no `KEY_ROTATION_REQUIRED` analog here.

## Response `201`

Key published — `state: pending` for `keystore_v1` (answer `activation` on `/activate`) or when wrappers were supplied (activate the first wrapper); `active` for a legacy direct-PRF publish. A 200 with `deduplicated: true` means the submitted key already IS the principal's current key (no write).

- object
  - `principal_id` string, uuid
  - `encryption_key_id` string, uuid
  - `ek_fingerprint` string
  - `suite` string
  - `custody_scheme` string
  - `state` 'pending' | 'active'
  - `created_at` string, date-time
  - `signing_fingerprint` string, nullable: vault-multi-writer rev 47. `vk_<32 hex>` for the signing half just published/refreshed, or the principal's existing one if this call omitted the signing fields entirely (byte-identical to a pre-rev-47 response, and `null` if no signing half has ever been published). Never the raw `signing_pubkey`.
  - `signing_possession_verified_at` string, date-time, nullable: vault-multi-writer rev 47. Timestamp the `possession_signature` was verified — synchronous with this call, so equal to `created_at`/now on a fresh signing-half publish; carried forward unchanged when this call didn't touch the signing half.
  - `wrapper_activations` object[]: One ECDH possession challenge per created wrapper (see the activate route).
    - `wrapper_id` string, uuid
    - `kind` string
    - `challenge_id` string, uuid
    - `epk` string
    - `expires_at` string, date-time
  - `deduplicated` boolean: True when the publish matched the principal's current key exactly and nothing was written.
  - `activation` object, nullable: `keystore_v1` only: the key-level ECDH possession challenge. `proof = base64url(HMAC-SHA256(X25519(keystore_private, epk), "r402s/v0/keystore-key-possession/v1\n" + JCS{challenge_id, encryption_key_id, public_key}))`.
    - `challenge_id` string, uuid
    - `epk` string: Server ephemeral X25519 public key, base64url.
    - `expires_at` string, date-time
  - `next_actions` object[]

## Other responses

- `400`: VALIDATION_FAILED
- `403`: NOT_A_RECIPIENT_PRINCIPAL (a `ci` principal) or KEY_ROTATION_NOT_SELF_AUTHORIZABLE (`replace_current` by a principal that shares an org with another custody-eligible member).
- `409`: KEY_ROTATION_REQUIRED — the principal already has a published key that differs from the one submitted; rotation is never automatic. `details` names both fingerprints and the remedies.

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
