Scan sessions from your backend
Contents
Scan requests let your own code ask Replay Vision about specific sessions. Use them when something outside PostHog decides which sessions matter: a support tool that wants a summary of what the customer did before they wrote in, a job that checks every session behind a failed payment, or an internal tool that scores sessions on request.
One request scans up to 200 sessions, with a saved scanner or a question you ask inline. You get back one request ID to poll, and can have a webhook or Slack message sent when it finishes.
To scan sessions every time an event happens, a workflow with a Replay Vision step needs no code at all.
Before you start
- Accept AI data processing. Your organization must allow AI analysis before any scan runs.
- Use the project's main environment. Scan requests aren't available in child environments.
- Create a key. Use a project secret API key with the
replay_scanner:writeandsession_recording:readscopes. A project secret API key belongs to the project rather than a person, so it keeps working when people leave. A personal API key with the same scopes works too, and scans with that person's access.
Start a request
Send the session IDs and either a saved scanner's scanner_id or an inline question:
Use eu.posthog.com if your project is in the EU.
| Field | Required | What it does |
|---|---|---|
session_ids | Yes | Up to 200 session recording IDs. Duplicates are ignored. |
scanner_id | One of these two | Scan with a saved scanner. Its filters and sampling don't apply: every session you send is scanned if it's eligible. |
inline | One of these two | Ask a question without saving a scanner. Takes prompt, plus optional scanner_type (monitor by default, or classifier, scorer, summarizer), scanner_config (tags for a classifier, scale for a scorer) and model. See scanner types. |
idempotency_key | No | Any unique string for this request. Sending the same key again returns the first request instead of starting new scans, so a retry after a timeout never charges twice. |
reference | No | Your own ID for the request, such as a ticket or job ID. It comes back unchanged. |
wait_for_session_end | No | Waits until each session has been quiet for 35 minutes before scanning it, so the scan sees the whole session. Use it when sessions may still be recording. Sessions still active after 6 hours are scanned as they are. Off by default. |
A new request answers 202 with the request. A repeated idempotency_key answers 200 with the original request. If the key belongs to a request made by another key or user, you get 409 and should send a new key.
Asking the same inline question about the same session again reuses the earlier answer at no charge. A saved scanner never scans a session it has already observed: you get its existing observation.
Read the results
Poll the request with its id:
The request's status is running until every session has settled, then completed. Each entry in sessions has:
| Field | What it holds |
|---|---|
session_id | The session. |
state | pending or running while in progress. Then succeeded, failed, ineligible, skipped (a limit stopped it before it started), or lost (the scan ran out of time). |
scan_outcome | How the scan started: started, already_running, already_scanned, skipped_limit, skipped_quota, skipped_scanner_limit, or failed. Null while the request waits for the session to end. |
observation_id | The observation, once one exists. |
scanner_result | The answer, once the session succeeds. model_output holds the fields for the scanner type, such as verdict and reasoning for a monitor. |
Scans usually take a few minutes. A session that hits a credit or concurrency limit comes back skipped rather than failing the whole request. See quota and limits for what each limit means.
Get notified when a request finishes
Instead of polling, send a webhook or Slack message when a request completes. PostHog emits a $replay_vision_request_completed event for each finished request, and Data pipelines can send it on. Create the destination through the Hog functions API as an internal_destination that filters on that event. For example, a webhook:
Set method, headers, and body yourself. Inputs you leave out don't get the template's defaults through the API, so the webhook would arrive without the event. The body above sends the event's properties, including the request_id.
This call needs a personal API key with the hog_function:write scope.
The event carries:
| Property | What it holds |
|---|---|
request_id | The request to fetch results for |
reference | Your reference from the request |
scanner_id | The scanner that ran, or the hidden one behind an inline question |
session_count | How many sessions the request had |
succeeded_count, failed_count, ineligible_count, skipped_count, lost_count | How many sessions ended in each state |
The event has counts only. Anyone who can add a destination can receive it, so session IDs and answers stay behind the request endpoint, which checks recording access. Fetch the request to read them.
A destination may occasionally receive the same request twice. Use request_id to ignore repeats.
Costs and limits
Each session costs the scanner model's credits, like any other scan. An inline question uses Gemini 3 Flash at 5 credits per session unless you pick another model.
You can start 60 requests a minute and 1,000 an hour. With a project secret API key, the same limits also apply across all keys in the project.
Over MCP
Agents can make the same requests with the vision-requests-create and vision-requests-get MCP tools.











