D driftstack docs

Team RBAC

Driftstack supports multi-user teams: one owner-account plus zero or more member-accounts joined to it via the /v1/team/* endpoints. Each member uses their own login + their own dashboard sessions. API keys remain account-scoped (shared across the team) and admin-gated.

Status (V-326e — May 2026): end-to-end. Members can read the owner’s resources via X-Driftstack-Account: acc_<owner-uuid> on any /v1/* request; members with the admin role can also write. See “Acting on behalf of an owner” below.

Concepts

  • Owner account. The account that pays the subscription. Shows up as owner_account_id on every membership and invite.
  • Member account. A separate accounts row (own login, own email) joined to the owner via team_members. Cascade-delete on either side removes the membership.
  • Invite. A pending double-opt-in record in team_invites. Generated by the owner; consumed by the invitee. Token-hashed at rest (sha256), 7-day expiry.
  • Role. member (read-only on owner resources) or admin (full read + write).

Acting on behalf of an owner

Once a member is on a team, they can scope ANY request to the owner’s resources by passing the X-Driftstack-Account header:

GET /v1/sessions
Authorization: Bearer ds_live_<member-key>
X-Driftstack-Account: acc_<owner-uuid>

The server validates that the caller is on the team referenced by the header. The response shape is unchanged; resources are scoped to the owner instead of the caller.

Role gating:

  • Read endpoints (GET) accept both member and admin roles.
  • Write endpoints (POST / PATCH / DELETE / api-keys rotate) require admin role on the team. member role gets 403.

Endpoints that honor the header (V-326 / V-330):

  • /v1/sessions (GET / POST / DELETE) + /:id/{navigate,interact, wait,capture,gui-input,state}
  • /v1/profiles (GET / POST / PATCH / DELETE)
  • /v1/api-keys (GET / POST / DELETE / :id/rotate)
  • /v1/webhooks (GET / POST / DELETE) + /:id/deliveries + /v1/webhook-deliveries/:id/replay
  • /v1/account/audit-log (GET) + /audit-log/export (GET)
  • /v1/account/email-preferences (GET / PUT — PUT admin-only)
  • /v1/usage + /v1/usage/series (GET)

Endpoints that do not honor the header (operate on the caller’s own account regardless):

  • /v1/team/* — managing your own team (members + invites).
  • /v1/account/me — always your own profile (with the team list populated so clients know which owners they can act for).
  • /v1/auth/* — authentication is per-caller.

Inverse view: the teams I am ON

Members can list the owners they’re a member of:

GET /v1/team/owners — returns { data: TeamOwner[] } where each entry has owner_account_id, role, and membership_id. Useful for populating an “act as” picker in custom dashboards. The same data is also embedded in GET /v1/account/me under teams[].

Invite a team member

POST /v1/team/invites — sends an email with a 7-day accept link to the invitee.

Request:

{ "email": "[email protected]", "role": "member" }

Response (202):

{ "message": "Invite sent. The invitee can accept via the email link." }

The invitee receives an email containing a token URL. They sign up on Driftstack first if they don’t already have an account, then accept.

List pending invites

GET /v1/team/invites — pending (unaccepted, unexpired) invites for the calling owner.

{
  "data": [
    {
      "id": "inv_…",
      "owner_account_id": "acc_…",
      "invitee_email": "[email protected]",
      "role": "member",
      "expires_at": "2026-05-15T10:00:00Z",
      "invited_by_account_id": "acc_…",
      "accepted_at": null,
      "created_at": "2026-05-08T10:00:00Z"
    }
  ]
}

Accept an invite

POST /v1/team/invites/accept — invitee calls this after signing in to their own Driftstack account.

Request:

{ "token": "<token-from-email>" }

Response (200):

{
  "membership": {
    "id": "mem_…",
    "owner_account_id": "acc_…",
    "member_account_id": "acc_…",
    "member_email": "[email protected]",
    "role": "member",
    "invited_at": "2026-05-08T10:00:00Z",
    "accepted_at": "2026-05-08T10:05:00Z",
    "invited_by_account_id": "acc_…"
  }
}

The accepting account’s email MUST match the invitee email — server returns 409 Conflict otherwise. This prevents accidentally accepting an invite addressed to someone else even if they share the URL.

List members

GET /v1/team/members — confirmed memberships for the calling owner.

Remove a member

DELETE /v1/team/members/:id — owner-scoped. 404 if the membership isn’t owned by the calling account.

SDK examples

TypeScript:

await client.team.invite('[email protected]', { role: 'admin' });
const members = await client.team.listMembers();
await client.team.acceptInvite(tokenFromEmail);
await client.team.removeMember('mem_…');

Python:

client.team.invite("[email protected]", role="admin")
members = client.team.list_members()
client.team.accept_invite(token_from_email)
client.team.remove_member("mem_…")

Go:

client.Team.Invite(ctx, &driftstack.TeamInviteRequest{
    Email: "[email protected]",
    Role:  driftstack.TeamRoleAdmin,
})
members, _ := client.Team.ListMembers(ctx)
client.Team.AcceptInvite(ctx, tokenFromEmail)
client.Team.RemoveMember(ctx, "mem_…")

Audit log

Every team mutation writes an entry to the customer audit log (/v1/account/audit-log):

ActionFires when
team.member_invitedowner calls POST /v1/team/invites
team.invite_acceptedinvitee calls POST /v1/team/invites/accept
team.member_removedowner calls DELETE /v1/team/members/:id

Use the audit-log export (V-297) to download the team history at any time:

curl -H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
  "https://api.driftstack.dev/v1/account/audit-log/export?format=csv" \
  > team-history.csv

Privacy / DPA

A member is a separate Data Subject from the owner. Their account email is processed under Privacy §3.1 (Account data) on the same legal basis as any other Customer contact.