---
title: "Create a persisted browser session"
method: POST
path: "/v1/sessions"
tags: ["Sessions"]
---

# Create a persisted browser session

`POST /v1/sessions`

## Request

- Base URL: `https://api.spicrawl.com`
- URL: `https://api.spicrawl.com/v1/sessions`
- Auth: HTTP bearer

Creates a session: a sealed cookie jar and web-storage snapshot, pinned to one
exit intent, that you reuse by passing its `id` as `session_id` on `/v1/scrape`.

Pitfalls:
- Every field is optional. An empty body (Content-Length 0) means "all defaults"; `{}` also works.
  Unknown fields are refused with 400.
- Sessions are single-writer. While a render holds the lease, a second scrape on the same session,
  and release/delete without `force=true`, get 409 `ERR::SESSION::BUSY` with a `Retry-After`.
  Serialise work per session, or create one session per concurrent worker.
- `rotate_ip` and `sticky_key` are mutually exclusive (400 `ERR::REQUEST::INCOMPATIBLE_FLAGS`).
  Omitting both pins the exit: `sticky_key` is derived from the session id.
- `ttl_seconds` above the organization's maximum is refused (400), never clamped. It slides on use
  but never past `hard_expires_at`.
- `proxy_country` requires `premium_proxy: true`. `fingerprint` is an advanced option and may be refused with 400.
- A project may hold only one active session per `sticky_key` (409 `ERR::REQUEST::CONFLICT`).
- An organization may hold at most 100 live sessions (429 `ERR::LIMIT::SESSIONS_EXCEEDED`,
  Retry-After 30).
- `engine` is optional (`fetch`, `js` or `browser`): omit it and the session's rendering is chosen for you. Scrapes with the session run on that tier, so a `js` session refuses `actions`, `block_resources`, a screenshot and a PDF (400 `ERR::REQUEST::CAPABILITY_UNSUPPORTED`).

## Request body

- SessionCreateRequest: Body of `POST /v1/sessions`. All fields optional; unknown fields are refused with 400.
  - `engine` 'fetch' | 'js' | 'browser': The rendering tier a session is bound to for its lifetime (`fetch` is HTTP only, `js` is JavaScript rendering, `browser` is full-browser rendering). Chosen automatically when the session is created unless the create names one.
  - `ttl_seconds` integer: Sliding lifetime in seconds. Omitted or 0 means 1800, reduced to the organization maximum with a warning if that is lower. Values above the organization maximum (default 604800, i.e. 7 days) are refused with 400 naming the real maximum.
  - `sticky_key` string: Coming soon. Pins the exit IP across the session. Letters, digits and hyphens, not starting or ending with a hyphen. Omitted means derived from the session id (still pinned). Must be unique among active sessions in the project. Cannot be combined with `rotate_ip`.
  - `region_pool` string: Coming soon. Regional exit pool to draw from. Trimmed; longer than 64 characters is refused.
  - `rotate_ip` boolean: Coming soon. Keep the cookie jar but let the exit IP change between requests. Use for many-page crawls; avoid for replayed logins, which get challenged on a moving IP. Refused together with `sticky_key`.
  - `premium_proxy` boolean: Coming soon. Use the residential tier instead of datacenter. Same meaning as on `/v1/scrape`.
  - `proxy_country` string: Coming soon. ISO 3166-1 alpha-2 exit country, case-insensitive. Requires `premium_proxy: true`, otherwise 400 `ERR::REQUEST::INCOMPATIBLE_FLAGS`.
  - `fingerprint` object, nullable: Browser fingerprint pin, stored verbatim. Advanced option; a non-empty object is refused with 400 where the session's rendering cannot apply it. `null` and `{}` count as absent.
  - `domain_scores` object: Seed per-domain health, keyed by lowercase registrable domain (e.g. `example.com`). Scores must be non-negative. Above 64 entries, the least useful (unblocked, lowest score, oldest) are evicted rather than refused.
  - `session_context` SessionContext: Everything a browser holds that makes it logged in. Maps and arrays are never null in responses.
    - `cookies` SessionCookie[]: Browser-wide cookie jar. At most 2000.
      - `name` string, required: Cookie name. Required; trimmed.
      - `value` string: Cookie value.
      - `domain` string, required: Cookie domain; a leading dot (".example.com") makes it domain-wide and is preserved. Required.
      - `path` string: Cookie path. Empty becomes `/`.
      - `expires` number: Expiry in seconds since the epoch (CDP units). Omit for a session cookie. Zero or negative values (such as CDP's -1) are treated as a session cookie and omitted.
      - `http_only` boolean: HttpOnly flag.
      - `secure` boolean: Secure flag. Required to be true when `same_site` is `None`.
      - `same_site` 'Strict' | 'Lax' | 'None': SameSite attribute in exact CDP casing. Omit when unset; it is not defaulted. `None` without `secure: true` is refused.
    - `local_storage` object: Origin (bare `http(s)://host[:port]`, no path) to key/value map. At most 100 origins and 1000 keys per origin.
    - `session_storage` object: Same shape and limits as `local_storage`. Many single-page apps keep access tokens here; include it when importing a login.
    - `indexed_db` object: Origin to list of IndexedDB databases, each carried verbatim as JSON. At most 50 origins and 50 databases per origin. Treat as opaque.

## Response `201`

Session created. Use `id` as `session_id` on `/v1/scrape`. Read `warnings` for any decision made on your behalf (e.g. a reduced default TTL).

- Session: A session as every session endpoint reports it. Never contains cookies.
  - `id` string, required: Session id (26-character ULID). Pass it as `session_id` on `/v1/scrape`.
  - `project_id` string, required: Project the session belongs to (the creating key's project).
  - `engine` 'fetch' | 'js' | 'browser': The rendering tier a session is bound to for its lifetime (`fetch` is HTTP only, `js` is JavaScript rendering, `browser` is full-browser rendering). Chosen automatically when the session is created unless the create names one.
  - `status` 'active' | 'released' | 'expired', required: Derived from the clock, not a stored flag. `active` sessions can be used; `released` and `expired` ones have no context and cannot be revived. Create a new session.
  - `sticky_key` string, required: Exit pin in use. The lowercase session id when you did not set one; empty string when `rotate_ip` was set.
  - `proxy` SessionProxy, required: The session's exit intent. Not an endpoint or IP.
    - `tier` 'none' | 'datacenter' | 'residential', required: Exit tier. `residential` when created with `premium_proxy`, otherwise `datacenter`. `none` is reserved and not produced by the create endpoint.
    - `country` string: Lowercase alpha-2 exit country. Omitted when no country was requested.
    - `region_pool` string: Regional pool requested at creation. Omitted when none.
  - `created_at` string, date-time, required: Creation time, RFC 3339 UTC.
  - `last_used_at` string, date-time, nullable, required: Last time a successful scrape used the session. Null means never used.
  - `expires_at` string, date-time, required: Current expiry. Each successful scrape moves it to that time + the session's `ttl_seconds` (never earlier than it was, never past `hard_expires_at`). After it passes the context is purged.
  - `hard_expires_at` string, date-time: Fixed ceiling set at creation from the organization's maximum TTL. Plan a re-login before this time. Omitted only for legacy rows.
  - `released_at` string, date-time: When the session was released. Present only on released sessions.
  - `usage_count` integer, required: Number of successful scrapes that have used the session. Failed scrapes and `/context` reads do not count.
  - `context` SessionContextSummary, required: Non-secret counts describing the stored context.
    - `schema_version` integer, required: Stored context format version. Currently 1.
    - `cookies` integer, required: Number of cookies stored.
    - `origins` integer, required: Distinct origins across local storage, session storage and IndexedDB.
    - `bytes` integer, required: Serialised plaintext size in bytes. The limit is 1048576.
  - `retired` 'expired' | 'usage_exhausted' | 'explicit': Why a worker took this session out of service. Absent while usable. A retired session should be replaced.
  - `domain_scores` object: Per-domain block evidence the workers maintain. Omitted when empty.
  - `fingerprint` object: Browser fingerprint, as stored. Omitted when none was set.
  - `lease` SessionLease: Present only while a render holds the single-writer lease. While present, other scrapes, release and delete get 409 `ERR::SESSION::BUSY`.
    - `holder` string: Identifier of the worker holding the lease. Diagnostic only.
    - `expires_at` string, date-time, required: When the lease lapses at the latest. Retry after this time.
  - `warnings` string[]: Decisions made on your behalf at creation, e.g. a TTL reduced to the organization maximum. Only on the create response; omitted when empty.

## Other responses

- `400`: The request is invalid. Not retryable; fix the request using `code` and `diagnostics.hint`.
- `401`: Missing, invalid, revoked or expired key. Not retryable with the same key.
- `403`: The key lacks the scope this route requires (`ERR::AUTH::INSUFFICIENT_SCOPE`), or the action is not permitted.
- `409`: Conflicts with the resource's current state, for example a session in use (`ERR::SESSION::BUSY`, retryable).
- `413`: `ERR::REQUEST::PAYLOAD_TOO_LARGE`. The request body exceeds 1 MiB, or `session_context` serialises to more than 1 MiB. Nothing is truncated; trim cookies or storage origins you do not need.
- `429`: Rate, concurrency or live-session limit reached. Retryable after `Retry-After`.
- `500`: Internal error. Retryable when `retryable` is true.
- `503`: No proxy exit or engine capacity was available. Retryable after `Retry-After`; 0 credits.

---

[API](https://skmtc.dev/spicrawl/apis/spicrawl-api.md) · [All operations](https://skmtc.dev/spicrawl/apis/spicrawl-api/llms.txt) · [OpenAPI document](https://skmtc.dev/spicrawl/apis/spicrawl-api/revisions/c4b4c6f97219?raw)
