D driftstack docs

Profile management

A profile is a persistent identity Driftstack maintains across sessions. Cookies, local storage, IndexedDB, and the WebKit-fork’s stealth state survive between session lifetimes when a session binds to a profile.

If a session doesn’t bind a profile, it starts ephemeral — fresh cookies, fresh storage, no continuity. That’s the right choice for one-shot fetches. For workflows that need login state, multi-step flows, or returning-visitor signals, bind a profile (note: programmatic binding via the SDK is planned — see “Bind a session to a profile” below).

Tier limits

Each tier has a profile cap, enforced at POST /v1/profiles creation time. Exceeding the cap returns 402 with a profile_cap_reached body and an upgrade link.

TierProfile cap
Trial pack1
Solo Manual10
Team Manual50
Agency Manual200
API Starter25
API Builder100
API Scale500
EnterpriseCustom

Pricing source of truth: driftstack.dev/pricing.

Self-hosted tiers don’t enforce per-account profile caps — they enforce concurrent-session caps + archetype counts at the fleet level instead.

Create a profile

POST /v1/profiles with at minimum a name. The archetype field is optional and defaults to the locked archetype (iphone16pro_ios18_7_safari26_4 — current iPhone 16 Pro on iOS 18.7 with Safari 26.4). Pin to an older archetype only if you have a behavioural-stability reason.

TypeScript:

const profile = await client.profiles.create({
  name: 'shopper-account-1',
  description: 'Returning-visitor profile for the shopping flow',
});
console.log(profile.id, profile.archetype);

Python:

profile = client.profiles.create({
    "name": "shopper-account-1",
    "description": "Returning-visitor profile for the shopping flow",
})

Go:

profile, err := client.Profiles.Create(ctx, &driftstack.CreateProfileRequest{
    Name:        "shopper-account-1",
    Description: "Returning-visitor profile for the shopping flow",
})

The response is the full Profile:

{
  "id": "prf_01HV...",
  "name": "shopper-account-1",
  "archetype": "iphone16pro_ios18_7_safari26_4",
  "description": "Returning-visitor profile for the shopping flow",
  "last_used_at": null,
  "created_at": "2026-05-07T11:00:00.000Z",
  "updated_at": "2026-05-07T11:00:00.000Z"
}

name is unique within an account. Re-using a name returns 409 Conflict.

List profiles

GET /v1/profiles returns paginated results with cursor-based pagination.

const { data, has_more, next_cursor } = await client.profiles.list({ limit: 50 });
for (const p of data) console.log(p.name, p.last_used_at);

The last_used_at field updates every time a session binds to the profile (once binding lands; see “Bind a session to a profile” below). Sort by it client-side to find recently active profiles.

Get one profile

GET /v1/profiles/:id:

const profile = await client.profiles.get('prf_01HV...');

Bind a session to a profile

Status: planned (V-294 catalog). Programmatic session-to-profile binding via the SDK isn’t yet wired. POST /v1/sessions currently accepts { archetype, purpose, label, metadata } only — no profile_id field. Sessions started via the SDK are profile-less today; the binding lives in the dashboard’s GUI client driver layer.

When the binding lands, sessions created with a profile reference will inherit the profile’s storage state on launch and write new state back on clean destroy (or clean idle-timeout). Without a profile, the session starts ephemeral.

Track the rollout: the V-294 feature catalog lists “Profile-to-session binding” as IN-FLIGHT; the change will be additive (new optional field on the request body).

In the meantime, profiles still serve as long-lived archetype anchors for the dashboard’s GUI flows + as restore-from-snapshot targets (V-312).

Delete a profile

DELETE /v1/profiles/:id. Permanent — storage state is wiped.

await client.profiles.delete('prf_01HV...');

If a session is currently bound to the profile, the deletion blocks until the session ends (or returns 409 Conflict if you set force=false, the default).

Clone a profile (V-313)

POST /v1/profiles/:id/clone. Duplicates the profile metadata into a new row carrying the source’s archetype + description. Underlying storage state is NOT cloned — the new profile starts with a fresh state slot under the same archetype.

// Auto-derived "(copy)" / "(copy 2)" / ... naming.
const copy = await client.profiles.clone('prf_01HV...');

// Or pass an explicit name.
const named = await client.profiles.clone('prf_01HV...', { name: 'staging-mirror' });

Tier-cap + name-conflict are checked the same way as create: 429 if your tier limit would be exceeded, 409 on explicit-name collision, 404 if the source profile isn’t yours or doesn’t exist. The audit-log entry for the new profile carries payload.cloned_from: "profile_<uuid>" (the internal profile_ prefix; see audit-log payload reference for the format).

Snapshots — immutable point-in-time copies (V-312)

A snapshot is a frozen copy of a profile. The parent profile keeps evolving — its name, description, and storage state mutate as you use it. The snapshot does not.

Capture. POST /v1/profiles/:id/snapshots.

const snap = await client.profileSnapshots.capture('prf_01HV...', {
  label: 'before-iOS-26',
  description: 'pre-rollout reference', // optional
});
// snap.id — "psnap_<uuid>"
// snap.parent_archetype, snap.parent_name — frozen at capture time

List. Per-profile or cross-account.

const perProfile = await client.profileSnapshots.listForProfile('prf_01HV...');
const everySnapshot = await client.profileSnapshots.list();

// Iterate every snapshot in your account, walking cursor pages.
for await (const s of client.profileSnapshots.iterate()) {
  console.log(s.label, s.captured_at);
}

Restore. Creates a NEW profile (the original is never modified).

const restored = await client.profileSnapshots.restore(snap.id, {
  name: 'restored-from-baseline',
});

Tier-cap + name-conflict apply the same way as create. The audit-log entry on the new profile carries payload.restored_from_snapshot: "psnap_<uuid>" (the public psnap_ prefix; see audit-log payload reference).

Delete. DELETE /v1/profile-snapshots/:id.

Snapshots have no automatic lifecycle. Capture as many as you want; they sit until you delete them. Deleting the parent profile sets parent_profile_id to null but keeps the snapshot — the captured parent_archetype, parent_name, and state stay restorable.

The same surface is available in the Python and Go SDKs as client.profile_snapshots.* and client.ProfileSnapshots.* respectively.

Profile-name conventions

Profile names are free-form strings up to 120 characters. Conventions that work well:

  • <flow>-<account-or-persona>-<index> — e.g. signup-test-acct-3, shopper-personaA-1.
  • <environment>-<purpose> — e.g. staging-smoke, prod-canary.

Names ARE visible in the dashboard and any team-member access logs. Don’t put PII or secrets in profile names; use description for human notes if you need them.

Archetypes

An archetype is the device + OS + browser fingerprint a session impersonates. The locked default (iphone16pro_ios18_7_safari26_4) tracks current iPhone — when iOS 18.8 ships, the locked archetype slug bumps and new profiles default to the new fingerprint.

Profiles pin to one archetype at creation time. The pin is stable: a profile created against iphone16pro_ios18_7_safari26_4 keeps that fingerprint forever, even after the locked default rolls forward. This stability is intentional — re-using a profile shouldn’t surprise downstream behavioural-detection systems with a sudden iOS bump.

To migrate a profile to a newer archetype, create a fresh profile pinned to the new archetype and walk through any session-state migration manually.

What gets persisted

When a session binds to a profile, on session destroy the profile’s storage state captures:

  • HTTP cookies (incl. Secure, HttpOnly, SameSite attributes; partition keys preserved).
  • WebStorage: localStorage and sessionStorage (per-origin partitions).
  • IndexedDB databases (per-origin partitions).
  • Service Worker registrations + Cache Storage entries (per-origin partitions).
  • The WebKit-fork’s stealth state (canvas/font/audio noise seeds — re-used across sessions to keep the fingerprint stable).

What does NOT persist:

  • The DOM tree of the last page (sessions always start with a fresh page).
  • Active WebSocket / EventSource connections (open a fresh one on the next session).
  • WebRTC peer connections.

Next steps

  • Session lifecycle — full lifecycle reference including profile-binding semantics.
  • API versioning — how additive fields roll out without breaking existing SDK calls.
  • Webhook eventsprofile.created, profile.deleted event subscriptions.