---
title: "Add a source-access wrapper for the caller's own encryption key"
method: POST
path: "/agent/v1/source-access/wrappers"
tags: ["Org"]
---

# Add a source-access wrapper for the caller's own encryption key

`POST /agent/v1/source-access/wrappers`

## Request

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

vault-recovery-custody (consult round 1): the member's vault encryption private key is held only as encrypted wrappers — one per PRF-capable passkey (`webauthn_prf`, KEK derived from that credential's WebAuthn PRF output) plus at most one `recovery_code` wrapper (KEK derived from the source recovery code, a decrypt-only secret that never authenticates). Every wrapper is born PENDING and counts for nothing until the possession-proof activation (`POST …/wrappers/{wrapper_id}/activate`) — the response carries the ECDH challenge. The `swrap2` blob is sealed client-side with a canonical context binding (principal, key id, wrapper id, kind, credential, FULL public-key hash, RP id) as both HKDF info and AAD; the server cannot open or validate the ciphertext. For `webauthn_prf`, the credential is SERVER-derived: pass `elevation_id` from a `gitvault.wrapper.add` step-up verify — the client is never authoritative for the credential id. Adding a device is adding a wrapper — the published key and `ek_` fingerprint never change. For a NEW (random-key) enrollment, use the atomic form: pass `wrappers[]` on `POST /agent/v1/whoami/encryption-key` so the key row and its first wrapper commit in one transaction (both pending until activation).

## Headers

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

## Request body

- object
  - `wrapper_id` string, uuid, required: Client-minted wrapper row UUID — bound inside the sealed context, so the caller mints it before sealing.
  - `kind` 'webauthn_prf' | 'recovery_code', required
  - `wrapper_ciphertext` string, required: `swrap2_` + base64url(kdf_salt32 ‖ nonce24 ‖ XChaCha20-Poly1305 ciphertext) sealing the 32-byte private scalar under the kind's KEK with the canonical context as AAD.
  - `elevation_id` string, uuid: Required for `webauthn_prf`: the single-use `gitvault.wrapper.add` elevation from step-up verify — the server takes the credential id from the VERIFIED assertion it records.

## Response `201`

Pending wrapper created — activate it with the returned possession challenge

- object
  - `wrapper_id` string, uuid
  - `encryption_key_id` string, uuid
  - `kind` 'webauthn_prf' | 'recovery_code'
  - `state` 'pending'
  - `format_version` string
  - `credential_subject` string, nullable
  - `blob_sha256` string
  - `created_at` string, date-time
  - `activation` object
    - `challenge_id` string, uuid
    - `epk` string: base64url server-ephemeral X25519 public key for the ECDH key-confirmation proof.
    - `expires_at` string, date-time

## Other responses

- `400`: VALIDATION_FAILED | UNKNOWN_CREDENTIAL
- `403`: ELEVATION_REQUIRED | ENROLLMENT_REQUIRES_SESSION
- `409`: NO_ACTIVE_ENCRYPTION_KEY | WRAPPER_EXISTS

---

[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)
