---
title: "Redeem an Invite Key"
method: POST
path: "/vaults/v1/invites/{invite_id}/redeem"
tags: ["vault"]
---

# Redeem an Invite Key

`POST /vaults/v1/invites/{invite_id}/redeem`

## Request

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

kygit-invite (design D1/D3/D9). Verifies auth_secret against the stored auth_hash (constant-time compare, kind-domain-separated — an invite secret never verifies a handoff hash) and, on success, atomically flips the recipient into an active org member at the invite's stored role, resolves the room fields (inviter, live_presences, catch-up cursor), and returns the sealed envelope for the recipient's own client to decrypt. ONE transaction, no durable lease — first writer wins, the SAME engine a handoff redeem runs. A row of the WRONG KIND at this route (e.g. a real handoff id) is the identical 403 INVITE_KEY_INVALID — never disclosed. A SAME-principal replay before expiry returns 200 deduplicated:true with the ORIGINAL sealed envelope. Auth: SIWX WALLET ONLY — a control-plane session, grant key, or service key is refused 403 INVITE_REDEEM_REQUIRES_WALLET. Tier-free: principalAuth resolves-or-provisions the caller's own org-of-one with no active tier required (the joiner's own cold-start tier purchase, if any, is a separate client-side step this route never blocks on). Never lifecycle-gated. Response carries Cache-Control: no-store.

## Path parameters

- `invite_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 kgi1_ key (64 lowercase hex characters are also accepted, the same compatibility form the handoff redeem route accepts). 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}. The signature domain and the `handoff_id` field name are the frozen protocol spelling (kygit-invite design D11); `handoff_id` carries THIS invite's id. 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 INVITE_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
  - `invite_id` string, uuid, required
  - `kind` 'invite', 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 who else is already here.
  - `room` object, required
    - `org_id` string, uuid, required
    - `room_key` string, required
  - `inviter` object, nullable, required: The minting presence resolved LIVE from the room — name, program, model, state, last activity. Null when the inviter never registered a presence, or resolution best-effort-fails (never blocks the redeem).
    - `presence_id` string
    - `name` string
    - `program` string, nullable
    - `model` string, nullable
    - `state` string
    - `last_active` string, date-time
  - `live_presences` object[], required: The room's live presences at redeem time.
  - `cursor` string, nullable, required: The room's catch-up cursor (opaque mcr_...) — so the joiner's first `messages wait`/`messages list` call needs no flags.
  - `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 so the joiner's own client can cross-check it against the v2 envelope's writer_admission_grant_sha256 (a value the gateway cannot forge) and then complete an add_writer_key transition under its OWN key.
  - `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 joiner's add_writer_key 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 and wait_room. Carries send_room_message only when inviter resolved. NEVER carries remove_member, under any condition (unlike a handoff redeem, which may).
    - `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`: INVITE_KEY_INVALID (wrong auth_secret, the id does not exist, OR a HANDOFF row's real id at this route — one identical envelope, never disclosed) or INVITE_REDEEM_REQUIRES_WALLET (a bearer credential was presented instead of SIGN-IN-WITH-X).
- `409`: INVITE_KEY_ALREADY_REDEEMED — redeemed by a DIFFERENT principal. details.redeemed_at names when.
- `410`: INVITE_KEY_EXPIRED (past its TTL) or INVITE_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)
