> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gigstack.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Account approvals

> Integration guide for Account approvals

Prepare a credential, membership, managed identity/session or administrator invitation action for the initiating person to review in the authenticated
browser. Preparation does not issue credentials, accept terms, or revoke existing keys.
Use a verified personal MCP or Firebase identity; team API/OAuth keys cannot prepare
these requests or impersonate their creator. Pass an authorized current team in
the `team` query parameter for authentication context; team-scoped preparation should match
`body.team_id`. Billing-account invitations instead require `billing_account_id` and no
`team_id`; the query team is only authentication context. Reads and cancellation can use the selected current team even if
the original credential team has changed. This does not change the credential mode.

## Prepare an action

Save a UUIDv4 before the first request. This synthetic example prepares a live/test
API key pair for a team with no active API keys:

```bash theme={null}
curl --request POST 'https://api.gigstack.io/v2/users/me/account-approvals?team=team_example' \
  --header 'Authorization: Bearer YOUR_PERSONAL_MCP_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"operation_id":"00000000-0000-4000-8000-000000000001","action":"api_keys.generate","team_id":"team_example","payload":{}}'
```

Each new intended action needs its own saved UUID. A repeated preparation with the
same creator, originating credential/mode, UUID and exact normalized input reads the
same request. A changed payload returns `409 operation_conflict`. After a preparation
timeout, preserve and reuse the original ID and content; never generate a new ID as an
automatic retry.

| Action | Payload | Effect after browser confirmation |
| - | - | - |
| `api_keys.generate` | `{}` | Create live and test API keys when none are active |
| `api_keys.rotate` | `{}` | Revoke the reviewed API keys and issue a new live/test pair; preserve MCP tokens |
| `team.ownership.transfer` | `{"new_owner_id":"user_example"}` | Transfer to an active current member; the previous owner remains an admin |
| `team.members.promote_admin` | `{"member_id":"user_example"}` | Promote an active existing member to administrator |
| `team.members.add_admin` | `{"member_id":"user_example"}` | Add an eligible existing user from the same billing account as a visible administrator |
| `mcp_tokens.create` | `{"name":"Local agent","livemode":false}` | Create a personal MCP token after explicit browser terms acceptance |

A token name is trimmed and must contain 1–64 characters. Mode must be explicit.
Unknown fields, caller-supplied authority and agent terms acceptance are rejected.
A personal MCP token grants the user's access across authorized teams; `team_id`
is its initial context, not a security boundary limiting the resulting credential.
The new token does not change the calling credential's mode.

## Membership scope

Membership actions require the current team owner using a verified personal identity.
They affect access across production and test. `requested_modes: []` means that no
credential mode is being selected; it does not isolate the grant to test mode.
The frozen `membership_change` shows the target account ID, email and name from Auth,
previous role, current owner, and whether ownership changes. The target and owner
must still be active, and changed identity, roster, permissions or account state
requires a new review. Adding an admin is for an existing eligible user in the same
billing account; use invitations for someone outside that account.

Transfers keep the previous owner as an administrator and preserve billing-account
ownership. No membership action issues an API key, password or login link.

## Read and cancel

The response is HTTP 200 `{success:true,data:approval,timestamp}` with
`Cache-Control: no-store`. It includes a 64-character lowercase approval `id`,
`operation_id`, action, team identity, billing-account ID, normalized payload,
requested modes, effect scope, status, millisecond timestamps and `review_url`.
It never contains secrets or a review challenge.

```bash theme={null}
gigstack account-approvals get APPROVAL_ID --team TEAM_ID --json
gigstack account-approvals cancel APPROVAL_ID --team TEAM_ID --yes --json
```

These use `GET /users/me/account-approvals/{id}` and
`DELETE /users/me/account-approvals/{id}`. Reads and cancellation require the same
creator; MCP calls also require the same originating credential and mode. Cancellation
only affects a pending request. It cannot revoke issued credentials or undo completion.

| Status | Meaning |
| - | - |
| `pending` | The person still needs to review and confirm; no credential has been issued by preparation |
| `completed` | The action-specific safe receipt was recorded; this does not prove delivery, sign-in, or email receipt |
| `cancelled` | The pending request was cancelled |
| `expired` | Its ten-minute preparation lifetime elapsed |
| `invalidated` | Relevant account or key state changed; a new request needs a fresh review |
| `processing` | A managed identity/session attempt was consumed and may be in flight; do not retry its effect |
| `unknown` | A provider effect or its final recording is uncertain; do not replay it |
| `activation_required` | A managed identity exists disabled and needs its separate activation review |
| `failed` | Managed creation recorded a known failure; inspect its safe details |

The completed `receipt` contains `keys:[{key_id,type,livemode}]`,
`revoked_count`, and nullable `membership`, `invitation`, `managed_identity`, and `managed_session`. Membership receipts identify the action,
member, current and previous owner, resulting role, and whether a member was added.
They have `keys: []` and `revoked_count: 0`. Credential receipts have `membership: null` and `invitation: null`. Invitation receipts have no keys or membership result. Every GET remains metadata-only, even after a lost execution response.
No endpoint retrieves a previously delivered secret.

## Human browser confirmation

The person opens `review_url` in the authenticated application and verifies its
configured environment, team, action, requested modes and consequences. The link is
not a bearer grant and contains no secret. Production uses `https://app.gigstack.pro`;
staging uses its separately configured application origin.

The application privately obtains a short-lived, intent-bound review challenge and
submits the person's explicit confirmation. Those browser review and execution actions
are excluded from the public API reference and agent tools. They require recent real
Firebase sign-in, revocation checking, the same person, and enrolled MFA when applicable;
refreshing an old ID token is not recent authentication. Custom/anonymous sessions,
managed-user sessions and personal MCP/API credentials cannot perform them.

The server rechecks current membership, authority, origin-credential validity, relevant
key/account state and applicable entitlement before committing. API rotation revokes
only the reviewed key set; a state change invalidates review. MCP creation rechecks the
ten-token limit and the person's explicit terms acceptance. The disclosure version is
the application's review contract, not an independently verified legal PDF revision.

Only the first browser execution that commits returns new secrets to that browser.
Execution replay returns `secrets:null`; reads, cancellation and preparation never return
secrets or challenges. If that response was lost, inspect the metadata receipt and use
a separately reviewed replacement if needed. Do not automatically repeat issuance.

## Migrating direct issuance and privileged membership changes

Direct `POST /api-keys`, `POST /api-keys/rotate` and `POST /users/me/mcp-tokens`
return HTTP 403 `account_approval_required`. They no longer return HTTP 201 or credentials.

CLI `api-keys create`, `api-keys rotate` and `me mcp-tokens create` now prepare approvals
with required `--operation-id`. Their former `--out` and `--accept-terms` issuance options
are removed. MCP uses `prepare_account_api_keys`, `prepare_account_api_key_rotation` and
`prepare_my_mcp_token`, followed by `get_account_approval` or `cancel_account_approval`.
List and revoke commands remain available under their existing authority checks.

Direct `POST /teams/{id}/transfer-ownership` now returns `403 account_approval_required`.
The same error applies to `role: admin` on member PATCH and add-member POST. Prepare
the corresponding action above; preparation itself does not change access. Non-admin
role changes, permitted module permissions, and member removal keep their existing
authority and direct routes. A completed transfer receipt remains readable by its
creator even after they become the previous owner, so a lost response does not require
repeating the transfer.

## Invite an administrator

Prepare an invitation with an explicit delivery choice:

```json theme={null}
{
  "operation_id": "00000000-0000-4000-8000-000000000004",
  "action": "team.invitations.create_admin",
  "team_id": "team_example",
  "payload": {"email": "member@example.com", "send_email": false}
}
```

Send this to `POST /users/me/account-approvals`, then open `review_url`. Only the
current canonical team owner can approve. For an administrator across the billing
account, use `billing_account.invitations.create_admin`, replace `team_id` with
`billing_account_id`, and have that account's canonical owner review. Team ownership,
billing administrator status and master-admin flags do not substitute for billing
ownership. `canonical_owner_required` means support must resolve missing ownership.

The browser shows the frozen email address, scope, administrator role, invitation
expiry and delivery choice in `invitation_change`. The email address identifies the
intended recipient; it is not proof of an existing person's identity. `team` is null
for billing-account invitations. Access applies across live and test.

Preparation expires after ten minutes. A new invitation's seven-day expiry starts at
preparation and is frozen during review. Completion atomically records the approved
invitation and `receipt.invitation`, which contains safe identity/scope metadata.
It does **not** mean an email was delivered or the recipient joined. Only the first
committing browser execution may attempt the reviewed email delivery. Replaying
execution never sends another email.

Use these owner-scoped endpoints for the receipt's invitation ID:

| Action | Request |
| - | - |
| Read status | `GET /users/me/account-invitations/{id}` |
| Revoke a pending invitation | `DELETE /users/me/account-invitations/{id}` with `{}` |
| Explicitly send or resend | `POST /users/me/account-invitations/{id}/resend` with `{}` |

`delivery.status=sent` records provider acceptance, not recipient acceptance.
`pending` can represent a delivery in progress; an active five-minute lease rejects
resend with `delivery_in_progress`. An expired lease or uncertain provider result
requires `{"acknowledge_unconfirmed_delivery":true}` and can cause a duplicate email.
Read the status and obtain that explicit choice before resending. Resend is not an
idempotent request: repeating a completed resend can send again. Revocation does not
retract an already-sent email. Management responses never expose invitation tokens.

To approve an existing unexpired pending admin invitation, include
`existing_invitation_id` in the preparation payload and set `send_email:false`.
Recovery preserves its existing link and expiry. Unknown historical delivery remains
unknown. An already-approved or expired invitation cannot be adopted again; revoke
and prepare a new invitation when needed. `approval_required` on management metadata
reports missing proof, but acceptance and resend always validate the full proof and
current owner authority independently.

Direct admin invitation creation returns `account_approval_required`. Ordinary
editor/viewer invitations retain their direct workflow. Administrator acceptance
requires the intended recipient's active Auth account, verified matching email, and
current valid owner approval. An existing team member is not silently promoted by accepting
a team invitation; use the separate promotion approval. A billing-account invitation
explicitly grants account administrator authority, including to someone already
associated with that account through an ordinary team seat.

## Managed session actions

Prepare delegated access to an existing managed identity using a saved operation ID:

```bash theme={null}
curl --request POST 'https://api.gigstack.io/v2/users/me/account-approvals?team=team_example' \
  --header 'Authorization: Bearer YOUR_PERSONAL_MCP_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"operation_id":"00000000-0000-4000-8000-000000000007","action":"managed_users.issue_session","team_id":"team_example","payload":{"user_id":"managed_example"}}'
```

Open the returned `review_url` for the initiating person. Team API/OAuth keys cannot
prepare the action, and agents cannot review, execute, receive or redeem its credential.
To request refresh-token revocation instead, use `managed_users.revoke_sessions`
with the same payload shape and a new saved operation ID. It is a separate reviewed
action, not cancellation of a prepared request.

Both actions require the current canonical billing-account owner and the owner of
every reachable team to be the initiating person. The selected team must be within
the target's authoritative access. Private managed provenance must match the current
Auth identity; historical `from: api` or `managedBy` labels alone are insufficient.
Foreign or ambiguous grants are refused. Issuance requires an active target;
reviewed revocation can apply to a disabled managed identity.

`managed_session_change` freezes the target ID, email, Auth creation time, billing
account, reachable teams/accounts and prior issuance/revocation attempts. Team roles
include inherited `billing_account_admin` and `master_team_billing_access` where
applicable. These disclose current access, not a lifetime restriction on the global
Firebase identity. `effect_scope` is `managed_session`, and `requested_modes: []`
means the access is not isolated to test mode.

`managed_session_execution` reports `attempting`, `issued`, `revoked` or `unknown`,
with the attempt timestamp, nullable `custom_token_expires_at`, and delivery status.
For issuance, `delivery: not_observable` means the backend cannot establish whether
the person received or used the credential. A safe completed `receipt.managed_session`
contains the target, approval ID, action and exchange deadline; it never contains a
credential. Session fields and this receipt variant are null for other actions.

The custom-token deadline limits **exchange**, not the life of a redeemed Firebase
session. A completed issuance does not prove sign-in. After an uncertain or lost
response, read this approval; do not replay its effect, automatically start another
intent, or treat elapsed token time as permission to issue again. Any new issuance
requires a separate human review acknowledging the exact frozen prior attempts.

Reviewed revocation invalidates refresh tokens. Already-issued ID tokens can remain
usable where consumers do not check revocation, and an outstanding custom token can
still be exchanged. A successful revocation does not resolve an earlier unknown
issuance. The receipt therefore says `revocation_scope: refresh_tokens_only` and
`session_expiration: not_bounded_by_custom_token` rather than promising an ended session.

The browser handles any first credential privately and deliberately, preserving the
owner's existing session. Public preparation, GET and cancellation always return safe
metadata. There is no agent endpoint for recovering a lost credential.

Direct `POST /users/login-link` returns `403 account_approval_required`. The legacy
Proserver loginUrl and redeem terminals are also retired; old opaque links cannot be
redeemed there. Prepare the reviewed action above instead.

Session preparation is disabled by default and unavailable in production until an
authoritative reverse-grant index with coordinated writers is implemented. It returns
`503 managed_session_scope_unavailable` before preparation reads when the strategy
is unavailable. Do not retry with new operation IDs to bypass this guard.

The staging prototype requires explicit `GCLOUD_PROJECT=gigstackprodev` and
`MANAGED_SESSION_SCOPE_STRATEGY=prototype_scan`. It permits at most 1000 teams,
1000 billing accounts and 100 previous attempts, reading one extra result to detect
overflow. `503 managed_session_scope_limit` refuses incomplete scope before issuance;
a truncated result is never treated as authorized scope. This interim guard preserves
the intended reviewed workflow but does not complete production capability.
Configuration and rollout must be verified separately; documentation is not evidence
that a deployed environment enables the strategy.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.