Profiles
A profile is a named, persistent browser identity Driftstack
remembers between sessions. Cookies, localStorage, IndexedDB,
service workers, and any other state the browser retains are kept
under one logical handle so you can resume where you left off.
A profile’s metadata is intentionally light — a name, device profile
(archetype), the optional fields you set (description, folder, tags,
icon, note, notes, and the proxy and launch settings
below), and timestamps such
as last used. The browser state
itself is managed by Driftstack and isn’t directly exposed through
this API.
Tier caps
Each plan limits the number of profiles an account keeps. Only live profiles
count: a profile in the trash counts toward no limit, so deleting a profile
frees its slot at once (see Delete below).
Crossing the cap on POST /v1/profiles (or
POST /v1/profiles/:id/clone) returns 429 Tier limit. The caps are
below (the public @driftstack/api-types package also exports them as
PROFILES_PER_TIER):
| Plan | Tier id | Profiles cap |
|---|---|---|
| Trial | trial |
1 |
| Starter | starter_v3 |
25 |
| Pro | pro_v3 |
100 |
| Team | team_v3 |
300 |
| Scale | scale_v3 |
1,000 |
| Enterprise | enterprise |
Unlimited |
Enterprise has no profile cap.
The earlier plans, no longer offered, keep their caps for the accounts that hold them:
| Plan | Tier id | Profiles cap |
|---|---|---|
| Personal | solo_manual |
10 |
| Team | team_manual |
50 |
| Agency | agency_manual |
200 |
| API Starter | api_starter |
25 |
| API Builder | api_builder |
100 |
| API Scale | api_scale |
500 |
Resource shape
{
"id": "prof_<uuid>",
"name": "production",
"archetype": "iphone15_ios18_7_safari26_5",
"description": "primary prod-data scrape profile",
"folder": "checkout-flows",
"tags": ["prod", "us-east"],
"icon": "🛒",
"note": "rotate cookies weekly",
"last_used_at": "2026-05-09T22:00:00.000Z",
"created_at": "2026-04-15T11:30:00.000Z",
"updated_at": "2026-05-09T22:00:00.000Z"
}
name— unique within the account. Lowercase + hyphen recommended; max 120 chars. Must start and end with an alphanumeric character; allowed inner characters are letters, digits, spaces, underscore, hyphen, and dot. Leading/trailing whitespace is trimmed.archetype— the pinned device profile: the iPhone model, iOS version and Safari version. When omitted, the profile gets a randomly chosen iPhone model from those your plan includes that are open for new profiles, drawn once and kept. Every paid plan includes every device; the free trial includes the iPhone 13 and iPhone 13 mini only. When supplied, the id must be present in the currentGET /v1/archetypesresponse; this includes any older combination the platform still marksavailable. Once set, the device profile is fixed for that profile’s lifetime.description— free-form, max 2048 chars; nullable.folder— optional organising folder name;nullwhen the profile isn’t filed under a folder.tags— array of free-form organising tags;[]when none are set.icon— optional short emoji shown in the dashboard / GUI;nullfalls back to a monogram. Synced server-side so it follows the account across machines.note— optional short inline note;nullwhen unset. Synced server-side alongsideicon.last_used_at— updated when a session is created against this profile.nulluntil first use.
Create
POST /v1/profiles
{
"name": "production",
"archetype": "iphone15_ios18_7_safari26_5",
"description": "primary prod-data scrape profile"
}
Only name is required; archetype, description, folder, tags,
icon, note, default_proxy_id, geolocation and
stop_on_exit_ip_change are optional. Returns the created profile with
200 (not 201), the same status an Idempotency-Key replay returns.
Use GET /v1/archetypes to generate the request from the live selectable
catalog. Any id absent from the current response is rejected before anything
is written. Omitting the field gives the profile a randomly chosen iPhone
model from those your plan includes that are
open for new profiles; there is no default device.
Send an Idempotency-Key header (one new key per create; see
Idempotency keys) to make a retry safe: the same key
with the same body returns the profile the first request created (200), not
a second profile or a 409 for the name it just took. Keys are per account (the
workspace owner’s when you act on a team) and are kept for 30 days.
Errors:
400 ValidationFailed— invalid name shape, missing required field,descriptionover 2048 chars, or anarchetypeabsent from the live selectable catalog.409 Conflict—namealready exists in this account (nocode).409 Conflictwithcode: "idempotency_key_reused"— theIdempotency-Keywas used before with a different body. Nothing was created; use a new key.409 Conflictwithcode: "profile_from_key_gone"— the profile thisIdempotency-Keycreated is in the Trash or no longer in the account (profile_id, andprofile_state).trashed: it was deleted, or transferred to another account (a transfer leaves it in your Trash);deleted: it has left the Trash. Nothing was created again. If you deleted it, restore thetrashedprofile from the Trash; otherwise use a new key.409 Conflictwithidempotency_status: "in_progress"— the first request with this key has not finished. Send the same request again with the same key.400 ValidationFailed— anIdempotency-Keylonger than 255 characters or containing whitespace or non-printable characters.429 TierLimit— account at the profile cap. Body extension:{limit, current, resource: "profile", tier, trash_count}.currentcounts live profiles only. Thedetailreads, for example, “Your plan (Trial) keeps 1 saved profile, and this account already has 1. Delete a profile to free a slot, or choose a plan with more profiles.” A plainDELETE /v1/profiles/:idfrees the slot at once: profiles in the trash count toward no limit. This refusal carries nocode.trash_countis still sent for one release so older clients keep working, but it is always0and will then be removed;code: "profile_cap_trash"is no longer sent. The same applies to Clone, Import and restoring a Snapshot. A Launch refused because the account holds more live profiles than its plan (code: "over_plan_limit") also carriestrash_count: 0.
List
GET /v1/profiles?limit=50&cursor=<...>
{
"data": [<profile>, ...],
"has_more": false,
"next_cursor": null
}
limit 1-100 (default 50). Cursor is the prior page’s last id;
ordering is created_at desc, id desc for stable tie-break.
This lists live profiles only. Profiles you deleted are in the trash
for 7 days and count toward no limit: list them with
GET /v1/profiles?status=trashed (or GET /v1/profiles/trash, the same
rows), and bring one back with Restore. status=active is
the default; any other status is a 400. The bracket form
(status[]=trashed) is read as the same parameter; sending status more
than once, both forms together, or any other bracketed form
(status[0]=trashed) is a 400.
status=trashed returns the whole trash in one page, exactly as
GET /v1/profiles/trash does: limit and cursor are ignored (not
applied and not validated), has_more is always false and next_cursor
is null.
Get one
GET /v1/profiles/:id
Returns 404 if the profile doesn’t exist or belongs to a different account (we don’t leak existence cross-account).
Patch (rename + edit description)
PATCH /v1/profiles/:id
{
"name": "production-eu",
"description": "EU-region primary"
}
Every field is optional: name, description, folder, tags, icon,
note, notes, and the proxy and launch settings
below; pass
description: null to clear. The
device profile is intentionally not editable — to move to a different
device, create a new profile with the new archetype, then delete the
old profile after migration.
Launch
POST /v1/profiles/:id/launch
{ "label": "checkout-run-2026-05-20", "proxy_id": "6f1c2a9e-4b7d-4c1e-9a3f-2d8e5b6c7a10" }
label is an optional override with the same 120-character maximum as direct
session creation. proxy_id is one of your saved proxies, the same field
POST /v1/sessions takes; omit it to launch through the proxy the profile is
bound to. network_detail: true asks for the session’s network request
detail, as on POST /v1/agent-sessions;
read it on the agent session the launch runs as (listed by
GET /v1/agent-sessions as a manual session whose driftstack_session_id
names this session). The body is strict: any other key, including raw proxy, is
rejected before profile lookup or session creation. Everything else flows from
the profile (the profile’s archetype and metadata are inherited, and
last_used_at is updated automatically). One-shot wrapper around POST /v1/sessions — equivalent to:
{
"profile_id": "prof_<uuid>",
"archetype": "<profile.archetype>",
"metadata": { "profile_id": "...", "profile_name": "..." }
}
but in a single round-trip, and the server stamps the linkage on the session’s metadata so the audit + usage trail tie back to the profile.
Returns the freshly-minted session (same shape as POST /v1/sessions). The customer then drives the session via the
normal navigate / interact / wait / capture /
destroy verbs (or via the desktop app’s Live session view).
The launch runs through a proxy of yours exactly as
POST /v1/sessions
describes: the proxy_id you send, or the proxy the profile is bound to.
The session it returns names that proxy in proxy_id, as every later read of
the session does. Where every session runs through a proxy of yours (hosted Driftstack runs them
that way), a launch with neither is answered 422 proxy-required, and one from
a profile whose proxy was deleted 409 (code: "proxy_detached"). A
session launched through a proxy cannot run the step-by-step verbs yet (they
answer 503 and leave it ready); to drive the profile’s pages through your
proxy, use POST /v1/agent-sessions with its profile_id and a proxy_id.
Errors:
404if the profile isn’t owned by the effective account — your own, or the owner you are acting as viaX-Driftstack-Account(deliberate anti-enumeration — aprofile_idoutside that account is indistinguishable from a missing one).403if you sendX-Driftstack-Accountfor a team you are a member of but not an admin of: launching a profile on a team owner requires the admin role on that team.409 profile-in-useif the profile already has a live session — a profile can run only one session at a time (the body’sactive_session_idnames the live one). End it first if you created it, then launch.409 device-unavailableif the profile’s device is on hold or still in development, so it cannot start a session. Nothing is started; the body’sarchetypenames the device andheld_reasonis the reason for the hold in one plain sentence when there is one.400for an unknown body key, a label longer than 120 characters, an explicit rawproxyfield, or aproxy_idnaming an HTTP proxy. Raw proxy data is never applied.400(code: "network_detail_not_offered") fornetwork_detail: truewhere the deployment does not offer request detail, or where the launch does not run through one of your proxies. Nothing is launched.404(resource: "proxy") if theproxy_idis not one of your saved proxies, and403(feature: "vpnEgress") for a VPN proxy on a plan without VPN egress.409withcode: "proxy_detached"if noproxy_idwas sent and the profile’s proxy was deleted. Choose a proxy.422 proxy-requiredif a proxy of yours is required and there is none (noproxy_id, and the profile is bound to no proxy);422 proxy-validation-failedif the proxy could not be used.429 tier-limitwithcode: "over_plan_limit"if the account holds more saved profiles than its plan keeps and this one is not among the most recently used, up to the plan’s number (on the trial, only the most recently used one launches). Nothing is deleted.429 tier-limitwithcode: "trial_session_time_used"once the free trial’s 2 hours of session time are used up.- Any other error the underlying
POST /v1/sessionscan return, most commonly429when the concurrent-session cap is reached.
Clone
POST /v1/profiles/:id/clone
{ "name": "production-staging" }
Body fully optional. When name is omitted the server auto-derives
a non-conflicting ${source} (copy) / (copy 2) / (copy 3) …
naming (caps at 99 to avoid runaway loops; rejects with 409 if it
gets there).
The clone inherits source’s archetype + description, and its
free-text notes come along, as they do on a transfer (a snapshot never
carries them). Underlying
browser state is NOT cloned — the new profile starts with a fresh
state slot under the same device profile. Use clone primarily for:
- Keeping a copy of a profile’s settings before you replace it with a new profile on a different device.
- Splitting a busy production profile into per-environment copies before they diverge.
- Pre-creating staging profiles ahead of a load test.
Returns the cloned profile (same shape as create). The audit_log
entry for profile.created carries payload.cloned_from: profile_<source-id> so the audit shows provenance.
Errors mirror create: 429 if the cap would be exceeded by the clone, 409 on explicit-name collision, 404 if the source isn’t found / not owned by the caller.
Export / Import
Export a profile as a versioned, metadata-only JSON envelope, then
import it (in the same account or another) to mint a fresh profile
from it. This is how you give a profile to another Driftstack account:
the source profile is left intact, and you move the JSON yourself
(commit it, attach it, hand it to a teammate). Only the metadata
travels — name, archetype, description and the free-text
notes. Underlying browser state is not in the envelope.
Import is a new-profile write, so envelope.profile.archetype must be present
in the current selectable catalog. An export of an older retained profile still
succeeds, but importing that envelope returns 400 ValidationFailed if its
pinned id is no longer selectable, or if that device is closed for new
profiles (code: "device_closed_for_new_profiles"; see
GET /v1/archetypes). A clone and a snapshot restore are
refused the same way.
Export
GET /v1/profiles/:id/export
{
"version": 1,
"exported_at": "2026-05-20T12:00:00.000Z",
"source_profile_id": "prof_<uuid>",
"source_account_id": "<account-uuid>",
"profile": {
"name": "production",
"archetype": "iphone15_ios18_7_safari26_5",
"description": "primary prod-data scrape profile",
"notes": "Signed in as the EU test shopper. Cart has two items."
}
}
source_profile_id and source_account_id are informational only —
they record where the file came from. Importing always mints a fresh
id, into whatever account holds the calling key. Consumers must honor
the envelope version and reject versions they do not understand.
Import
POST /v1/profiles/import
{
"envelope": { "version": 1, "...": "..." },
"name_override": "production-copy"
}
envelope is a v1 export envelope (paste the file). name_override
is optional — rename on import without editing the file; when
omitted, envelope.profile.name is used. envelope.profile.notes
is restored onto the new profile; an envelope exported before notes
existed has no notes key and imports with notes: null. Returns the created
profile (same shape as create). Tier-cap + name-conflict semantics
match Create: 429 if the import would exceed your
profile cap, 409 if the name already exists. Importing an envelope
from a different account is permitted (file-based transfer between
teammates).
Errors:
400 ValidationFailed— the envelope is malformed, is not a v1 shape, or carries anarchetypeabsent from the live selectable catalog.409 Conflict—name(orname_override) already exists.429 TierLimit— importing would exceed your tier’s profile cap.
SDK usage:
const envelope = await client.profiles.export('prof_<uuid>');
const restored = await client.profiles.import({ envelope });
Export is a read — any valid bearer with read scope. Import
requires the write:profiles scope on the calling key (a broad
write key also satisfies it). Team RBAC: X-Driftstack-Account
is honored — admin members can import on the owner’s account.
Snapshots
Snapshots are immutable point-in-time metadata records of a profile. The parent profile keeps evolving — its device profile, name, description, and underlying browser state mutate as you use it. The snapshot’s recorded metadata (device profile, name, description) is frozen the moment you capture it. Browser state (cookies, logins) is NOT captured today — see /api/profile-snapshots for the full contract.
Capture
POST /v1/profiles/:id/snapshots
{ "label": "before-iOS-26-rollout", "description": "optional, max 2048 chars" }
The response carries the snapshot’s id (prefix psnap_),
parent_profile_id, parent_archetype, parent_name (frozen at
capture time), and captured_at.
List
GET /v1/profiles/:id/snapshots — newest-first, paginated.
GET /v1/profile-snapshots — every snapshot the effective account owns — your own, or the owner you are
acting as via X-Driftstack-Account, across all
profiles. Same pagination shape.
Get one
GET /v1/profile-snapshots/:id
Restore
POST /v1/profile-snapshots/:id/restore
{ "name": "restored-from-baseline" }
Creates a NEW profile carrying the snapshot’s parent_archetype +
description. The original parent profile is NOT modified — even
if it has been renamed, edited, or deleted in the meantime. The
new profile counts against your tier cap (429 if it would exceed)
and 409s on name collision. The audit_log entry for
profile.created carries payload.restored_from_snapshot: psnap_<id>.
Delete
DELETE /v1/profile-snapshots/:id → 204 No Content.
Snapshots have no automatic lifecycle. Capture as many as you want;
they sit until you delete them. Deleting the parent profile does NOT
delete the snapshot — the captured parent_archetype + parent_name +
description remain restorable. Note that DELETE /v1/profiles/:id is a
soft delete into the 7-day recycle bin (see below), so the snapshot’s
parent_profile_id stays populated while the parent sits in the bin —
it only becomes null once the profile is purged, restored, or the bin
expires it. Until then the id is present but a GET on that profile
404s, so do not treat a non-null parent_profile_id as proof the parent
is reachable.
Profiles pinned to a retired device
Catalog restrictions apply to new direct session/profile creates and profile imports. Existing profiles keep their stored device profile even after it leaves the selectable catalog. They remain listable, readable, clonable, snapshot-restorable, and launchable; these compatibility operations do not silently rewrite the pinned browser identity.
Delete (recycle bin)
DELETE /v1/profiles/:id moves the profile to the trash.
DELETE /v1/profiles/:id?permanent=true deletes it permanently.
A profile in the trash counts toward no limit: the delete frees its slot
at once, for create, clone, import, transfer, snapshot restore and launch
alike. It stays in the trash for at least 7 days and is then removed for
good.
Deleting it permanently (?permanent=true on this call, or Purge below for a
profile already in the trash) only removes it sooner; it is not needed to
free a slot.
Without permanent, the delete is a soft delete: the profile is hidden
from GET /v1/profiles and can’t be launched or looked up by id, but it’s
recoverable (see Restore below). Deleting also frees the name
immediately, so you can create a new profile with the same name right away.
With permanent=true, a live profile is moved to the trash and purged in the
same call, and a profile already in the trash is purged. Its stored browser
state is deleted, and it cannot be restored.
Same scope and team-admin rule as a plain delete. A profile with a live
session is refused with 409 Conflict and nothing is deleted: stop the
session if you started it, then retry.
permanent=false, an empty permanent= or no parameter is the soft delete.
Any other value (permanent=1, permanent=yes) is refused with
400 ValidationFailed and nothing is deleted.
Returns 204 No Content, and is idempotent — re-deleting an already-trashed
profile, permanently deleting one that is already gone, or an id that was
never yours also returns 204.
Trashed profiles are retained for at least 7 days, then permanently
purged (the underlying state is hard-deleted at that point and is no longer
recoverable). Each trashed profile carries purges_at: the earliest time the
profile is permanently deleted (at least 7 days after the delete; removal
follows at the next daily clean-up). The clean-up runs once a day, at 04:00
UTC, so a profile is removed after its purges_at, normally within a day,
and never before it; until it is removed it can still be restored. Profiles that were already in the
trash when the window changed from 30 days to 7 keep the date they already had,
and never less than 7 days from the change.
List trash
GET /v1/profiles/trash → { "data": [ ...profile ] }
Lists your trashed profiles, newest-deleted first. Each row has the same shape
as a live profile plus two non-null timestamps: deleted_at (when it was
deleted) and purges_at (the earliest time the profile is permanently
deleted: at least 7 days after the delete; removal follows at the next daily
clean-up). Both are null on a live profile. Read scope suffices (both team
roles).
{
"id": "prof_<uuid>",
"name": "production",
"deleted_at": "2026-10-08T09:15:00.000Z",
"purges_at": "2026-10-15T09:15:00.000Z"
}
(Other profile fields omitted.) Profiles in the trash count toward no limit.
Restore
POST /v1/profiles/:id/restore → the restored profile
Un-trashes a soft-deleted profile (write scope; admin-only on a team workspace,
same as delete). The restored profile is live again and its deleted_at and
purges_at are null.
A restore needs two things:
- A free slot. The restored profile counts toward your plan’s profile limit again, so the account must be under its cap.
- A restore left. Each account may restore a set number of profiles in
any rolling 30 days. The number follows the plan: 10% of the plan’s
profiles, and never fewer than 3; Enterprise is a flat 100 (
@driftstack/api-typesexports it asPROFILE_RESTORES_PER_TIER).
| Plan | Restores per rolling 30 days |
|---|---|
| Trial | 3 |
| Starter | 3 |
| Pro | 10 |
| Team | 30 |
| Scale | 100 |
| Enterprise | 100 |
The earlier plans, no longer offered, follow the same rule: Personal 3,
Team (team_manual) 5, Agency 20, API Starter 3, API Builder 10 and
API Scale 50.
Only a successful restore from the trash uses one. A refused restore, a snapshot restore, a purge, a permanent delete and the automatic purge use none. The desktop app’s Trash view shows how many restores are left.
Returns:
404if the id isn’t one of your trashed profiles.409 Conflictif a live profile has since taken the trashed profile’s name — rename the live one (or the restored profile) first, then retry.429 TierLimitwithcode: "restore_cap"if the account has used its restores for the rolling 30 days. Body extension:{limit, current, resource: "profile_restore", tier, code, window_days, next_restore_at}.limitis the plan’s number,currentis how many the account has used,window_daysis30andnext_restore_atis the ISO time the next restore becomes available. Thedetailreads, for example, “Your plan (Pro) allows 10 restores from Trash in 30 days, and this account has used 10. The next one is available on 2026-10-21. Profiles in Trash are kept until the date shown beside each one.”429 TierLimitwithcode: "restore_needs_slot"if the account’s live profiles are at the plan’s number. Body extension:{limit, current, resource: "profile", tier, code}.currentcounts live profiles. Thedetailreads, for example, “Your plan (Pro) keeps 100 profiles and this account has 100. Delete one or choose a bigger plan, then restore.”
Both 429s leave the profile in the trash and use no restore. restore_cap
is checked first, so an account that is out of restores and also at its cap
gets restore_cap. On a team workspace both are judged on the plan of the
account that owns the profile. Waiting for next_restore_at does not extend
a profile’s time in the trash: it is still removed at the first daily
clean-up after its purges_at.
Purge (permanent delete)
DELETE /v1/profiles/:id/purge → 204 No Content
Permanently deletes a trashed profile now, without waiting for the 7-day
auto-purge. Its slot was already freed by the delete that put it in the
trash, so purge is not needed to free a slot. The underlying encrypted state is hard-deleted; this is
irreversible (no restore afterward). Write scope (admin-only on a team
workspace, same as delete). Returns 404 if the id isn’t one of your trashed
profiles — purge only acts on the trash, never on a live profile (delete
it first, or use DELETE /v1/profiles/:id?permanent=true, which does both).
The 404’s detail reads: “No profile with this id is in the trash for this
account. List the trash with GET /v1/profiles/trash; for a team owner’s
profiles, send the X-Driftstack-Account header with the owner’s account id
(team admins only).”
Returns 409 Conflict if the profile has a live session; nothing is purged.
Read endpoints (GET) accept any valid bearer with read scope;
write endpoints (POST, PATCH, DELETE) require the write:profiles
scope on the calling key (a broad write key also satisfies it).
One exception, documented here because the endpoint is: POST /v1/profiles/{id}/launch requires write:sessions, not
write:profiles — it creates a session, so it is gated as a session
write. A key scoped to write:profiles alone can manage profiles but
cannot launch one.
Team RBAC: X-Driftstack-Account is honored for both reads and
writes — member roles cannot write on the owner’s account; admin
members can.
Recent activity
GET /v1/profiles/:id/activity
Returns the profile’s recent navigation — every page an agent session
planned to open while using this profile — most recent first, with the
session it belonged to. Each url is the site of the page
(scheme://host[:port]/): the path, query and fragment are not kept.
This is account activity, not browsing history. The profile itself
stores no history; these rows come from the account’s agent session
transcripts, which are operational records of what the account did. That
is why clearing a profile’s history (POST /v1/profiles/:id/trim with
scope: "history", or Clear history in the desktop app) does not
remove them: that action clears the profile’s saved open tabs and
nothing else. Session records are removed only when the account is deleted.
{
"data": [
{
"at": "2026-09-05T08:12:44.120Z",
"url": "https://example.com/",
"agent_session_id": "agt_3f9a2c1d-7b4e-4c8a-9d2f-1e6b5a7c3d9f"
}
],
"sessions_scanned": 12,
"truncated": false,
"pages_withheld": false
}
The response is bounded: the server reads the profile’s most recent
sessions and returns the most recent navigations from them. When
truncated is true, older activity exists but is not returned. Requires
read:profiles. Returns 404 for a profile the account does not own and
503 on deployments where agent sessions are not enabled.
Each session is read from the history its agent works from: in a long
session whose oldest entries were dropped from that history to make room
(see history under
agent sessions), the pages in the
dropped entries are not returned.
The pages and session ids come from agent session records, so they are
shown only to a caller who could read those sessions: the key needs
read:sessions as well, and in a teammate’s workspace
(X-Driftstack-Account) the caller must be an admin there, as for
/v1/agent-sessions. Anyone else still gets the rest of the response —
sessions_scanned and truncated — with data empty and
"pages_withheld": true. Every response carries pages_withheld;
it is false when the pages are included.
Trim cached site data
POST /v1/profiles/:id/trim
Clears a profile’s stored data. What it clears depends on scope,
and three of the four values are destructive of identity state.
With no body it clears only the re-fetchable caches (HTTP/media
cache and per-origin CacheStorage / service-worker registrations) and
keeps cookies, localStorage, IndexedDB and open tabs. That is the
behaviour this endpoint had before scope existed, so an existing
caller that sends nothing is unaffected.
scope |
Clears | Keeps |
|---|---|---|
(omitted) / cache |
Re-fetchable caches | Cookies, site data, tabs |
cookies |
Cookies and per-origin site data — signs the profile out everywhere | Cached files, tabs |
history |
The profile’s remembered open tabs | Logins, cached files |
all |
Caches, site data and tabs together — signs the profile out | The profile and its fingerprint |
No scope alters the profile’s fingerprint: it stays the same device to every site. Clearing a profile’s tabs does not clear the server-side record of session activity, which is governed by the retention policy rather than by this endpoint.
{ "scope": "cookies" }
The body is optional and strict: an unknown key, or a scope outside
the four values, is refused with 400 rather than falling back to the
default — this body selects which data a destructive operation destroys.
Write scope (write:profiles), admin-only on a team workspace, same as
the other mutating routes.
The trim runs against the profile’s last saved state while no session
is using it, so the response is a discriminated 200 body in
every case:
// "ok" — trimmed; the smaller size is persisted immediately
{ "status": "ok", "size_bytes": 18874368, "bytes_reclaimed": 41943040 }
// "unavailable" — nothing ran; reason says why
{ "status": "unavailable", "reason": "profile is currently in use — stop its running session before clearing its data" }
// "timeout" — no reply in time
{ "status": "timeout" }
// "error" — the trim reported a failure; the stored state is untouched
{ "status": "error", "reason": "<failure reason>" }
unavailable covers the expected-inert states: the profile is bound
to a still-running session (stop it first if you started it — a live session would
save its full un-trimmed state back over the trimmed result), the
profile has no saved state yet (a fresh profile has nothing to
trim), or profile storage isn’t enabled on this deployment. On
"ok", size_bytes (the new stored size) updates
the storage meter and launch-quota checks immediately. Returns 404
if the profile isn’t found or isn’t owned by the effective account —
your own, or the owner you are acting as via X-Driftstack-Account.
Proxy, launch settings and revision (2026-09-28)
A profile remembers which proxy it launches through, its fixed location and its stop-on-exit-IP switch — on the account, so the same profile leaves through the same exit on every computer. Three fields on every profile, all settable on create and PATCH:
default_proxy_id— one of the account’s saved proxies (404withresource: "proxy"for any other;400withproxy_scheme: "http"for an HTTP proxy, which cannot carry a session), ornullto unset.proxy_choice— set by the server, three states:"unset"(never chose: a launch without aproxy_idis refused as it always was),"bound"(a launch with thisprofile_idand noproxy_idusesdefault_proxy_id),"detached"(its proxy was deleted: a launch naming it without aproxy_idis409proxy_detacheduntil you choose one — never another proxy, never the first in the list). Send"proxy_choice": "detached"on PATCH to mark a profile detached on purpose (2026-09-29); it is the only value a request may send, and a non-nulldefault_proxy_idbeside it is a400.geolocation({latitude, longitude, accuracy?}ornull) andstop_on_exit_ip_changedefault onto a session created with the profile when the create omits them; an explicit value wins.
revision moves on every edit (a launch’s last_used_at and a
save-back do not move it). Send it back as expected_revision on PATCH
and a profile another computer changed first is refused with 409
(code: "stale_revision", current_revision) and nothing changes; omit
it to write unconditionally. Every edit is recorded as profile.updated
in the audit log with the names of the changed fields.
Clone copies the three fields (same account). Import, transfer and
snapshot restore reset them: the new profile starts "unset" with no
location. The account summary says profile_trash_count beside
profile_count: profile_count and the profile cap both count live
profiles only, and profiles in the trash count toward no limit. It also
carries profile_restores_used and profile_restores_limit, the restores
from the trash the account has made in the rolling 30 days and the number
its plan allows (see Restore).
Lifecycle interaction
A session is bound to a profile at creation time
(POST /v1/sessions { profile_id }). The session carries the
profile’s state forward; on destroy, any changes are saved back to
the profile. A profile can only be used by one live session at a
time: creating a second session with the same profile_id is refused
with 409 profile-in-use until the first session has ended, so saved
cookies and logins are never overwritten by a competing session.
See Session lifecycle for the full flow.