Download OpenAPI specification:
Session Replay provides an API for capturing bug reports with full context - replays, the sites and teams they belong to, team membership, webhook destinations and authentication - in JSON:API format.
Exchange credentials for a token pair, or exchange a refresh token for a fresh one. grant_type decides which: 'password' (the default) reads email and password, 'refresh_token' reads refresh_token.
| grant_type | string Enum: "password" "refresh_token" Defaults to password |
string Password grant only | |
| password | string Password grant only |
| name | string Label for the session being created |
| refresh_token | string Refresh token grant only |
{- "grant_type": "password",
- "email": "string",
- "password": "string",
- "name": "string",
- "refresh_token": "string"
}{- "data": {
- "id": "string",
- "type": "auth_token",
- "attributes": {
- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "scope": "string",
- "expires_in": 0,
- "refresh_expires_in": 0
}
}, - "included": [
- { }
]
}Second half of the browser authorization flow. A tool opens /authorize in a browser, is redirected back to its own loopback address with a code, and trades that code here for a token pair. Codes are single use and expire within minutes, must be redeemed against the same redirect_uri they were issued for, and must be presented with the code_verifier whose SHA-256 was sent as the code_challenge on the authorization request.
| code required | string The code the redirect carried back |
| redirect_uri required | string The loopback address the code was issued for |
| code_verifier required | string The secret behind the code_challenge sent to /authorize |
{- "code": "string",
- "redirect_uri": "string",
- "code_verifier": "string"
}{- "data": {
- "id": "string",
- "type": "auth_token",
- "attributes": {
- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "scope": "string",
- "expires_in": 0,
- "refresh_expires_in": 0
}
}, - "included": [
- { }
]
}Revokes the session the access token belongs to, so both of its tokens stop working.
| Authorization | string Bearer token |
{- "data": { },
- "meta": {
- "message": "string"
}
}Who the browser session belongs to. Authenticated by the site's own session cookie rather than a token, because this is what the extension calls once the user has signed in on the site. Passing extension_id hands any replay captured anonymously from that installation to the signed-in user.
| extension_id | string Installation id of the extension asking |
{- "data": {
- "id": "string",
- "type": "user",
- "attributes": {
- "email": "string",
- "first_name": "string",
- "last_name": "string",
- "full_name": "string",
- "initials": "string",
- "roles": [
- "string"
], - "referral_code": "string"
}
}, - "meta": {
- "claimed_replays": 0
}
}Every membership of every team the caller belongs to, newest first. A membership with no user and an invite_email is an invitation that has not been accepted yet.
| page[after] | string Cursor for pagination (after this cursor) |
| page[size] | integer Page size (max 100, default 25) |
| Authorization | string Bearer token |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "membership",
- "attributes": {
- "role": "owner",
- "invite_email": "string",
- "accepted_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}
], - "meta": {
- "page_size": 0,
- "has_next_page": true,
- "has_previous_page": true,
- "count": 0
}, - "links": {
- "self": "string",
- "next": "string",
- "prev": "string"
}
}| id required | string Membership id |
| include | string Include related resources (e.g., 'user,team') |
| Authorization | string Bearer token |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "membership",
- "attributes": {
- "role": "owner",
- "invite_email": "string",
- "accepted_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "included": [
- { }
]
}Team admins change roles; a member may update their own membership, which is how an invitation is accepted.
| id required | string Membership id |
| Authorization | string Bearer token |
object |
{- "data": {
- "type": "membership",
- "attributes": {
- "role": "owner"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "membership",
- "attributes": {
- "role": "owner",
- "invite_email": "string",
- "accepted_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}Team admins remove members; a member may remove their own membership to leave.
| id required | string Membership id |
| Authorization | string Bearer token |
{- "data": { },
- "meta": {
- "message": "string"
}
}What the reporter answered to the questions the domain's owner asks, posted after the report they belong to rather than with it - the panel asks them once the share link is already on screen. Two ways to be allowed: anonymously, with the replay id in the path AND the extension id the report was filed with, both matching the stored report; or signed in, as the report's own submitter. The share token is not a key here - it exists to be forwarded. A report filed through the integration library carries no extension id, so it can only be answered by a signed-in submitter. Posting again replaces the previous answers, so a retry is harmless. Every refusal of the poster is the same 404, whatever was wrong. The whole request may not exceed 64 KB; one that does, or one that declares no length, is refused with 413 and the error code 'answers_too_large'.
| id required | string The replay's id, as returned when it was created |
| extension_id | string The installation that filed the report. Required unless signed in as its submitter. |
required | Array of objects <= 2 items In the order the questions were asked. A question that was skipped is left out rather than sent empty - an entry with no values stores no row. Required: leaving it out is refused rather than read as an empty list, so a retry whose body was truncated cannot erase what was already stored. An explicit empty list does clear them, which is how an answer is taken back. |
{- "extension_id": "string",
- "answers": [
- {
- "site_question_id": "string",
- "question_text": "string",
- "value": [
- "string"
]
}
]
}{- "data": [
- {
- "id": "string",
- "type": "replay_question_answers",
- "attributes": {
- "site_question_id": "string",
- "question_text": "string",
- "value": [
- "string"
], - "position": 0
}
}
]
}Create a new replay. Supports three authentication methods: Site API Key (recommended), JWT token, or anonymous. Two types supported: 'screenshot' for quick snapshots with basic metadata, 'video' for full session recordings with extended metadata. The whole request may not exceed 600 MB. One that does, or one that declares no length, is refused with 413 and the error code 'replay_too_large' before anything is stored, and the accompanying detail is written to be shown to the person reporting the bug.
| X-Site-API-Key | string Site API key for authentication (get from Site Settings) |
| Authorization | string JWT Bearer token (alternative to API key) |
ZIP file containing metadata.json and recording file (RECOMMENDED - uploads directly to S3, can be reprocessed)
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "replay",
- "attributes": {
- "url": "string",
- "page_title": "string",
- "replay_type": "screenshot",
- "share_token": "string",
- "share_url": "string",
- "owner_key": "string",
- "zip_url": "string",
- "user_agent": "string",
- "browser_info": { },
- "screen_size": { },
- "share_expires_at": "2019-08-24T14:15:22Z",
- "share_view_count": 0,
- "resolution_status": "pending",
- "resolved_at": "2019-08-24T14:15:22Z",
- "expired": true,
- "network_data": [
- { }
], - "interactions_data": [
- { }
], - "console_logs": [
- { }
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": {
- "site": {
- "data": {
- "id": "string",
- "type": "site"
}
}, - "submitter": {
- "data": {
- "id": "string",
- "type": "user"
}
}
}
}, - "meta": {
- "message": "string"
}
}Get a paginated list of replays (requires authentication - returns user's own replays and team replays)
| page[after] | string Cursor for pagination (after this cursor) |
| page[before] | string Cursor for reverse pagination (before this cursor) |
| page[size] | integer Page size (max 100, default 25) |
| filter[replay_type] | string Enum: "screenshot" "video" Filter by type |
| filter[resolution_status] | string Enum: "pending" "in_progress" "resolved" "wont_fix" Filter by status |
| include | string Include related resources (e.g., 'site,submitter') |
| Authorization | string Bearer token |
{- "data": [
- {
- "id": "string",
- "type": "replay",
- "attributes": { }
}
], - "meta": {
- "page_size": 0,
- "has_next_page": true,
- "has_previous_page": true,
- "count": 0
}, - "links": {
- "self": "string",
- "next": "string",
- "prev": "string"
}
}Get a specific replay (requires authentication)
| id required | string Replay ID |
| include | string Include related resources (e.g., 'site,submitter') |
| Authorization | string Bearer token |
{- "data": {
- "id": "string",
- "type": "string",
- "attributes": { }
}
}Update a replay (requires team membership or ownership). An update may carry a recording, so it takes the same 600 MB ceiling the upload does and is refused the same way: 413 with the error code 'replay_too_large'.
| id required | string Replay ID |
| Authorization | string Bearer token |
required | object |
{- "data": {
- "type": "replay",
- "id": "string",
- "attributes": {
- "page_title": "string",
- "resolution_status": "pending"
}
}
}{- "errors": [
- {
- "status": "string",
- "code": "string",
- "title": "string",
- "detail": "string"
}
]
}Delete a replay (requires team membership or ownership)
| id required | string Replay ID |
| Authorization | string Bearer token |
{- "data": { },
- "meta": {
- "message": "string"
}
}Return a single still from the replay's screen recording at the offset given, extracted per request and never stored. The image is JPEG, scaled down to the requested width and never scaled up past the recording's own size. Screenshot reports have no recording and are refused.
| id required | string Replay ID |
| offset_ms required | integer Moment in the recording, in milliseconds from its start - the same unit the replay's own duration and log offsets use |
| width | integer Widest the frame may be, in pixels (64-2560, default 1280). A narrower recording is returned at its own width. |
| Authorization | string Bearer token |
The sites of every team the caller owns or belongs to, newest first.
| page[after] | string Cursor for pagination (after this cursor) |
| page[size] | integer Page size (max 100, default 25) |
| q | string Free text search |
| Authorization | string Bearer token |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string",
- "tier": "free",
- "replay_retention_days": 0,
- "replays_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}
], - "meta": {
- "page_size": 0,
- "has_next_page": true,
- "has_previous_page": true,
- "count": 0
}, - "links": {
- "self": "string",
- "next": "string",
- "prev": "string"
}
}Claims a domain for the caller's own team. Claiming a domain is a paid feature, so a team with no plan, or one already at its domain limit, is refused.
| Authorization | string Bearer token |
required | object |
{- "data": {
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string",
- "tier": "free",
- "replay_retention_days": 0,
- "replays_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}| id required | string Site id |
| include | string Include related resources (e.g., 'team') |
| Authorization | string Bearer token |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string",
- "tier": "free",
- "replay_retention_days": 0,
- "replays_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "included": [
- { }
]
}Team admins only. Retention and tier are not settable here - both follow the team's plan.
| id required | string Site id |
| Authorization | string Bearer token |
object |
{- "data": {
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "site",
- "attributes": {
- "name": "string",
- "domain": "string",
- "tier": "free",
- "replay_retention_days": 0,
- "replays_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}The teams the caller owns or is a member of, newest first.
| page[after] | string Cursor for pagination (after this cursor) |
| page[size] | integer Page size (max 100, default 25) |
| Authorization | string Bearer token |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "team",
- "attributes": {
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}
], - "meta": {
- "page_size": 0,
- "has_next_page": true,
- "has_previous_page": true,
- "count": 0
}, - "links": {
- "self": "string",
- "next": "string",
- "prev": "string"
}
}The caller becomes the team's owner.
| Authorization | string Bearer token |
required | object |
{- "data": {
- "type": "team",
- "attributes": {
- "name": "string"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "team",
- "attributes": {
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}| id required | string Team id |
| include | string Include related resources (e.g., 'owner,sites') |
| Authorization | string Bearer token |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "team",
- "attributes": {
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "included": [
- { }
]
}Team admins only.
| id required | string Team id |
| Authorization | string Bearer token |
object |
{- "data": {
- "type": "team",
- "attributes": {
- "name": "string"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "team",
- "attributes": {
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}Registers a URL of yours against a team you administer. Name the team with team_id, or leave it out and the destination is attached to your personal team. The response carries the signing key once, on create: every delivery is signed with it and there is no second chance to read it over the API.
| Authorization | string Bearer token |
required | object |
{- "data": {
- "type": "webhook_destination",
- "attributes": {
- "url": "string",
- "events": [
- "string"
], - "team_id": "810007d0-bec5-486c-b5d1-28fcd8a079ba"
}
}
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "webhook_destination",
- "attributes": {
- "team_id": "810007d0-bec5-486c-b5d1-28fcd8a079ba",
- "url": "string",
- "events": [
- "string"
], - "auto_send": true,
- "all_sites": true,
- "secret": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "relationships": { }
}, - "meta": {
- "message": "string"
}
}Removes a destination of a team you administer. A destination belonging to any other team answers as though it does not exist.
| id required | string Webhook destination id |
| Authorization | string Bearer token |
{- "data": { },
- "meta": {
- "message": "string"
}
}