Skip to main content
The Pascal API lets your code ask the Architect, Pascal’s building model, to work on a project. It speaks the OpenAI Responses and Chat Completions formats, so the OpenAI SDKs and the AI SDK’s OpenAI providers call it the way they call a reasoning model. Each request is one turn on one project: the Architect edits the scene, answers in text, and saves what it changed as a named version of the project. The base URL is:
Every route, parameter and response is in the OpenAPI document. For Python, TypeScript and AI SDK examples side by side, see Use the OpenAI SDK. The API works on projects in a hosted Pascal account. For projects on your own machine, run the local editor and its MCP tools with the Pascal CLI.

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 with npx tsx example.ts:
With the AI SDK, pass the project in a header:
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 reasoning items (reasoning_content in 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 as metadata.pascal_version, pascal_version_label and pascal_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_release is the deployed commit the turn ran from. It is not pascal_version, which is your project’s saved version.
  • Questions. When the Architect needs an answer, the turn ends completed with metadata.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.
Each call joins the project’s chat thread, so the turn appears in the editor’s chat too.

The Architect sees its work

During an API turn, the Architect can render views of the scene with view_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 without background 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:
The request answers 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 an Idempotency-Key header with a value you generate, such as a UUID:
Send the same request with the same key again and you get back the turn the first one started, as it stands now, with the header 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 with include (Responses only):
Each call is an 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

Set metadata.pascal_max_credits to the most credits one turn may spend. Like every metadata value, it’s a string:
With the AI SDK, send it through 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:
The first turn ends with a question asking permission. Answer with 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:
Publishing does what the editor’s Save & publish does: it saves the draft as a version if it changed, publishes the latest version, and starts its bake. It needs permission to publish the project and leaves its privacy alone, so a private project stays private. The response gives 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 carries x-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 a Request-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: lods holds its signed URLs.
  • none: no compatible, fresh file is bound to that saved version. Render graph.
Saved reads return only 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 with manage 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:
The response is 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:
Never put an API key in the browser. Only pass the short-lived grant to it. 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 as misty-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:
Unavailable or ambiguous origins return 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:
Use the original 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:
The browser must send the callback’s exact Origin. Successful exchange returns:
There is no refresh token. Keep the child only in this tab’s 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, add interaction=sign_in to a session authorize request. Any other access value with it is rejected.
Pascal checks your registration and callback first; an invalid request stops on Pascal and never returns to your viewer. Then:
  • 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, unchanged state and iss, 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_denied with unchanged state and iss, and no code.
An account can open a project when it can view the project in Pascal and someone who manages the project has published it and linked it to your viewer. The project must be private: open public projects with anonymous reads instead. Pascal never says which condition failed, so a missing, unpublished or differently linked project looks the same. Check an 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 address https://<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:
The CLI checks the build against the limits below, uploads it, activates it and prints the live address. --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):
The response is {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.