Before you start
- An API key. Create one in Settings under API keys, the same keys the MCP server uses. Its user or organization must be able to edit the project.
- A project id. Every request names an existing project; the API never creates one.
- Credits. Turns are paid with Pascal credits, like messages to the AI assistant. See Credits.
Run a turn
The examples read your key and project id from two environment variables, and the TypeScript ones run withnpx tsx example.ts:
Each effort adds to the one below. The times are for a typical house; more input takes
longer. Whatever the request names is built at any effort:
architect-low asked for a
roof builds a roof. What the sources don’t show is inferred from context and listed in
the reply, with what is still open.
Every model also answers with a pascal- prefix: pascal-architect,
pascal-architect-low, pascal-architect-medium, pascal-architect-high and
pascal-architect-pro run the same model as their architect names. Use them when your
client pools several providers and needs model names that say whose they are.
GET /v1/models lists both names. The OpenAI SDK guide uses
the pascal-architect names; both work.
If your plan doesn’t include the effort you asked for, the request is refused with
403 upgrade_required. A request without enough credits gets
402 insufficient_quota.
Images and PDFs can go in the request as input_image and input_file parts, inline
or as file ids from POST /v1/files.
What a turn returns
- The reply is plain text, which can include Markdown. Rich cards that the editor’s chat draws, such as a room-area report, arrive as their text, never as raw JSON.
- The reasoning comes as
reasoningitems (reasoning_contentin Chat Completions): the model’s thinking and one line per step it took. - The version. A turn that changed the scene saves one version, named by
metadata.commit_message. The response reports it asmetadata.pascal_version,pascal_version_labelandpascal_version_url. A turn that changed nothing saves nothing. - Which Architect answered. Every response reports
metadata.pascal_architect_version, one number for all four models ("1.0"): a minor step (1.1) when the Architect’s prompts, tools or orchestration change, a major step (2.0) for a new generation. Beside it,metadata.pascal_releaseis the deployed commit the turn ran from. It is notpascal_version, which is your project’s saved version. - Questions. When the Architect needs an answer, the turn ends
completedwithmetadata.pascal_waiting: "question"and the question as the reply. Your next request on the project is the answer; sending just an option’s number picks that option.
The Architect sees its work
During an API turn, the Architect can render views of the scene withview_scene to
check what it built, as it does in the editor’s chat. This works even when you don’t
have the project open.
Run long turns in the background
A request withoutbackground follows its turn for up to 13 minutes. A turn still
running then is stopped, except an architect-pro turn, which keeps running and
answers status: "in_progress" for you to poll. Send work that may take longer, such
as building a house from a floor plan, with background: true. It works with every
model:
queued at once. The turn runs to its end with nobody connected:
credits are charged and the version is saved as for any other turn. Read it with
GET /v1/responses/{id} until it ends completed, incomplete, failed or
cancelled. Stop it with POST /v1/responses/{id}/cancel, which the OpenAI SDKs send
as client.responses.cancel(turn.id). Cancelling a turn that already ended returns it
as it ended. Only the person who started a turn can cancel it; cancelling anyone else’s
answers 404 turn_not_found, as for a turn that doesn’t exist.
From a terminal, the Pascal CLI 1.0.6 or
newer does the same with pascal architect run, status and cancel.
Retry safely with an idempotency key
A turn edits the scene and spends credits, so a retry after a dropped connection must not start a second one. Send anIdempotency-Key header with a value you generate, such
as a UUID:
Idempotent-Replayed: true. Nothing new
runs. Pascal keeps a key for 24 hours, for you and the project the request names. A key
sent again with a different request is refused with 409 idempotency_mismatch, so use
a new key for each new request.
See the Architect’s tool calls
By default a turn shows one reasoning line per step, not the tools behind it. To get each tool call with its arguments and result, as the editor’s chat lists them, ask for them withinclude (Responses only):
mcp_call output item with server_label: "pascal", in the order the
Architect made it: arguments and output are JSON strings, and a call that failed has
error and status: "failed". A background turn asked this way returns its calls when
you poll it; GET /v1/responses/{id}?include=pascal.tool_calls returns them for any
turn you can read.
Cap the credits a turn spends
Setmetadata.pascal_max_credits to the most credits one turn may spend. Like every
metadata value, it’s a string:
providerOptions.openai.metadata. Without a cap there
is no limit. A value that isn’t a number above 0 is refused with a 400 naming
metadata.pascal_max_credits, so a turn never runs uncapped by mistake.
When the turn reaches the cap, it stops starting new work and ends with
status: "incomplete" and incomplete_details.reason: "max_credits" (Chat
Completions: finish_reason: "length"). The work it finished is kept and saved as a
version, and the reply ends with a short note saying where it stopped. On Architect Pro,
a turn never pays more than its cap: work under way when it reaches the cap is charged
up to the cap. At other levels, model calls are billed as they finish, and the turn
checks what it has spent before each one, retries included. It can end above the cap
by the call during which it crossed it, plus any tool compute time not yet billed at
that point.
Studio renders (generate_studio_media) and online research (research_web) count
toward the cap, net of refunds. Site data (pull_site_data) doesn’t: it is charged
separately, up to its own maxCredits; see Hosted services.
Collect a Matterport tour
Ask for a public Matterport tour in the input, and run it in the background, since collection usually takes about 10 minutes:Allow (this tour, this turn), Always allow (every tour in this project, for you, in
chat and API turns) or Cancel. Collection then saves files to a
Matterport MODEL_ID folder in the project’s Files,
without changing the scene. The reply lists what was saved and what is missing.
A Zillow, Redfin, Realtor.com or Trulia listing link works the same way: the first turn
asks permission, then the listing’s photos, floor plans and facts land in a folder named
after the property (Files).
Publish and download the 3D file
A turn’s version is saved, not published. To get the project’s 3D file, publish it with the same API key, then download the GLB once it has baked:publishedVersion, didSaveVersion and didPublishVersion.
The GLB route answers:
Every answer except the
404 carries X-Pascal-Version, the published version it
refers to.
Errors
Don’t retry a failed turn automatically: it may already have edited the scene. Every error carriesx-should-retry: false, which the OpenAI SDKs obey, except a rate limit
(429), which nothing ran for. The AI SDK retries by status code, so pass
maxRetries: 0 with its OpenAI providers. A non-streamed Responses turn that fails
after it started answers 200 with status: "failed" and an error object.
A refused request answers with an HTTP status and an error object in the OpenAI shape,
with a link to this section:
type is the class of error, code is the specific reason, and param names the
request field or header at fault (or null). Branch on code. For handling errors in
each SDK, see the OpenAI SDK guide.
A turn id you can’t read answers
404 turn_not_found, the same as one that doesn’t
exist.
A streamed architect-pro turn still running after 13 minutes ends its stream with an
error event, code turn_still_running. Poll GET /v1/responses/{id} for its result.
Request ids
Every answer carries aRequest-Id header. Include it when you contact Pascal about a
request. A turn’s reply also carries x-request-id, the same value as its response id;
every other answer, each error included, carries the Request-Id value there. The
OpenAI SDKs expose x-request-id as response._request_id, and as error.request_id
on an error; see Get the request id for
support.
Read a published scene
GET https://editor.pascal.app/api/projects/{id}/scene?revision=published
returns the published viewer contract. Omit revision for the same result with
account credentials or a published grant; a saved grant defaults to its own version.
revision=saved:<n> reads one saved version (see
Read a saved version). Any other value returns
400 { "error": "unsupported_revision" }.
Public projects accept anonymous reads. Private projects require a Pascal session
cookie or Authorization: Bearer sk_live_… (or sk_test_…) with view access, or a
project-scoped viewer grant (see below). A
presented invalid credential returns 401, even for public projects. OAuth tokens
are not supported here yet. Missing and inaccessible projects both return
404 { "error": "not_found" }; a project without a live published version returns
404 { "error": "not_published" }. With account credentials and no saved revision,
editors receive the published version too.
The response has this shape:
graph removes unreachable nodes, hidden subtrees, annotations, node metadata and
provenance, site location/legal data, item asset ownership fields, collections,
and unused materials and plugins. Scans and guides follow the project’s public
visibility settings. Names, camera bookmarks and zone polygons remain. Bake version
3 and later may be served. Bakes are withheld for scenes with hidden sites because
the legacy exporter preserves their descendants; the graph projection removes them.
Every response uses Cache-Control: private, no-store. Each bake URL is signed for
10 minutes when minted. URLs can be reused across reads; use each LOD’s expiresAt
for its actual expiry, and fetch this endpoint again to renew expired URLs.
bytes and triangles may be null. When no bake is served, bakeVersion and
hash are null and lods is empty.
Anonymous public reads allow any origin (Access-Control-Allow-Origin: *). Reads
with account credentials accept same-origin, loopback and origins configured by Pascal’s
PASCAL_SCENE_API_ORIGINS allowlist, and vary on Origin, Authorization and
Cookie. Keep API keys on your server. OPTIONS preflight needs no bearer, allows
GET with Authorization, Content-Type, and is cached for 600 seconds. An origin
outside the credentialed allowlist can still preflight and read anonymously.
Read a saved version
GET https://editor.pascal.app/api/projects/{id}/scene?revision=saved:12 returns
version 12 as it was saved, in the same shape, with revision.kind: "saved". It needs
a Pascal session or an API key with edit access to the project, or a viewer grant made
for that version (see View a private project). Anonymous
reads, view-only access, unknown versions and the unsaved working draft all return
404 { "error": "not_found" }.
bake describes that version’s own 3D file, never the published one:
ready:lodsholds its signed URLs.none: no compatible, fresh file is bound to that saved version. Rendergraph.
ready or none; project-wide bake jobs do not describe a
saved version. A version that was once published can reuse its own fresh, model-bound
artifact. Files without a model identity and files for other versions are never used.
View a private project
Workspace owners and admins, or the personal project owner, can create share links withmanage access. A share link looks like
https://editor.pascal.app/s/pvg_…: anyone with the link can open the hosted viewer.
It sets a secure, HTTP-only viewer cookie for that project and redirects to
/viewer/{id}. The viewer and /preview/{id} show only the published revision,
including when ?draft=1 is present. A link made for one saved version (Share this
version in History) opens /viewer/{id}?version=<n> and shows only that version,
also on public projects. Links can have a label and optional expiry;
the complete URL is returned only when created. Revoking the link or transferring
the project to another workspace or owner ends its access.
For an integration, apply your own login on your server, then mint a short-lived
delegated grant using an API key with manage access:
201 { "token": "pvd_…", "expiresAt": "…" }. origins must
contain exact HTTPS origins, or http://localhost:<port> for development, with
no paths or trailing slash. ttlSeconds defaults to 3600 and must be an integer
from 1 to 3600; expiry is also capped by the API key’s expiry. externalSubject
is an optional opaque audit string of at most 200 characters, attributed to the
issuing integration key; it grants no Pascal identity or permissions. Session
cookies, unclassified keys and unclaimed agents cannot mint grants. Organization
keys stay within their workspace. Claimed agent keys may mint with manage access.
Issuance and revocation share a per-key limit of 60 requests per minute per server
instance; this is not a global quota.
To show one saved version instead of the published one, add revision:
revision is "published" (the default) or "saved:<n>", a version number from
History. An unknown version returns 404 { "error": "not_found" }. The grant stays on
that version when the project is published again, and stops working if the version is
deleted.
Pass the returned token to your browser and read the scene:
pvg_… share tokens can also read this endpoint as Bearer credentials. Share
reads echo any requesting origin; delegated reads require an origin in the grant’s
allowlist. Requests without an Origin header are allowed for server-to-server
reads. Origin checks restrict browsers; they cannot prevent a server holding a
token from replaying it. Invalid, expired or revoked grants return 401, with
no anonymous or session fallback. Every grant names exactly one project and either
its published revision or one saved version. Draft grants and OAuth projects access
remain unavailable.
A reader needs no change for a saved-version grant: without revision, scene and
artifact reads use the grant’s own version. The scene returns revision.kind: "saved". Sending
revision=saved:<n> for that same version also works. revision=published, another
version, or a saved revision on a published grant returns 401.
For private browser handoff, see Open a project in your viewer.
Delegated reads recheck the issuer key’s revocation, expiry, subject ban, agent
state, workspace membership and manage authority each time. Moving the project
to another workspace invalidates the grant. To revoke one, use the same API-key
management authentication with
DELETE /api/projects/{id}/viewer-grants/{grantId}; workspace managers can revoke
grants for that project. The mint response
includes a Location header naming this revocation URL; keep it on your server.
Revoking stops new access. Existing download links may work for up to 10 minutes. Content already downloaded cannot be recalled.
Open a project in your viewer
Register a self-hosted viewer with a display name, workspace or personal owner, entry URL, exact origins and callbacks. Pascal generates each new viewer’s name, such asmisty-cove-4tg7. The name is permanent and can’t be chosen or changed; names chosen before
Pascal generated them keep working. Workspace owners and admins manage registrations;
personal registrations belong to their owner. A project manager can bind any active,
verified viewer, including another workspace’s viewer. Binding trusts that publisher’s
future deployments: the viewer receives readable project data and can retain it.
Production destinations use canonical HTTPS URLs, without wildcards, userinfo, fragments
or trailing-dot hosts. Verify control before private handoff: request a verification
challenge, serve its plain text at /.well-known/pascal-viewer-verification on the exact
origin, then ask Pascal to verify it within ten minutes. Pascal follows no redirects and
refuses private network addresses. Development uses exact http://localhost:<port>
origins with a nondefault explicit port; it is never fetched for verification. Production
origins and viewer names stay reserved after retirement. Pascal-hosted verification awaits the
deployment control plane.
Resolve your public registration with
GET https://editor.pascal.app/api/viewers/resolve?origin=https%3A%2F%2Fviewer.example:
404 { "availability": "unavailable" }.
This projection contains no ownership or verification secrets.
A same-origin, session-authenticated POST /api/projects/{id}/viewer-open returns a
303 to the registered entry URL with public project, viewer and
access=session|anonymous parameters. Send an Origin header matching Pascal; accept
no destination URL from the user. An unpublished project returns not_published.
A new external share link pins its viewer and security version. Opening /s/pvg_…
sets Pascal’s host-only, HTTP-only, Secure, SameSite=Lax cookie, then redirects to the
pinned entry with access=share for private projects. A saved-version link opens
Pascal’s /viewer/{id}?version=<n> and keeps its credential even on public projects.
The durable token never crosses
to the viewer. Changing the project’s binding does not retarget that link. Disabled,
suspended, retired or changed pinned destinations make it unavailable. Legacy unbound
links continue to Pascal’s viewer.
For private entry, generate independent 32-byte random state and PKCE verifier values.
Keep the transaction in this tab’s sessionStorage, including the expected issuer
https://editor.pascal.app, viewer, project, callback, access mode, creation time and
validated local return path. Navigate the top-level tab to:
session or share mode. A missing, expired or mismatched requested
principal is refused, even if the other principal is valid. Pascal sends a single-use
pvc_ code, unchanged state and fixed iss to the exact registered callback. Codes
expire within 60 seconds, bounded by the parent. Invalid callbacks fail locally.
Before analytics or scene loading, capture and immediately scrub callback parameters
with history.replaceState. Check state, issuer, transaction age and expected context;
an unsolicited or mismatched callback must stop. Exchange with cookies omitted:
Origin. Successful exchange returns:
sessionStorage, keyed
by issuer, viewer, project and browser origin. Never put credentials in query strings,
localStorage or a deployment bundle. Send it as Bearer to the scene and artifact GET
routes with credentials: "omit"; these reads require the exact registered Origin.
A child authorizes only that project’s selected scene projection and its reachable
render artifacts. It grants no drafts, other saved versions, scripts, editing, uploads,
MCP tools or spending. It expires within an hour and no later than its parent.
A share handoff from a saved-version link returns "revision": "saved:<n>", and its
child reads that version instead of the published one, by the same rules as a
saved-version grant. Such a link needs its credential even on a public project.
session handoff, including Sign in with Pascal, always returns "published".
Renew near expiry with fresh state and verifier in a new top-level authorize trip,
using the original mode. Session deletion, account ban, membership loss, parent
revocation, project transfer, session binding changes or viewer suspension stop new
reads. Destination changes invalidate existing handoffs and links through the viewer’s
security version; explicitly renew the link. A failed renewal should offer “Reopen from
Pascal” or “Reopen your share link”, without a loop or alternate-principal fallback.
Handoff follows the most recently opened share link, so an older handoff tab may need
its original link again. Hosted viewing keeps independent cookie slots for the published
version and each saved version; opening one does not replace the others.
Public published projects use anonymous reads with cookies omitted and skip issuance.
An invalid presented credential or malformed callback still fails; public access never
rescues a failed authentication attempt. All handoff responses are private/no-store
and no-referrer. Already downloaded content remains, and signed storage URLs may work
for up to ten minutes after revocation. Never forward Pascal credentials to storage.
Let visitors sign in with Pascal
To offer “Sign in with Pascal” in your viewer, addinteraction=sign_in to a
session authorize request. Any other access value with it is rejected.
- A visitor who isn’t signed in sees Pascal’s sign-in page, then continues this request. Sign-in must finish within ten minutes.
- If the account can open the project in your viewer, Pascal redirects to your callback
with
code, unchangedstateandiss, without asking. Exchange it as above and keep the resulting pass in this tab; it lasts up to one hour. - Otherwise Pascal shows one page naming the signed-in account, with Use another
account (signs out of Pascal and returns to sign-in) and Return to your viewer.
Returning sends
error=access_deniedwith unchangedstateandiss, and no code.
error callback exactly as you check code (state, issuer, transaction), then
stop without exchanging and offer a fresh start. Pascal-side failures stay on Pascal.
One-time codes and Google sign-in return in the same tab. A magic link returns in the tab
that opens it, which may not have your sessionStorage transaction, and some sign-in
errors end on Pascal. When the transaction is missing or expired, offer one click to
start again; don’t loop or try to recover across tabs. Viewers that don’t send
interaction behave exactly as before. The
viewer templates implement this flow
and are the reference client.
Host a static viewer on Pascal
Register under Settings → Viewers → Host on Pascal. Pascal assigns the addresshttps://<name>.pascal-viewer.app/ from the viewer’s generated name, such as
https://misty-cove-4tg7.pascal-viewer.app/. Deploy a static build with the
Pascal CLI, in the next @pascal-app/cli release after 1.1.1:
--dry-run runs only the checks, without a key or network. Hidden files, folders and
symlinks are skipped, except a top-level .well-known/; any other symlink, and files with other
extensions, stop the deploy before anything uploads. The endpoints below are for writing your own
client.
These endpoints accept an API key with manage authority on the viewer’s owning workspace,
or a managing session (session POSTs require the same-origin Origin header). Organization keys
stay within their workspace; personal keys manage personal viewers. Responses are private/no-store.
Hosting returns 503 when the deployment storage credentials are not configured.
Creation body (SHA-256 is lowercase hex):
{deploymentId,expiresIn:900,uploads:[{path,url,headers}]}. PUT each file’s exact
bytes to its URL with every returned header. URLs bind Content-Type, Content-Length,
x-amz-checksum-sha256 (base64) and If-None-Match: *; already uploaded paths cannot be overwritten.
Browsers set Content-Length automatically. Do not send a Pascal API key or cookies to the upload URL.
After all uploads finish, POST finalize with no body. Finalize/activate return
{deploymentId,status:"active",url}, where url is the live address,
https://<name>.pascal-viewer.app/. Already-active activation is idempotent.
Limits: 2,000 files, 100 MiB total, 25 MiB per file, 30 creates/viewer/hour per server instance,
and a root index.html. Canonical relative paths only, no traversal, backslashes, percent escapes
or dotfiles except .well-known/. Extension/MIME pairs are restricted to static web, font, audio,
video and 3D assets: html, js, mjs, css, json, wasm, map, svg, png, jpg, jpeg, webp, avif, gif, ico,
txt, woff, woff2, ttf, glb, gltf, bin, ktx2, hdr, mp3, ogg, wav, mp4, webm.
Invalid manifests return 400, unauthorized credentials 401, cross-origin session writes 403,
inaccessible resources 404, unavailable/invalid deployment states or upload mismatches 409,
oversized API bodies 413, rate limits 429 with Retry-After, and unavailable storage 503.
Suspension, disable and retirement refuse deployment and take down the site. New activation retains
the active deployment and nine newest others, with an upload-expiry grace period. Hosted callbacks
remain exact registered URLs on the assigned origin, defaulting to / and /pascal/callback.
Deployments do not change the viewer’s security version. The Worker serves SPA routes, validators,
immutable hashed assets and a fixed isolation CSP. See the repository’s
wiki/community/viewer-hosting.md for operational controls and the DB/R2 transaction boundary.