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.
Team directory reads (GET /v1/team/invites, /members, and
/owners) require broad read or account_owner. Team membership
mutations—including accepting an invite—require account_owner.
The dashboard’s web session carries account_owner automatically.
Status (shipped — 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 theadminrole 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_idon every membership and invite. - Member account. A separate
accountsrow (own login, own email) joined to the owner viateam_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) oradmin(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
memberandadminroles. - Write endpoints (POST / PATCH / DELETE / api-keys rotate)
require
adminrole on the team.memberrole gets403. - Agent-session exception.
/v1/agent-sessionscontains AI transcripts and live-control state, so its collection and:idsurface requireadminfor both reads and writes.
Endpoints that honor the header :
/v1/sessions(GET / POST / DELETE) +/:id/{navigate,interact, wait,capture,gui-input,state}/v1/agent-sessions(GET collection / POST create /:idreads and controls) — an admin member can list and operate the owner’s runs or launch the owner’s profile; each run scopes to the owner (counts against the owner’s cap, ships the owner’s per-profile DEK).memberrole gets403on this whole surface./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, owner_email (falls back to
acc_<id> when unknown), owner_name (nullable), 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[].
Requires broad read or account_owner.
Invite a team member
POST /v1/team/invites — sends an email with a 7-day accept link to
the invitee.
Requires account_owner.
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.
Requires broad read or account_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.
Requires account_owner; a dashboard web session satisfies this.
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.
Requires broad read or account_owner.
Remove a member
DELETE /v1/team/members/:id — owner-scoped. 404 if the membership
isn’t owned by the calling account.
Removal also revokes the API keys that member minted on your account.
An admin member can mint keys against your workspace, and such a key
authenticates as your account — it does not stop working when their
membership ends. So removal revokes every live key created by that member,
in the same transaction as the membership delete.
Two consequences worth planning for:
- If one of those keys is used by your own systems, it stops working at
removal. Mint a replacement under your own account first, then remove the
member. The
team.member_removedaudit entry lists the revoked key ids, so you can always see afterwards exactly what was invalidated. - Keys created before this behaviour shipped have no recorded creator and are
not revoked, because there is nothing to attribute them to. Review
GET /v1/api-keyswhen offboarding a long-standing member.
The team itself
Everything above is about MEMBERSHIP — who belongs to your team. These two are about the team as a thing with a name.
GET /v1/teams — the teams you own. Requires broad read or account_owner.
{
"data": [
{
"id": "team_3f9a2c1d-...",
"name": "Acme Research",
"slug": null,
"owner_account_id": "acc_...",
"created_at": "2026-08-25T12:00:00.000Z",
"updated_at": "2026-08-25T12:00:00.000Z"
}
]
}
PATCH /v1/teams/:id — rename a team you own. Requires account_owner. Body is
{ "name": "..." }, 1–120 characters. Returns { "team": { ... } }.
Renaming writes a team.updated audit entry carrying both the previous and new
name.
404 covers two cases deliberately: a team id that does not exist, and one that belongs to someone else. They are indistinguishable on purpose — otherwise the status code would tell you which team ids are real.
slug is always null today. The field is published because it exists on
the record, but no endpoint sets one: whether team slugs become public URL
components is still open. It is safe to read and will stay null until that is
decided.
Teams cannot be created through the API. Your team was created for you, and there is deliberately no create endpoint. A second team under the same owner is not yet possible — memberships are currently keyed to the owning account rather than to a team, so a second team could not hold any member who is already on your first one. Creating teams arrives with that change.
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):
| Action | Fires when |
|---|---|
team.member_invited |
owner calls POST /v1/team/invites |
team.invite_accepted |
invitee calls POST /v1/team/invites/accept |
team.member_removed |
owner calls DELETE /v1/team/members/:id |
Use the audit-log export 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.