TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { Metadata } from "next";2import { CodeBlock } from "@/components/ui/code-block";3import { DocPage } from "@/components/docs/doc-page";4import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";5import { Callout } from "@/components/docs/callout";6import { Endpoint } from "@/components/docs/endpoint";7import { ParamTable } from "@/components/docs/param-table";8import { CodeTabs } from "@/components/docs/code-tabs";9import { ResponseExample } from "@/components/docs/response-example";10import { apiTabs, fetchTabs } from "@/components/docs/snippets";1112export const metadata: Metadata = {13 title: "Sessions",14 description: "Sticky sessions keep the same exit identity across requests: create, use, inspect and close them, with TTL, geography, idempotency and expiry rules.",15};1617const SESSION = {18 id: "sess_8f2k1m9d3p7q4r6s",19 label: "checkout-user-42",20 status: "active",21 network: "residential",22 country: "CA",23 region: "quebec",24 city: null,25 request_count: 0,26 last_used_at: null,27 expires_at: "2026-09-07T14:32:11.000Z",28 created_at: "2026-09-07T14:17:11.000Z",29};3031export default function SessionsPage() {32 return (33 <DocPage path="/docs/sessions" eyebrow="Core API" title="Sessions" description="A session pins a stable exit identity for a period of time so that a sequence of requests looks like one visitor. Use it for login flows, paginated listings and any site that ties state to the client IP." status="Stable">34 <H2>What a session preserves</H2>35 <Ul>36 <Li>37 <Strong>Exit identity.</Strong> All requests that reference the session are routed through the same network route with the same sticky key, so the target sees the same IP for the38 session's lifetime, as long as the network keeps that exit available.39 </Li>40 <Li>41 <Strong>Network class and geography.</Strong> The class chosen at creation and the country you asked for are applied to every request that omits them.42 </Li>43 <Li>44 <Strong>Cookies you pass.</Strong> Fetcha does not maintain a cookie jar. Read <Code>cookies</Code> from a response and send them back in the next request's <Code>cookies</Code>{" "}45 field. Because the IP is stable, the target accepts them as belonging to the same visitor.46 </Li>47 </Ul>48 <Callout variant="info" title="Retries inside a session">49 Normally each retry uses a fresh exit IP. When a request is pinned to a session, retries keep the session's identity so the target never sees the visitor jump between IPs. If the50 session's exit is being blocked, close the session and create a new one.51 </Callout>5253 <H2>Create a session</H2>54 <Endpoint method="POST" path="/v1/sessions" scope="sessions:write" status="Live" />55 <ParamTable56 rows={[57 { name: "country", type: "string", constraints: "exactly 2 chars", description: <>ISO 3166-1 alpha-2 country for the exit. See <A href="/docs/geolocation">Geolocation</A>.</> },58 { name: "region", type: "string", constraints: "≤ 64 chars", description: <>State or province hint.</> },59 { name: "city", type: "string", constraints: "≤ 128 chars", description: <>City hint.</> },60 { name: "network", type: '"auto" | "datacenter" | "residential" | "isp" | "mobile"', default: '"auto"', description: <>Network class. The concrete class picked by the engine is returned in <code>network</code>. Classes outside your plan or without a sticky-capable route return <code>NETWORK_UNAVAILABLE</code>.</> },61 { name: "ttl", type: "integer (seconds)", default: "600", constraints: "60–1,800", description: <>Lifetime of the session. It is fixed at creation and is <strong>not</strong> extended by use.</> },62 { name: "label", type: "string", constraints: "≤ 128 chars", description: <>Free-form label for your own bookkeeping, shown in the dashboard.</> },63 ]}64 />65 <P>66 The body may be empty (<Code>{"{}"}</Code>): you then get a 10-minute session on the best sticky-capable route with no geography constraint. The schema is strict; unknown fields fail67 with <Code>INVALID_REQUEST</Code>.68 </P>69 <CodeTabs tabs={apiTabs({ method: "POST", path: "/v1/sessions", body: { country: "CA", region: "QC", ttl: 900, label: "checkout-user-42" }, idempotencyKey: "checkout-user-42-2026-09-07" })} />70 <ResponseExample status={200} body={SESSION} />71 <ParamTable72 showDefault={false}73 rows={[74 { name: "id", type: "string", description: <>Session identifier (<code>sess_…</code>). Pass it as <code>session</code> in fetch requests.</> },75 { name: "label", type: "string | null", description: <>Your label.</> },76 { name: "status", type: '"active" | "expired" | "closed"', description: <><code>expired</code> is derived from <code>expires_at</code>; <code>closed</code> means you deleted it.</> },77 { name: "network", type: "string", description: <>Concrete class serving the session.</> },78 { name: "country / region / city", type: "string | null", description: <>Normalised geography (region and city as slugs).</> },79 { name: "request_count", type: "integer", description: <>Number of fetch requests that used the session.</> },80 { name: "last_used_at", type: "string | null", description: <>ISO 8601 timestamp of the last fetch, or <code>null</code>.</> },81 { name: "expires_at", type: "string", description: <>ISO 8601 expiry, <code>created_at + ttl</code>.</> },82 { name: "created_at", type: "string", description: <>ISO 8601 creation time.</> },83 ]}84 />8586 <H3>Idempotency-Key</H3>87 <P>88 Send an <Code>Idempotency-Key</Code> header with the create request to make it safe to retry. If Fetcha has already created a session for that key in your project within the last 2489 hours, it returns the existing session instead of creating a new one. Keys are scoped to the project; any string works, though a value derived from your own entity (user id, job id) is90 the most useful.91 </P>9293 <H2>Use a session in a fetch</H2>94 <P>95 Reference the id in the <Code>session</Code> field. If you omit <Code>country</Code>, the session's country applies; if <Code>network</Code> is <Code>auto</Code>, the session's class96 applies. <Code>metadata.session</Code> echoes the id.97 </P>98 <CodeTabs99 tabs={fetchTabs(100 { url: "https://www.example.ca/account", session: "sess_8f2k1m9d3p7q4r6s", cookies: { sid: "3f9a…" } },101 {102 javascript: `console.log(data.metadata.session, data.metadata.country); // "sess_8f2k1m9d3p7q4r6s" "CA"`,103 python: `print(data["metadata"]["session"], data["metadata"]["country"])`,104 },105 )}106 />107 <Table dense>108 <THead>109 <Tr>110 <Th>Condition</Th>111 <Th>Result</Th>112 </Tr>113 </THead>114 <TBody>115 <Tr>116 <Td>Id does not exist, or belongs to another project</Td>117 <Td mono>404 SESSION_NOT_FOUND</Td>118 </Tr>119 <Tr>120 <Td>121 Session is past <Code>expires_at</Code>, or was closed122 </Td>123 <Td mono>410 SESSION_EXPIRED</Td>124 </Tr>125 </TBody>126 </Table>127 <P>128 Both are raised before any network activity, so they cost nothing and do not consume an attempt. Pass the same geography as the session or omit it; requesting a different country on a129 pinned session is allowed but defeats the purpose.130 </P>131132 <H2>List sessions</H2>133 <Endpoint method="GET" path="/v1/sessions" scope={null} status="Live" />134 <P>Returns the 100 most recent sessions of the project, newest first, including expired and closed ones.</P>135 <CodeTabs tabs={apiTabs({ method: "GET", path: "/v1/sessions" }, ["curl", "javascript", "python"])} />136 <ResponseExample status={200} body={{ data: [SESSION] }} />137138 <H2>Get a session</H2>139 <Endpoint method="GET" path="/v1/sessions/:id" scope={null} status="Live" />140 <CodeTabs tabs={apiTabs({ method: "GET", path: "/v1/sessions/sess_8f2k1m9d3p7q4r6s" }, ["curl", "javascript", "python"])} />141 <P>142 Returns the same object as creation, with a live <Code>status</Code>. Unknown ids return <Code>404 SESSION_NOT_FOUND</Code>.143 </P>144145 <H2>Close a session</H2>146 <Endpoint method="DELETE" path="/v1/sessions/:id" scope="sessions:write" status="Live" />147 <CodeTabs tabs={apiTabs({ method: "DELETE", path: "/v1/sessions/sess_8f2k1m9d3p7q4r6s" }, ["curl", "javascript", "python"])} />148 <ResponseExample status={200} body={{ id: "sess_8f2k1m9d3p7q4r6s", status: "closed" }} />149 <P>150 Closing is immediate and irreversible; later fetches with the id return <Code>SESSION_EXPIRED</Code>. Sessions you do not close simply expire at <Code>expires_at</Code>. There is no per-plan151 limit on the number of sessions.152 </P>153154 <H2>Two-step login example</H2>155 <P>156 A typical flow: create a session, POST credentials, keep the returned cookies, then GET the protected page with the same session and cookies. The SDK version is on the{" "}157 <A href="/docs/examples">Examples</A> page.158 </P>159 <CodeBlock160 lang="bash"161 title="Terminal"162 code={`# 1. Create a Canadian session for 15 minutes163SESSION=$(curl -s https://www.fetcha.co/v1/sessions -X POST \\164 -H "Authorization: Bearer $FETCHA_API_KEY" -H "Content-Type: application/json" \\165 -d '{"country": "CA", "ttl": 900}' | jq -r .id)166167# 2. Log in (cookies come back in .cookies)168curl -s https://www.fetcha.co/v1/fetch -X POST \\169 -H "Authorization: Bearer $FETCHA_API_KEY" -H "Content-Type: application/json" \\170 -d "{\\"url\\": \\"https://www.example.ca/login\\", \\"method\\": \\"POST\\", \\"session\\": \\"$SESSION\\",171 \\"headers\\": {\\"Content-Type\\": \\"application/x-www-form-urlencoded\\"},172 \\"body\\": \\"user=me&pass=secret\\"}" | jq .cookies173174# 3. Fetch the account page with the same session and the returned cookies175curl -s https://www.fetcha.co/v1/fetch -X POST \\176 -H "Authorization: Bearer $FETCHA_API_KEY" -H "Content-Type: application/json" \\177 -d "{\\"url\\": \\"https://www.example.ca/account\\", \\"session\\": \\"$SESSION\\", \\"format\\": \\"text\\",178 \\"cookies\\": {\\"sid\\": \\"3f9a…\\"}}" | jq -r .text`}179 />180 </DocPage>181 );182}183