---
title: "Upload HITL keypair"
method: PUT
path: "/api/v1/hitl/{id}/keys"
tags: ["HITL"]
---

# Upload HITL keypair

`PUT /api/v1/hitl/{id}/keys`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/hitl/{id}/keys`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `auth_token`

Upload the client-generated RSA keypair for a HITL config. `?force=true` acknowledges the key-rotation cascade, which invalidates envelopes wrapped to the previous key. A ROTATION (a new `kid` on an active config) from a JWT dashboard session requires a live `hitl_key_rotate` 2FA step-up grant (`403 2fa_required` — start one via `POST /me/2fa/start` with `action_type: hitl_key_rotate`); an API-key caller is not step-up gated. Every successful rotation, from either path, emails the account owner at their primary email address (when one is on file). A same-`kid` update (re-wrapping the private key) and the first upload on a config without a keypair need no grant and send no email.

## Path parameters

- `id` string, required: HITL config id

## Query parameters

- `force` 'true' | 'false': Acknowledge the key-rotation cascade

## Request body

- PutHitlKeysBody: The client-generated RSA keypair for a HITL config. `public_key`, `encrypted_private_key` and `kdf_salt` are required; the `wrapped_dek`/`dek_iv`/`dek_tag` trio carries the v2 master-key envelope and the `backup_*` trio the recovery wrapper. The server never receives the master password, so it cannot decrypt any of it.
  - `public_key` string, required
  - `encrypted_private_key` string, required
  - `kdf_salt` string, required
  - `wrapped_dek` string
  - `dek_iv` string
  - `dek_tag` string
  - `backup_wrapped_dek` string
  - `backup_dek_iv` string
  - `backup_dek_tag` string

## Response `200`

Keypair stored

- object

## Other responses

- `400`: Validation error
- `403`: 2fa_required — a JWT dashboard session rotating the keypair needs a live `hitl_key_rotate` step-up grant; nothing was changed
- `404`: HITL config not found
- `409`: Refused because this would ROTATE an existing keypair: pending confirmations would be denied, approver enrollments reset and previously resolved confirmations left permanently unreadable. Re-send with `?force=true` to acknowledge that cascade.
- `500`: key_rotation_partial — a HALF-APPLIED forced rotation: pending confirmations were already denied and approver enrollments invalidated, but the new keypair did not persist. The config is left without a usable key, so this must be retried rather than treated as a no-op failure.

---

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