---
title: "Redeem a Handoff Key"
method: POST
path: "/vaults/v1/handoffs/{handoff_id}/redeem"
tags: ["vault"]
---

# Redeem a Handoff Key

`POST /vaults/v1/handoffs/{handoff_id}/redeem`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/vaults/v1/handoffs/{handoff_id}/redeem`
- Auth: API key in header `SIGN-IN-WITH-X`

kygit-handoff (design D3). Verifies auth_secret against the stored auth_hash (constant-time compare) and, on success, atomically flips the recipient into an active org member at the handoff's stored role and returns the sealed envelope for the recipient's own client to decrypt (the gateway never sees the plaintext). ONE transaction, no durable lease — first writer wins. A SAME-principal replay before expiry returns 200 deduplicated:true with the ORIGINAL sealed envelope (safe: only the key holder can derive wrap_key). Auth: SIWX WALLET ONLY — a control-plane session, grant key, or service key is refused 403 HANDOFF_REDEEM_REQUIRES_WALLET, because the keystore key published on this call is what makes the recipient a real key-holder. Tier-free: principalAuth resolves-or-provisions the caller's own org-of-one with no active tier required. Never lifecycle-gated. Response carries Cache-Control: no-store — it is never cached by an intermediary. vault-multi-writer rev 47, REQUIRED (no observe mode): the redeemer must ALSO submit writer_acceptance, its own double-signed acceptance of the mint-time writer_admission_grant — both signatures independently verified. On success the redeemer's directory signing half is published possession-verified in the SAME transaction.

## Path parameters

- `handoff_id` string, uuid, required

## Headers

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

## Request body

- object
  - `auth_secret` string, required: Base64url — the HKDF-derived auth_secret half of the parsed kgh1_ key (64 lowercase hex characters are accepted as a permanent compatibility form: the public client 4.67.0–4.68.0 sent that). Client-derived only; a malformed value simply never matches any stored hash (never a distinguishing error).
  - `writer_acceptance` object, required: vault-multi-writer rev 47, REQUIRED (no observe mode). The redeemer's own double-signed acceptance of the mint-time writer_admission_grant: {statement: {domain:"r402s/v0/handoff-writer-accept/v1", handoff_id, auth_hash, writer_key_id, signing_pubkey, encryption_pubkey, encryption_fingerprint}, acceptance_signature, possession_signature}. acceptance_signature verifies under the STORED grant's handoff_admission_pubkey; possession_signature verifies under the redeemer's OWN signing_pubkey — both over the identical preimage, genuinely independent proofs. A redeem under a DIFFERENT writer_key_id than an earlier successful redeem by the same principal refuses 409 HANDOFF_REDEEM_WRITER_KEY_MISMATCH.

## Response `200`

Claimed. On a same-principal replay before expiry, deduplicated is true and sealed_envelope is the ORIGINAL envelope (never a pretend re-redeem).

- object
  - `handoff_id` string, uuid, required
  - `kind` 'handoff', required
  - `deduplicated` boolean, required
  - `sealed_envelope` string, byte, required: Base64 ciphertext — decrypt client-side under the derived wrap_key. The gateway never sees the plaintext.
  - `envelope_kind` string, required
  - `repo_id` string, required
  - `org_id` string, uuid, required
  - `project_id` string, nullable, required
  - `checkpoint` object, required
    - `generation` integer: Echoed as an integer; the request may carry it as an integer or the canonical zero-padded generation string.
    - `snapshot_oid_hmac` string
  - `membership` object, required
    - `org_id` string, uuid
    - `role` string
    - `status` 'active'
  - `members` object[], required: Every current member of the org, so the recipient sees whether the sender is still present.
  - `expires_at` string, date-time, required
  - `writer_admission_grant` object, required: vault-multi-writer rev 47. The ORIGINAL writer_admission_grant from the mint, echoed back byte-for-byte for the client to persist/replay.
  - `writer_activation` object, required: vault-multi-writer rev 47. "pending" on a FRESH redeem, always. On a REPLAYED redeem (deduplicated:true) reflects the row's CURRENT state, which may already read "active" if the redeemer's add_writer_key{"handoff"} push admitted the key on-chain in the meantime. The unconditional way to confirm admission: the added writer_key_id appears in writer_set.writers[] on GET /vaults/v1/:vault_id.
    - `state` 'pending' | 'active'
  - `next_actions` NextAction[], required: Always carries push_repo (confirm access with a push). Also carries remove_member when the minter is still an active owner — the zombie-sender case: if that environment is gone for good, ask an owner to remove it.
    - `type` string, required: Advisory machine-readable action type such as retry, authenticate, submit_payment, fund_wallet, renew_tier, check_usage, resume_deploy, edit_request, edit_migration, contact_support, upgrade_client, poll (poll the project events feed — appended when the envelope carries a correlated_platform_incident), or hand_to_member (on a commit/promote response that just activated a public site: show the human urls.site and urls.console, relay Run402's offer to promote the build on @run402com, and ask yes or no — see `credited_as` below).
    - `method` string
    - `path` string
    - `auth` string
    - `why` string
    - `credited_as` string, nullable: `hand_to_member` only. The caller's own live presence name in the project's default room, else that room's most recently active live presence, else `null` — in which case `why` tells the agent that joining the room with a name (`run402 rooms join --name ...`) is how the promotion credits it.
    - `body` object: `hand_to_member` only. A ready-to-send `POST /feedback/v1` template: `{ project_id, message: "promote: yes", handle? }`.

## Other responses

- `403`: HANDOFF_KEY_INVALID (wrong auth_secret, or the id does not exist — identical envelope, so a guess and a wrong secret are indistinguishable) or HANDOFF_REDEEM_REQUIRES_WALLET (a bearer credential was presented instead of SIGN-IN-WITH-X).
- `409`: HANDOFF_KEY_ALREADY_REDEEMED — redeemed by a DIFFERENT principal (details.redeemed_at names when), or (vault-multi-writer rev 47) HANDOFF_REDEEM_WRITER_KEY_MISMATCH — a replay under a writer_key_id different from an earlier successful redeem by the same principal.
- `410`: HANDOFF_KEY_EXPIRED (past its TTL) or HANDOFF_KEY_REVOKED (revoked by the minter/an owner, OR the fail-closed authority-drift fences: the minter's membership is no longer active/high-enough, or the vault's project moved to a different organization).

---

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