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

# Mint an Invite Key

`POST /vaults/v1/{vault_id}/invites`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/vaults/v1/{vault_id}/invites`
- Auth: one of:
  - HTTP bearer
  - API key in header `SIGN-IN-WITH-X`
  - API key in header `x-admin-key`

kygit-invite (design D1/D2/D3/D9) — the SECOND claim kind on the same shared row: a working agent mints a single-use Invite Key another agent redeems for membership at a narrow role, the exact checkpoint, and a shared coordination room, while the inviter keeps working. One shared claim engine with kygit-handoff, parameterized by kind, not a fork. The `kgi1_<invite_id||master_secret>` key is assembled entirely CLIENT-SIDE (kind-domain-separated HKDF — an invite secret never verifies a handoff hash and vice versa) — `invite_id` is client-generated and REQUIRED in the body, same coordinator amendment as `handoff_id`. Auth: `principalAuth` (control-plane session, SIWX wallet, or admin key) at `invite.mint` (developer+, no grant key — minting CONFERS MEMBERSHIP and a grant key is never a member). Never lifecycle-gated. Bounds are SHARED with handoffs (one population, not two independent caps): 20 open per vault, 100 mints per org per UTC day.

## Path parameters

- `vault_id` string, required

## Headers

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

## Request body

- object
  - `invite_id` string, uuid, required: Client-generated (any UUID version). Salts the HKDF derivation of auth_secret/wrap_key, so the client must mint it before deriving the key. Used as the row's own primary key — a collision refuses 409 INVITE_ID_CONFLICT rather than ever overwriting a row.
  - `role` 'owner' | 'admin' | 'developer' | 'billing' | 'viewer': Requested role. The stored minted_role is min(requested ?? developer, the minter's own fresh role) — an invite never grants MORE than the minter's own role. Defaults to developer when omitted — NOT the minter's own role (the handoff kind's default).
  - `expires_in_seconds` integer: TTL in seconds. Default 3600 (1 hour).
  - `room_key` string: Optional. The agent-messaging room the claim resolves `inviter`/`live_presences`/`cursor` against. Omitted defaults to the vault's own project id — its default room. Plaintext, not secret; a listed invite shows it.
  - `inviter_presence_id` string: Optional. The minter's own `prs_...` presence id in that room, best-effort — the mint still succeeds when this is absent or stale; the claim's `inviter` simply resolves to null.
  - `checkpoint` object, required: The pinned checkpoint the sealed envelope bridges to — a keyed commitment, so the gateway never sees the plaintext commit oid.
    - `generation` union, required: The pinned generation: a non-negative integer, or vault's canonical zero-padded decimal string (what every head/ref_state/status surface renders, e.g. "0000000000000003"). Either form is accepted.
      - integer
      - string
    - `snapshot_oid_hmac` string, required: 64 lowercase hex chars (32 bytes) — the HMAC form heads already carry, never the plaintext oid.
  - `sealed_envelope` string, byte, required: Base64 ciphertext, opaque to the gateway: {v, kind:"invite", repo_id, epoch, k_e, checkpoint, note_schema} sealed under wrap_key with the vault's frame AEAD backend. Stored verbatim; never decrypted by the gateway.
  - `envelope_kind` string, required: Client-declared envelope format tag, opaque to the gateway (e.g. "kygit-invite-envelope-v1").
  - `auth_hash` string, required: 64 lowercase hex chars (32 bytes) — SHA-256("kygit/invite/auth-hash/v1" || auth_secret). The gateway stores only this hash, never auth_secret itself.
  - `writer_admission_grant` object, required: vault-multi-writer rev 47, REQUIRED (no observe mode). The minter's own signed grant proving THEY are currently an active vault writer and binding this mint: {format, object_kind, suite, repo_id, handoff_id, auth_hash, checkpoint_generation, checkpoint_head_sha256, grantor_writer_key_id, handoff_admission_pubkey, minted_role, claim_not_after, created_at, signature}. The PROTOCOL noun stays `handoff` (kygit-invite design D11 — one frozen admission door with two product doors in front of it): the grant's `handoff_id` field carries THIS invite's id, and the admission seed is the third HKDF output under info "kygit/invite/writer-admission/v1". Verified synchronously server-side under the minter's OWN signing_pubkey — every field cross-bound against this mint request's own resolved values. Stored verbatim + its sha256; echoed back byte-for-byte from the matching claim response.

## Response `201`

Invite minted. The Invite Key itself is assembled and printed exactly once, client-side (the gateway never sees it).

- object
  - `invite_id` string, uuid, required
  - `kind` 'invite', required
  - `role` 'owner' | 'admin' | 'developer' | 'billing' | 'viewer', required: The stored minted_role — never wider than the minter's own fresh role at mint time.
  - `room` object, required
    - `org_id` string, uuid, required
    - `room_key` string, required
  - `expires_at` string, date-time, 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
  - `writer_admission_grant_sha256` string: vault-multi-writer rev 47. SHA-256 of the EXACT writer_admission_grant bytes this mint stored, so the minting client can verify the gateway stored exactly what it sent (and can seal the same value into the v2 envelope the joiner cross-checks).
  - `warning` string, required: Verbatim blast-radius sentence, the SAME sentence the handoff mint uses with this invite's role: "Whoever claims this key first becomes a <role> of this org and a writer of this vault, permanently. The key works once and expires at <expires_at>."
  - `warnings` object[]: Machine-readable twin of `warning`: exactly one entry, code INVITE_KEY_CONFERS_ROLE.
    - `code` 'INVITE_KEY_CONFERS_ROLE', required
    - `message` string, required
  - `next_actions` NextAction[], required: Carries a revoke_invite entry (DELETE this same invite). The recipient's exact join command is CLI-synthesized (the gateway never learns the assembled kgi1_ key), so it is not on this response.
    - `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

- `400`: VALIDATION_FAILED — a malformed field (invite_id not a UUID, expires_in_seconds out of 60..86400, room_key not a valid slug, checkpoint/auth_hash/sealed_envelope shape).
- `403`: VAULT_ACCESS_DENIED — unauthorized, nonexistent, or wrong-project vault (one opaque envelope, authorize-before-reveal). Or (vault-multi-writer rev 47) INVITE_MINT_REQUIRES_WRITER — the caller is not currently an active, non-gateway-blocked writer on this vault; next_actions carries request_writer_sync naming `run402 repos access sync`, and no claim row, placeholder membership, or audit row is written.
- `409`: INVITE_ID_CONFLICT (the client-generated invite_id already exists) or INVITE_OPEN_LIMIT (this vault already has 20 open claims across BOTH kinds). Or (vault-byo-primary-bucket D11) VAULT_BYO_SINGLE_WRITER — this vault's storage_profile is "byo": a bring-your-own-bucket vault admits exactly one writer in v1, because its key envelopes live only in the customer's bucket, so a second writer would gain writer authority without the bucket access it needs to read them. details: { storage_profile: "byo", vault_id }. Refused before any claim row, placeholder membership, or audit row is written.
- `429`: RATE_LIMITED — the organization already minted 100 claims today (UTC), across BOTH kinds.

---

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