# Create a persisted browser session

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

Source: https://docs.spicrawl.com/api-reference/sessions/sessions-create

## POST /v1/sessions

Operation ID: `sessionsCreate`. API key scope: `sessions`.

Creates a session: a sealed cookie jar and web-storage snapshot, pinned to one engine and 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` requires `engine: camoufox`.
- 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).
- When `engine` is omitted, the default is the first of `obscura`, `chromium` that the deployment runs,
  else `fetch`. Check the `engine` in the response.
- An explicit `engine` must be `fetch` or a render engine this deployment runs; otherwise 503
  `ERR::ENGINE::UNAVAILABLE` and nothing is created. The detail lists the deployed engines.

### Example

```bash
curl -X POST "https://api.spicrawl.com/v1/sessions" \
  -H "Authorization: Bearer $SPICRAWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"engine":"chromium","ttl_seconds":7200,"premium_proxy":true,"proxy_country":"de","sticky_key":"acct-4812"}'
```

### Request body

`application/json`.

| Field | Type | Required | Description |
|---|---|---|---|
| `engine` |  | no | Engine `camoufox` is coming soon. Engine to pin. Case-insensitive. Omitted means the first of `obscura`, `chromium` that this deployment runs, else `fetch`. Choose explicitly if you need a browser-only feature such as localStorage. |
| `ttl_seconds` | integer; default `1800` | no | 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 | no | 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 | no | Coming soon. Regional exit pool to draw from. Trimmed; longer than 64 characters is refused. |
| `rotate_ip` | boolean; default `false` | no | 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; default `false` | no | Coming soon. Use the residential tier instead of datacenter. Same meaning as on `/v1/scrape`. |
| `proxy_country` | string | no | Coming soon. ISO 3166-1 alpha-2 exit country, case-insensitive. Requires `premium_proxy: true`, otherwise 400 `ERR::REQUEST::INCOMPATIBLE_FLAGS`. |
| `fingerprint` | object \| null | no | Camoufox fingerprint pin, stored verbatim. A non-empty object with any engine other than `camoufox` is refused with 400. `null` and `{}` count as absent. |
| `domain_scores` | object | no | 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` |  | no | Initial cookies and storage, e.g. an existing login or the `session_context` from a previous `GET /v1/sessions/{id}/context`. Serialised size limit 1 MiB (413). Omitted means an empty context. |

Example `pinnedLogin`: Pinned residential exit in Germany, 2-hour TTL

```json
{
  "engine": "chromium",
  "ttl_seconds": 7200,
  "premium_proxy": true,
  "proxy_country": "de",
  "sticky_key": "acct-4812"
}
```

Example `importLogin`: Seed with an existing login

```json
{
  "engine": "obscura",
  "rotate_ip": true,
  "session_context": {
    "cookies": [
      {
        "name": "sid",
        "value": "9f2c1e7a",
        "domain": ".example.com",
        "path": "/",
        "expires": 1790000000,
        "http_only": true,
        "secure": true,
        "same_site": "Lax"
      }
    ],
    "local_storage": {
      "https://app.example.com": {
        "token": "eyJhbGciOi"
      }
    },
    "session_storage": {},
    "indexed_db": {}
  }
}
```

### Responses

#### 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).

`application/json`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Session id (26-character ULID). Pass it as `session_id` on `/v1/scrape`. |
| `project_id` | string | yes | Project the session belongs to (the creating key's project). |
| `engine` | string: `fetch`, `obscura`, `chromium`, `camoufox` | yes | Engine a session is pinned to for its whole life. Scrapes on the session run on this engine; `fingerprint` is only valid with `camoufox`. |
| `status` | string: `active`, `released`, `expired` | yes | 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 | yes | Exit pin in use. The lowercase session id when you did not set one; empty string when `rotate_ip` was set. |
| `proxy` | object (`SessionProxy`) | yes | The session's exit intent. Not an endpoint or IP. |
| `created_at` | string (date-time) | yes | Creation time, RFC 3339 UTC. |
| `last_used_at` | string \| null (date-time) | yes | Last time a successful scrape used the session. Null means never used. |
| `expires_at` | string (date-time) | yes | 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) | no | 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) | no | When the session was released. Present only on released sessions. |
| `usage_count` | integer | yes | Number of successful scrapes that have used the session. Failed scrapes and `/context` reads do not count. |
| `context` |  | yes | Size summary of the stored context. Null once the session is not active (the context has been purged). |
| `retired` | string: `expired`, `usage_exhausted`, `explicit` | no | Why a worker took this session out of service. Absent while usable. A retired session should be replaced. |
| `domain_scores` | object | no | Per-domain block evidence the workers maintain. Omitted when empty. |
| `fingerprint` | object | no | Camoufox fingerprint, as stored. Omitted when none was set. |
| `lease` | object (`SessionLease`) | no | Present only while a render holds the single-writer lease. While present, other scrapes, release and delete get 409 `ERR::SESSION::BUSY`. |
| `warnings` | array of string | no | Decisions made on your behalf at creation, e.g. a TTL reduced to the organization maximum. Only on the create response; omitted when empty. |

Example `created`

```json
{
  "id": "01J9ZQ4M7R3T8VX2K5N6P0B1CD",
  "project_id": "01J9Y0000000000000000PROJ1",
  "engine": "chromium",
  "status": "active",
  "sticky_key": "acct-4812",
  "proxy": {
    "tier": "residential",
    "country": "de"
  },
  "created_at": "2026-09-22T10:00:00Z",
  "last_used_at": null,
  "expires_at": "2026-09-22T12:00:00Z",
  "hard_expires_at": "2026-09-29T10:00:00Z",
  "usage_count": 0,
  "context": {
    "schema_version": 1,
    "cookies": 0,
    "origins": 0,
    "bytes": 131
  }
}
```

#### 400

The request is invalid. Not retryable; fix the request using `code` and `diagnostics.hint`.

Headers: `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 401

Missing, invalid, revoked or expired key. Not retryable with the same key.

Headers: `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 403

The key lacks the scope this route requires (`ERR::AUTH::INSUFFICIENT_SCOPE`), or the action is not permitted.

Headers: `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 409

Conflicts with the resource's current state, for example a session in use (`ERR::SESSION::BUSY`, retryable).

Headers: `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 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.

`application/problem+json` (`Problem` schema).

#### 429

Rate, concurrency or live-session limit reached. Retryable after `Retry-After`.

Headers: `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 500

Internal error. Retryable when `retryable` is true.

Headers: `X-Request-Id`.

`application/problem+json` (`Problem` schema).

#### 503

No proxy exit or engine capacity was available. Retryable after `Retry-After`; 0 credits.

Headers: `Retry-After`, `X-Request-Id`.

`application/problem+json` (`Problem` schema).

Full OpenAPI spec: https://docs.spicrawl.com/openapi.yaml
