Passa al contenuto principale

Cloud API Endpoints

This page is the inventory of every remote HTTP endpoint in the Playroll ecosystem — everything a client can call over the internet, as opposed to the localhost core API documented in Core HTTP Routes.

There are three hosting surfaces. Which one an endpoint lives on tells you how it is authenticated and who is allowed to call it:

SurfaceBase URLTypical caller
AWS API Gateway (Lambda)https://ld608uple1.execute-api.us-east-1.amazonaws.com/prodThe recording core and server-side operator tools
Supabase Edge Functionshttps://vtyolotvuvlbvqbgbzde.supabase.co/functions/v1/<name>The launcher UI and internal automation
CDN / telemetry domainshttps://releases.playroll.gg, https://otel.playroll.ggThe installer/updater and the telemetry pipeline

The contributor portal has its own Supabase project and function boundary — see Contributor portal — architecture; it is not repeated here.

AWS API Gateway (Lambda-backed)

All routes share the single prod stage and speak JSON. CORS preflight (OPTIONS) exists on every route and is not listed. Unless stated otherwise, auth is a Bearer Supabase user JWT in the Authorization header, and errors come back as {"error": "<message>", "code": "<STABLE_CODE>"}403 AUTH_FAILED for a bad token, 400 for a malformed request.

Upload routes enforce ownership structurally: every s3_key must start with the caller's <user_id>/ prefix, so a valid token can never touch another user's objects.

Method + pathAuthCaller and purpose
POST /auth/presigned-urluser JWTThe core requests a presigned S3 URL (PUT, GET, or HEAD) for one of its own artifacts.
POST /auth/session-starteduser JWTThe core registers a recording session server-side at start and receives the uploadability gate decision.
POST /auth/session-endeduser JWTThe core marks the session ended, reports duration/versions, and gets the final gate decision back.
POST /auth/multipart/createuser JWTStart a multipart upload for a large artifact.
POST /auth/multipart/part-urluser JWTPresigned URL for one part of a multipart upload.
POST /auth/multipart/completeuser JWTFinalize a multipart upload from the collected part ETags.
POST /auth/multipart/abortuser JWTAbandon a multipart upload and free the parts.
GET /sessions/{session_id}/feedbackuser JWTThe launcher fetches the processed session's accept/reject verdict with plain-English reasons, projected from the pipeline's eval certification. The session must belong to the calling user.
POST /admin/presigned-downloadX-Admin-SecretOperator-only, server-to-server. The CRM backend batch-requests presigned S3 GET URLs so an operator can download any user's recording artifacts (for example a whole sync group). The secret lives only on the CRM server and the Lambda — never in a browser.

Two more Lambdas are event-driven, not HTTP: the upload trigger (fires on S3 object creation) and the L0 pipeline trigger it invokes. They have no callable endpoint.

Contracts

POST /auth/presigned-url

Request:

{
"s3_key": "<user_id>/<game>/2026/07/02/rec-.../file.mp4",
"action": "upload",
"content_type": "video/mp4"
}

action is upload (PUT), download (GET) or head; default upload. content_type only applies to uploads.

Response 200:

{
"presigned_url": "https://playroll-captures.s3.amazonaws.com/...",
"user_id": "<uuid>",
"expires_in": 3600,
"s3_bucket": "playroll-captures",
"s3_region": "us-east-1"
}

Errors: 400 MISSING_S3_KEY | INVALID_S3_KEY | S3_KEY_TOO_LONG | INVALID_ACTION, 403 ACCESS_DENIED (key outside your prefix), 500 URL_GENERATION_FAILED.

POST /auth/session-started

Request (session_id required, the rest optional):

{
"session_id": "rec-1781813920001",
"game_name": "Baldur's Gate 3",
"game_slug": "baldurs-gate-3",
"lobby_id": "<uuid-or-null>",
"recording_started_at": "2026-07-02T09:00:00Z",
"core_version": "5.0.1",
"installer_version": "5.0.1",
"ui_version": "5.0.1"
}

Response 200 — the uploadability gate decision:

{
"success": true,
"session_id": "rec-1781813920001",
"uploadability_status": "uploadable",
"block_reason": null,
"required_artifact_mode": "mp4_csv_json"
}

Errors: 400 MISSING_SESSION_ID, 500 DB_ERROR.

POST /auth/session-ended

Request (session_id required, the rest optional — omitted fields are left untouched server-side):

{
"session_id": "rec-1781813920001",
"status": "ended",
"game_name": "Baldur's Gate 3",
"game_slug": "baldurs-gate-3",
"lobby_id": "<uuid-or-null>",
"recording_started_at": "2026-07-02T09:00:00Z",
"recording_ended_at": "2026-07-02T09:34:00Z",
"duration_seconds": 2040,
"uploadability_status": "uploadable",
"block_reason": null,
"transcript_expected": true,
"core_version": "5.0.1"
}

If status is omitted it is derived: failed when uploadability_status = "blocked", else ended. duration_seconds falls back to the recording timestamps when absent.

Response 200:

{
"success": true,
"session_id": "rec-1781813920001",
"uploadability_status": "uploadable",
"block_reason": null,
"duration_seconds": 2040,
"required_artifact_mode": "mp4_csv_json"
}

Errors: 400 MISSING_SESSION_ID, 500 DB_ERROR.

POST /auth/multipart/create · part-url · complete · abort

The standard S3 multipart dance, proxied so credentials stay server-side. All four take the same ownership-checked s3_key.

create — request {"s3_key": "...", "content_type": "video/mp4"}

{ "upload_id": "...", "s3_key": "...", "s3_bucket": "playroll-captures", "s3_region": "us-east-1" }

part-url — request {"s3_key": "...", "upload_id": "...", "part_number": 1} (part_number 1–10000) →

{ "presigned_url": "https://...", "part_number": 1, "expires_in": 3600 }

complete — request:

{
"s3_key": "...",
"upload_id": "...",
"parts": [ { "PartNumber": 1, "ETag": "\"abc...\"" } ]
}

{ "success": true, "s3_key": "...", "etag": "\"...\"" }

abort — request {"s3_key": "...", "upload_id": "..."}{ "success": true }

Errors: 400 MISSING_S3_KEY | MISSING_UPLOAD_ID | INVALID_PART_NUMBER | MISSING_PARTS, 403 ACCESS_DENIED, 500 MULTIPART_*_FAILED | PART_URL_FAILED.

GET /sessions/{session_id}/feedback

Request — no body:

curl "https://ld608uple1.execute-api.us-east-1.amazonaws.com/prod/sessions/<session_id>/feedback" \
-H "Authorization: Bearer <supabase-user-jwt>"

Response 200:

{
"verdict": "pass",
"headline": "...player-facing one-liner...",
"dataset_id": "rec-...::full-vlm::0746105a5fff",
"checks": [ { "criterion": "...", "status": "ok", "message": "..." } ],
"failed_reasons": [ "...plain-English reason per failed criterion..." ],
"summary": { "passed": 4, "failed": 0, "pending": 1 }
}

verdict is pass, fail, or pending (session exists but the pipeline has not produced its eval yet — pending is not a failure). Free-play sessions simply carry no quest criteria in checks.

Errors: 401 missing/invalid token, 404 session unknown or not owned by the caller — deliberately indistinguishable.

POST /admin/presigned-download

Request (server-to-server only; batch — a sync group has many files):

curl -X POST "https://ld608uple1.execute-api.us-east-1.amazonaws.com/prod/admin/presigned-download" \
-H "Content-Type: application/json" \
-H "X-Admin-Secret: <shared secret>" \
-d '{"s3_keys": ["<user>/<game>/.../file.mp4", "<user2>/<game>/.../file.mp4"]}'

Response 200 — per-key results, so one bad key does not fail the batch:

{
"urls": [
{ "s3_key": "...", "presigned_url": "https://...", "error": null },
{ "s3_key": "...", "presigned_url": null, "error": "not found" }
]
}

Errors: 403 wrong or missing secret, 400 missing/empty s3_keys.

Supabase Edge Functions

Base: https://vtyolotvuvlbvqbgbzde.supabase.co/functions/v1/<name>. All take POST with a JSON body unless noted. "User JWT" means a Supabase user token in the Authorization: Bearer header; "open" functions do their own auth internally (shared secret or provider callback) — open never means unauthenticated in practice.

FunctionAuthPurpose
steam-loginopen (OpenID flow)Steam sign-in for the launcher.
resolve-updateopenThe launcher asks which version it should be on — the read side of staged rollouts.
redeem-activation-codeuser JWTActivation-code redemption during onboarding (see Onboarding gate flow).
consents-status / consents-accept / consents-revokeuser JWTRead and update the user's consent state (see Consent versioning).
register-device-hw-snapshotuser JWTThe launcher registers the device hardware inventory (see Device HW inventory).
nvenc-contention-reportuser JWTThe core reports encoder-contention diagnostics.
report-buguser JWTIn-app bug reporting entry point (see Bug reporting).
submit-feedbackuser JWTPost-upload feedback popup: 1–5 stars on the active feedback dimensions plus an optional comment.
bug-report-to-linearopen (internal secret)Internal hop that files the bug in the tracker — not called by clients.
emit_notificationopen (admin secret)Operator tooling emits an in-app notification (see Notification delivery flow).
create-contributor-link-intentuser JWTCreates the link intent the contributor portal consumes (see Identity link).

Contracts (client-called functions)

resolve-update

Request — all fields optional; rollout selection stays server-side:

{
"installId": "<stable install identifier>",
"track": "stable",
"os": "windows",
"installed": { "...": "installed component versions" }
}

Absent/unknown os resolves as windows (fail-closed for legacy clients). Response tells the client which release (if any) to move to for its track and platform.

redeem-activation-code

Request:

{ "code": "<activation code>" }

Rate-limited per user, per IP, and per failed-attempt history. Success binds the code to the calling account; errors are deliberately generic.

consents-status · consents-accept · consents-revoke

Consent types: age_confirmation, tos, privacy, eula, edc_gameplay.

consents-status — no body; returns the caller's current consent state per type.

consents-accept — request (1–16 entries):

{
"acceptances": [
{ "consent_type": "tos" },
{ "consent_type": "age_confirmation", "country": "IT" }
]
}

country is ISO 3166-1 alpha-2 and matters for age_confirmation; when omitted it falls back to the user's stored country, and "no country anywhere" is rejected.

consents-revoke — request:

{ "consent_type": "edc_gameplay" }

Only withdrawable types are accepted. Repeat withdrawal returns { "withdrawn": false, "alreadyWithdrawn": true }. All three are rate-limited.

register-device-hw-snapshot

Request — the hardware inventory object as collected by the launcher (size-capped JSON object):

{ "hardware": { "cpu": "...", "gpu": "...", "ram_gb": 32, "...": "..." } }

Upserts the snapshot for the calling user's device; see Device HW inventory for the fields the inventory carries.

nvenc-contention-report

Request — up to 16 encoder sessions observed on the device:

{ "sessions": [ { "...": "per-process encoder session fields" } ] }

Each session is recorded through an advisory-locked RPC so concurrent reports of the same process stay consistent.

report-bug

Request:

{
"title": "Recording didn't start on first launch",
"description": "What went wrong, from the user",
"category": "recording",
"images": ["https://...storage-url..."]
}

title and description are both required. category is one of ui, recording, performance, onboarding, other — plus feedback once the EC-509 problem-vs-feedback split is deployed (general feedback rides the same pipeline and is labeled Improvement instead of Bug in the tracker). images is optional (max 3), holding URLs produced by the separate screenshot-upload path. The launcher's main process also attaches hw_config, electron_runtime, connection, and component versions to the body so the renderer cannot spoof them. Rate-limited per user per minute. The function resolves the caller's Steam identity server-side and files the report; the bug-report-to-linear hop then creates the tracker issue.

submit-feedback

The post-upload popup's write path. The dimension set is data-driven: the launcher reads the active dimensions (with labels and player-friendly descriptions) from the feedback_dimensions table (RLS allows authenticated reads of active rows) and renders one skippable 1–5 star row per dimension plus an optional comment.

Request — at least one rating or a comment required; ratings keys must be active dimension keys:

{
"session_id": "rec-1781813920001",
"ratings": { "usability": 4, "fun": 3 },
"comment": "Upload went smooth this time.",
"ui_version": "5.0.1",
"core_version": "5.0.1",
"event_id": null
}

Responses: 201 {ok, id}; 400 invalid payload or unknown/inactive dimension; 401 bad token; 409 feedback already submitted for this session (one per user per session — the launcher should not re-show the popup); 429 rate limited.

steam-login

Not a JSON API — a browser OpenID redirect flow. The launcher opens the function URL, Steam authenticates the user and redirects back with openid.* query parameters, and the function verifies the assertion (single-use nonce), provisions the Supabase user on first login, and returns the session to the launcher.

CDN and telemetry domains

DomainPurpose
releases.playroll.ggInstaller and update artifacts (/playroll/vX.Y.Z/*, with a latest rewrite). Consumed by the installer and the updater, not by application code.
otel.playroll.ggTelemetry ingest for the observability pipeline (see OTel pipeline). Writes are gated by an edge secret; not a client-facing API.

The operator CRM also exposes its own private REST API, but it is reachable only through the operators' access gateway and is an implementation detail of that tool, not a platform surface.