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

# Team Collaboration API: Zero-Knowledge Key Grants

> Create ZK teams, issue wrapped DEK grants to members, rotate keys after revocation, share encrypted memories, and manage sub-keys — all without exposing plaintext.

<style>
  {`
      main, article, .prose {
        margin-left: 2.5cm !important;
        margin-right: 2.5cm !important;
      }
      `}
</style>

The Teams API enables zero-knowledge multi-agent collaboration. The server processes and stores only public keys, wrapped Data Encryption Keys (DEKs), and encrypted memory ciphertext — it never holds cleartext keys or decrypts any memory content. Access control is enforced through asymmetric key wrapping: you issue the team DEK wrapped to each member's public key, and each member independently decrypts it locally using their private key.

<Warning>
  **Session JWT required.** All team endpoints require your dashboard session JWT (issued by WorkOS AuthKit, verified via remote JWKS — see [Authentication](/api-reference/authentication)) in the `Authorization` header. Requests using `sov_live_` or `sov_proj_` tokens return `401 session_jwt_required`.
</Warning>

***

## POST /teams

Create a new collaborative team and register the owner's self-grant for the team's Data Encryption Key.

```http theme={null}
POST /teams
Authorization: Bearer <SESSION_JWT>
Content-Type: application/json
```

<ParamField body="name" type="string" required>
  A display label for the team. Between 1 and 128 characters.
</ParamField>

<ParamField body="owner_pubkey" type="string" required>
  The owner's public key as a PEM-encoded string. Used for asymmetric wrapping of the team DEK.
</ParamField>

<ParamField body="owner_wrapped_dek" type="string" required>
  The team's Data Encryption Key (DEK), encrypted (wrapped) to the owner's public key. The server stores this opaque blob and returns it to the owner when they call `GET /teams/:id/grant`.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "name": "alpha-agents",
  "role": "admin",
  "rotation_seq": 0,
  "created_at": "2026-06-09T16:45:30.123Z"
}
```

### curl Example

```bash theme={null}
curl -X POST https://ksrlmubaxzwufziwarps.supabase.co/functions/v1/v2-agent-state/teams \
  -H "Authorization: Bearer YOUR_SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "alpha-agents",
    "owner_pubkey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1...",
    "owner_wrapped_dek": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
  }'
```

***

## POST /teams/:id/grants

Invite a new member or update an existing member's access by publishing the team DEK wrapped to the recipient's public key.

```http theme={null}
POST /teams/:id/grants
Authorization: Bearer <SESSION_JWT>
Content-Type: application/json
```

<ParamField body="member_id" type="string" required>
  Identifier for the new member (e.g. their wallet address or user ID).
</ParamField>

<ParamField body="member_pubkey" type="string" required>
  The recipient's PEM-encoded public key. The team DEK must be wrapped to this key before submission.
</ParamField>

<ParamField body="wrapped_dek" type="string" required>
  The team DEK encrypted to `member_pubkey`. The server stores this opaque blob for the member to retrieve via `GET /teams/:id/grant`.
</ParamField>

<ParamField body="role" type="string" required>
  Access level for this member. One of `"admin"`, `"write"`, or `"read"`.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "member_id": "0x987654321...",
  "role": "write"
}
```

<Note>
  Issuing a grant to a new member runs the same seat-quota check as `POST /teams/:id/invitations` below — on a paid team plan at its seat limit, this can automatically increment your Polar subscription's seat count.
</Note>

***

## GET /teams/:id/grant

Fetch the wrapped DEK registered for the authenticated caller. Decrypt it locally with your private key to obtain the team's plaintext DEK.

```http theme={null}
GET /teams/:id/grant
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "wrapped_dek": "MIIBIjANBgkq...",
  "member_pubkey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1...",
  "rotation_seq": 1
}
```

Returns `403 no_grant` if the caller has not accepted an invitation or otherwise holds no grant for this team — this is how a revoked member is locked out: their grant row was deleted, so they cannot retrieve the DEK.

### curl Example

```bash theme={null}
curl -G https://ksrlmubaxzwufziwarps.supabase.co/functions/v1/v2-agent-state/teams/1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6/grant \
  -H "Authorization: Bearer YOUR_SESSION_JWT"
```

***

## POST /teams/:id/rotate

Revoke a member, increment the team's rotation epoch, and re-wrap the team DEK to all remaining active members. Clients must generate a new DEK, wrap it individually to each remaining member's public key, and submit all updated grants in a single atomic operation.

```http theme={null}
POST /teams/:id/rotate
Authorization: Bearer <SESSION_JWT>
Content-Type: application/json
```

<ParamField body="revoke_member_id" type="string">
  Identifier of the member to revoke. **Optional** — omit to rotate the DEK on a routine schedule without revoking anyone.
</ParamField>

<ParamField body="reason" type="string">
  Human-readable reason for revocation (e.g. `"offboarding"`, `"security_incident"`). Defaults to `"manual_rotation"` if omitted.
</ParamField>

<ParamField body="grants" type="array" required>
  Array of updated grant objects for every remaining active member. Each must include `member_id` (string) and `wrapped_dek` (string) — the new DEK wrapped to that member's existing public key.
</ParamField>

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "rotation_seq": 1,
  "revoked": "0x987654321...",
  "rewrapped": 1,
  "reason": "offboarding"
}
```

***

## POST /teams/:id/memories

Append a client-encrypted ciphertext block to the team's shared memory history. All active members holding the current DEK can decrypt this block locally. Requires `write` role or higher.

```http theme={null}
POST /teams/:id/memories
Authorization: Bearer <SESSION_JWT>
Content-Type: application/json
```

<ParamField body="ciphertext_b64" type="string" required>
  Base64-encoded AES-256-GCM ciphertext with the GCM auth tag appended. Maximum decoded size is 256 KB.
</ParamField>

<ParamField body="agent_key" type="string">
  Identifier to attribute this write to in the sync log. **Optional** — defaults to the authenticated caller's user ID.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "id": "7a8b9c0d-e1f2-3a4b-5c6d-7e8f9a0b1c2d",
  "seq": 0,
  "rotation_seq": 1,
  "created_at": "2026-06-09T16:45:30.123Z"
}
```

***

## GET /teams/:id/memories

Retrieve the complete encrypted memory ciphertext history for the team. Decrypt each entry locally using the team DEK you obtained from `GET /teams/:id/grant`. Requires `read` role or higher — a revoked (inactive) member is rejected here, so even ciphertext is withheld.

```http theme={null}
GET /teams/:id/memories
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "memories": [
    {
      "id": "7a8b9c0d-e1f2-3a4b-5c6d-7e8f9a0b1c2d",
      "seq": 0,
      "rotation_seq": 1,
      "ciphertext_b64": "SGVsbG8gV29ybGQ=",
      "author_id": "0x123456789...",
      "byte_size": 12,
      "created_at": "2026-06-09T16:45:30.123Z"
    }
  ]
}
```

***

## POST /teams/:id/subkeys

Issue a programmatic `sov_live_` API key bound to a specific team member grant. Sub-keys inherit the member's role and are independently revocable without affecting other keys. Requires `write` role or higher to bind a sub-key to yourself; binding to a different member requires `admin`.

```http theme={null}
POST /teams/:id/subkeys
Authorization: Bearer <SESSION_JWT>
Content-Type: application/json
```

<ParamField body="name" type="string" required>
  A human-readable label for this sub-key (e.g. `"ci-sync-script"`). Between 1 and 128 characters.
</ParamField>

<ParamField body="bound_member_id" type="string">
  Identifier of the team member this sub-key is bound to. **Optional** — defaults to the authenticated caller. The bound member must already hold a grant.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "id": "b1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e",
  "name": "ci-sync-script",
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "bound_member_id": "0x123456789...",
  "masked_key": "sov_live_abc1****************def2",
  "raw_secret": "sov_live_abc1234567890123...def2",
  "created_at": "2026-06-09T16:45:30.123Z"
}
```

<Warning>
  The `raw_secret` is shown exactly once and is never stored in plaintext. Copy it now — you cannot retrieve it again.
</Warning>

***

## GET /teams/:id/sync-log

Retrieve per-agent telemetry statistics for the team's recent replication activity (most recent 200 entries). Requires `read` role or higher.

```http theme={null}
GET /teams/:id/sync-log
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "entries": [
    {
      "agent_key": "0x123456789...",
      "op": "write",
      "bytes": 451,
      "latency_ms": 32,
      "synced_at": "2026-06-09T16:45:30.123Z"
    }
  ]
}
```

***

## GET /teams/:id/members

List all team members with their roles, activation statuses, and grant states. Requires `read` role or higher. Never returns `wrapped_dek`, `member_pubkey`, or any ciphertext — roster metadata only.

```http theme={null}
GET /teams/:id/members
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
  "members": [
    {
      "member_id": "0x123456789...",
      "role": "admin",
      "status": "active",
      "has_grant": true,
      "created_at": "2026-06-09T16:45:30.123Z"
    }
  ]
}
```

### curl Example

```bash theme={null}
curl -G https://ksrlmubaxzwufziwarps.supabase.co/functions/v1/v2-agent-state/teams/1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6/members \
  -H "Authorization: Bearer YOUR_SESSION_JWT"
```

***

## GET /teams

List every team the authenticated user belongs to (active memberships only).

```http theme={null}
GET /teams
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "teams": [
    { "id": "1a2b3c4d-...", "name": "alpha-agents", "role": "admin", "rotation_seq": 1, "created_at": "2026-06-09T16:45:30.123Z" }
  ]
}
```

***

## POST /teams/:id/invitations

Invite a new member by email. Admin-gated. Triggers a seat-quota check — if the team is at its seat limit on a paid plan, this **automatically increments the Polar subscription's seat count** and emails the owner; on a plan that doesn't support auto-scaling, it returns `403 seat_limit_reached` instead and emails the owner that a seat is needed.

<ParamField body="email" type="string" required>
  The invitee's email address.
</ParamField>

<ParamField body="role" type="string">
  Access level to grant on acceptance. One of `"admin"`, `"write"`, `"read"`. Defaults to `"read"`.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "invitation_id": "c3d4e5f6-...",
  "team_id": "1a2b3c4d-...",
  "email": "newmember@example.com",
  "role": "read",
  "expires_at": "2026-06-16T16:45:30.123Z"
}
```

***

## POST /teams/:id/guardians/invite

Invite a Shamir key-recovery guardian by email — a distinct invitation type from a regular team member, used for the Managed Key Recovery escrow flow.

<ParamField body="guardian_email" type="string" required>
  The guardian's email address.
</ParamField>

### Response — 201 Created

```json theme={null}
{
  "invitation_id": "d4e5f6a7-...",
  "guardian_email": "guardian@example.com",
  "type": "guardian",
  "expires_at": "2026-06-16T16:45:30.123Z"
}
```

***

## GET /teams/:id/audit

List the team's tamper-evident audit ledger entries, newest first. Requires active membership. Supports optional `actor`, `action`, `from`, `to` query filters and `limit` (default 200, max 1000).

Each row includes `prev_hash` and `entry_hash` so a client can re-verify the SHA-256 hash chain locally — see [Trust Center](/platform/trust) for the chain construction.

```http theme={null}
GET /teams/:id/audit?limit=50
Authorization: Bearer <SESSION_JWT>
```

### Response — 200 OK

```json theme={null}
{
  "team_id": "1a2b3c4d-...",
  "entries": [
    {
      "id": "e5f6a7b8-...", "seq": 42, "actor": "0x123...", "action": "member.add",
      "resource_type": "member", "resource_id": "0x987...", "metadata": { "role": "write" },
      "prev_hash": "a1b2...", "entry_hash": "c3d4...", "created_at": "2026-06-09T16:45:30.123Z"
    }
  ]
}
```

## GET /teams/:id/audit/verify

Re-verify the audit chain's hash integrity server-side and report the result.

### Response — 200 OK

```json theme={null}
{ "team_id": "1a2b3c4d-...", "valid": true, "checked": 42 }
```

If the chain has been tampered with, the response reports the exact `broken_seq`.

## GET /teams/:id/audit/export

Admin-gated (a full ledger export leaving the system is treated as an admin-grade action). Accepts optional `from`/`to` date filters. Returns the same verified, well-formed JSON export used for compliance evidence.
