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.
| Tier | Profile cap |
|---|---|
| Trial pack | 1 |
| Solo Manual | 10 |
| Team Manual | 50 |
| Agency Manual | 200 |
| API Starter | 25 |
| API Builder | 100 |
| API Scale | 500 |
| Enterprise | Custom |
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/sessionscurrently accepts{ archetype, purpose, label, metadata }only — noprofile_idfield. 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,SameSiteattributes; partition keys preserved). - WebStorage:
localStorageandsessionStorage(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 events —
profile.created,profile.deletedevent subscriptions.