DEVELOPER DOCUMENTATION
Hosted Instructions visual proof
Render the real employer Instructions workspace or designated worker reader into agent-readable visual evidence.
Hosted Instructions visual proof lets an authorized employer or agent inspect the actual OpenTrain Instructions UI without running a local browser. OpenTrain captures the real employer workspace or a real designated test-worker reader in Chromium and returns private PNG, ARIA, diagnostics, and provenance artifacts.
Base path: /api/public/v1/instructions/visualAgents should fetch the PNG and ARIA artifacts directly. Create an observe link only when an authorized human needs to inspect the same pinned render.
Workflow
Section titled “Workflow”- Read
capabilitiesto discover audiences, targets, viewports, retention, and worker-coverage rules. - Create a session pinned to the job’s current canonical Instructions tree.
- Select a page or stable node, or include it when creating the session.
- Render desktop, mobile, or both using the current state revision.
- Inspect the PNG and semantic evidence directly, or mint a human review link.
- Revoke the link or expire the session when review is complete.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
GET | /capabilities | Discover the hosted visual-proof contract. |
POST | /sessions | Create an employer or designated-worker session pinned to a job tree. |
GET | /sessions/{sessionId} | Read session state and live source drift. |
POST | /sessions/{sessionId}/manage | Select a page/node, expire the session, or refresh its renderer grant with an expected-state revision. |
POST | /sessions/{sessionId}/render | Capture desktop/mobile evidence through the real product renderer. |
GET | /sessions/{sessionId}/artifacts | List bounded private evidence records. |
GET | /artifacts/{artifactId} | Read one artifact with a fresh signed resource URL. |
POST/GET | /sessions/{sessionId}/links | Create or list observe-only links. |
POST | /sessions/{sessionId}/links/{linkId}/revoke | Revoke one link. |
Create and render
Section titled “Create and render”Employer session:
{ "jobId": "job_123", "audience": "EMPLOYER", "pageId": "page_456"}Worker session:
{ "jobId": "job_123", "audience": "WORKER", "workerContractId": "contract_test_789", "pageId": "page_456"}Worker proof is valid only through a real contract designated for testing. The
response carries typed workerCoverage; no synthetic worker is substituted.
Render with the session’s current revision:
{ "expectedStateRevision": 2, "viewports": ["desktop", "mobile"], "pageId": "page_456", "nodeId": "stable-section-id"}The response binds each artifact to the exact session, audience, source tree,
page/node, viewport, render revision, checksum, and byte size. It also reports
source drift against the live canonical Instructions. A successful render sets
browserRenderVerified: true.
collaborationSyncVerified is always false. The capture proves what the
renderer displayed at that revision; it cannot prove that an already-open
collaborative browser received a real-time update. To test whether an open
read-only view picks up a later change, use the
live-propagation probe.
Third-party embeds
Section titled “Third-party embeds”This section is about the proof image only. Instructions pages play YouTube and Loom videos as usual; the renderer does not load third-party frames, so the player area is blank in the PNG.
The renderer loads only OpenTrain’s own app and asset origins. It blocks
third-party embeds, such as YouTube (www.youtube-nocookie.com) and Loom
videos, by design. The embed area renders blank in the PNG, and the render
still reports browserRenderVerified: true. The blocked address appears only
in the diagnostics artifact, reduced to its origin and path. It is listed
under blockedRequests, and also under failedRequests, because the browser
reports a blocked request as failed.
The render result also counts the blocked embeds, with no address. When the
count was measured and at least one embed was blocked, manifest.warnings
carries a coded string for every caller:
THIRD_PARTY_EMBED_BLOCKED: 2 embedded third-party frame(s) were blocked by the renderer egress policy and render blank; redacted URLs are in the DIAGNOSTICS artifact.To get the count as a typed field, send this header on the render request:
X-OpenTrain-Features: instructions-render-warnings-v1The response then has a top-level renderWarnings list beside manifest:
{ "browserRenderVerified": true, "collaborationSyncVerified": false, "renderWarnings": [ { "code": "THIRD_PARTY_EMBED_BLOCKED", "count": 2 } ]}renderWarnings | Meaning |
|---|---|
| Absent | The count was not measured for this response. Never read it as “nothing blocked”. A response without the header never has the field. |
[] | Measured: the renderer blocked no third-party embed. |
[{ "code": "THIRD_PARTY_EMBED_BLOCKED", "count": 2 }] | Measured: 2 embedded third-party frames were blocked and render blank. |
codecomes from a closed list.THIRD_PARTY_EMBED_BLOCKEDis its only value today, andrenderWarningshas at most one entry.countis from 1 to 500. With more than one viewport, it is the highest count from a single viewport.- A warning never carries a URL, host, or query string. The redacted address stays in the diagnostics artifact.
- The header changes only the response. A retry with the same
Idempotency-Keyreturns the same saved render with or without the header, and it is not a key reuse. Render responses sendVary: X-OpenTrain-Features. - The manifest never has a
renderWarningsfield, so the inlinemanifestalways matches the stored manifest artifact.
Live-propagation probe
Section titled “Live-propagation probe”The probe checks whether a view that is already open shows a change that
another writer commits while it watches. Add syncProbe to the render request.
OpenTrain opens the session’s scoped read-only view once, records a baseline,
and then only reads that same view. The probe never writes, reloads the page,
or refreshes access. The CLI
and MCP pages show the
same probe on those surfaces.
{ "expectedStateRevision": 2, "viewports": ["desktop"], "syncProbe": { "timeoutMs": 45000, "expectedPageChecksum": "<64-hex page checksum>", "expectedTreeChecksum": "<64-hex tree checksum>" }}| Field | Rules |
|---|---|
syncProbe.timeoutMs | Required. Integer from 5000 to 45000: how long the probe watches the open view, in milliseconds. |
syncProbe.expectedPageChecksum | Optional. 64 lowercase hex characters: the page checksum from your writer’s receipt. |
syncProbe.expectedTreeChecksum | Optional. 64 lowercase hex characters: the page-tree checksum from your writer’s receipt. |
Before you call it:
- Select a page. The session must have a page selected, either at session
creation (
pageId) or withmanageselect, and that page must belong to the session’s pinned tree. Otherwise the render fails with409anddetails.reason: "SYNC_PAGE_REQUIRED"before the probe starts, and the idempotency key stays unused. The probe never picks a page for you. - Use the desktop viewport only. Omit
viewportsor send["desktop"]. Any other viewport list is rejected. - Start from a fresh session. The session’s pin is the baseline. If the
page or tree already differs from it when the probe opens the view, the
render fails with
409anddetails.reason: "SYNC_BASELINE_CHANGED". - Run the writer separately. The render call is synchronous and has no
“baseline ready” signal. Commit the change from another process while the
render is running. A change that lands before the baseline is recorded
returns
BASELINE_MISMATCH, not a pass. - Allow up to two minutes. The CLI and the local MCP server wait up to 130 seconds for a probe render; give your own HTTP client at least that long.
The expected checksums tie the result to your writer’s commit. Without them, a verified result proves that a change reached the open view, not which writer made it.
Results
Section titled “Results”A verified probe returns 200 with livePropagationVerified: true,
propagationSurface: "SCOPED_READ_ONLY_VIEW", and a syncReport (the
manifest carries the same report). Every other outcome returns 409 with
details.reason: "COLLABORATION_SYNC_NOT_VERIFIED". Its details include the
syncReport, the artifactIds of the saved evidence, and the sessionId.
{ "error": "The already-open view did not prove both body and tree propagation. Inspect the evidence; use a new session/key for a new experiment.", "code": "CONFLICT", "requestId": "<REQUEST_ID>", "details": { "reason": "COLLABORATION_SYNC_NOT_VERIFIED", "syncReport": { "status": "TIMEOUT", "baseline": { "pageId": "page_456", "pageChecksum": "<PAGE_CHECKSUM>", "treeChecksum": "<TREE_CHECKSUM>", "bodyRenderHash": "<BODY_RENDER_HASH>", "treeRenderHash": "<TREE_RENDER_HASH>", "jobRevision": 7, "folderRevision": 3 }, "observed": { "pageId": "page_456", "pageChecksum": "<PAGE_CHECKSUM>", "treeChecksum": "<TREE_CHECKSUM>", "bodyRenderHash": "<BODY_RENDER_HASH>", "treeRenderHash": "<TREE_RENDER_HASH>", "jobRevision": 7, "folderRevision": 3 }, "elapsedMs": 45020, "sameDocument": true, "authFailureObserved": false, "availabilityFailureObserved": false, "unavailableReadCount": 0, "firstFailure": null }, "artifactIds": ["<ARTIFACT_ID>"], "sessionId": "<SESSION_ID>" }}syncReport.status is one of these values:
| Status | Meaning |
|---|---|
VERIFIED | The open view showed the body and page-tree change, and a fresh authorized read of the canonical Instructions matched what it showed, including any expected checksums. This is the only success. |
CANONICAL_MISMATCH | The view changed, but the fresh canonical read, the expected checksums, or another check did not match. |
TIMEOUT | No complete, stable change arrived within timeoutMs. Images that never finish loading also end here. |
BASELINE_MISMATCH | The open view did not match the session’s pin when the probe started, or changed while the baseline was captured. |
AUTH_FAILURE | A request for Instructions data from the open view returned 401 or 403 during observation. |
AVAILABILITY_FAILURE | A request for Instructions data from the open view failed with another HTTP error or a network error. Cancelled requests are ignored. |
VIEW_NAVIGATED | The open view navigated away. |
VIEW_UNAVAILABLE | At least one read found the reader missing or hidden, or a fully loaded image broken. |
The schema also lists OBSERVED. That is an intermediate renderer state;
OpenTrain always resolves it to VERIFIED or CANONICAL_MISMATCH before it
responds.
To reach VERIFIED, the page body and the visible page tree must both change
in the same open document, and the job or folder revision must advance. The
probe then needs three identical reads 250 ms apart within the window.
Before it reports VERIFIED, OpenTrain re-checks the caller’s job access and,
for a worker session, the worker’s coverage. A worker who lost coverage gets
409 with details.reason: "WORKER_COVERAGE_GAP".
Other report fields:
baselineandobserved: the page ID, page and tree checksums, body and tree render hashes, and job and folder revisions. Either can benullwhen the probe did not get that far. The report carries digests and revisions, never page content.sameDocument: whether the observed view is the same open document as the baseline.authFailureObservedandavailabilityFailureObserved: separate flags. Authorization failures (401and403) never count as availability failures, and the reverse. Once set, a later successful retry in the page cannot clear them.unavailableReadCount: how many reads found the view unavailable.firstFailure: the route template and HTTP status of the first failed request, ornull. It never includes IDs or query strings.statusisnullfor a network error.elapsedMs: how long the probe ran.
What livePropagationVerified proves
Section titled “What livePropagationVerified proves”livePropagationVerified is true only for a VERIFIED probe. It means the
committed change appeared in a view that was already open, without a reload.
That view is the scoped read-only view the session renders, named by
propagationSurface: "SCOPED_READ_ONLY_VIEW".
collaborationSyncVerified stays false on every render, including a
verified probe. Scoped views are read-only snapshots with no collaboration
session, so the probe proves nothing about live collaborative editors.
One experiment per key
Section titled “One experiment per key”Each probe is a single experiment:
- A retry with the same
Idempotency-Keyreturns the saved result, including a failed one. It never starts another observer. - If an earlier attempt with that key was interrupted before its evidence was
complete, the render fails with
409anddetails.reason: "SYNC_PROBE_ALREADY_STARTED". Inspect the session’s artifacts instead. - For a new experiment, create a new session, which pins a new baseline, and use a new idempotency key.
The evidence set has separate before and after PNGs, an ARIA snapshot,
redacted diagnostics, and the manifest with syncReport. OpenTrain saves it
before it returns a failed result.
Authorization and safety
Section titled “Authorization and safety”- Reads and artifact access require
instructions:read. - Session control, rendering, and link mutations require
instructions:write, anIdempotency-Key, employer job access, and the Instructions visual-proof rollout for the actor. - State-changing operations require the current
expectedStateRevisionand fail closed on drift. - Sessions, artifacts, and links are organization-bound, expiring, and revocable.
- Visual proof writes only isolated preview evidence. It never changes an Instructions page or learner record.
- Retry a transient renderer
503with the same idempotency key.
@opentrain-ai/sdk 0.21.0 or later exposes
getInstructionsVisualCapabilities, createInstructionsVisualSession,
getInstructionsVisualSession, manageInstructionsVisualSession,
renderInstructionsVisual, artifact list/get, and link create/list/revoke.
Version 0.33.0 or later accepts syncProbe in renderInstructionsVisual.
Its default request timeout for a render is 120 seconds; pass a longer
timeoutMs for a probe:
const result = await client.renderInstructionsVisual( sessionId, { expectedStateRevision: 2, syncProbe: { timeoutMs: 45000 } }, { idempotencyKey: "sync-probe-v1", timeoutMs: 130_000 },);A probe that is not verified throws OpenTrainApiError with status 409.
Its details carry the reason, the syncReport, and the artifactIds.
Use the machine-readable OpenAPI document for complete request, response, and error schemas.