Errors

What pitch, the API and the room refuse, and what to do.

On this page

Every refusal you can meet building and running a pitch, where it comes from, and what to do about it: pitch validate's problems, what stops pitch deploy, the other commands' refusals, the platform API's refusals, and what the room shows a viewer when it will not let them in.

How to read an error

pitch prints a refusal on standard error and exits 1. A problem in pitch.json names where it is and the docs that explain it:

  pitch.json does not validate: 1 problem
    apps[0].device: Invalid option: expected one of "phone"|"tablet"|"laptop"|"screen"|"none"
      https://getroomi.com/docs/pitch-json#field-apps-device

A step of pitch deploy that fails starts FAIL, names the step, and says why. A refusal from the platform names the call and the status, then the platform's own words:

  POST /api/projects/:projectId/links answered 402: Free has no private viewer links: …

pitch validate

pitch validate checks pitch.json without building anything, and pitch deploy runs the same checks first. A problem stops a deploy; a note does not.

Problems with the file

Problem What to do
pitch.json: is not here. pitch init writes one Run pitch validate in the folder that holds pitch.json, or run pitch init.
pitch.json: is not valid JSON: … Fix the syntax at the position given. JSON has no comments and no trailing commas.
<field>: pitch does not know this field (did you mean "…"?); the platform would ignore it Rename the field to the one suggested, or remove it. The platform ignores a field it does not know, so a misspelt stroy is a deploy with no tour.
<field>: missing; it is required Add the field. pitch.json marks every required one.
kind: must be "container" or "static" kind is one of those two words.
<field>: Invalid option: expected one of … The field takes one of the values listed, such as a device.
<id>: lower-case letters, digits and hyphens, starting with a letter An app's, scenario's or step's id is at most 40 characters of a–z, 0–9 and -, and starts with a letter.
<path>: a path on the build's own origin, starting with / An app's path and health start with /.
<call>: a call such as "POST /api/demo/reset" A call is a method (GET, POST, PUT, PATCH or DELETE), one space, and a path on the demo's own origin.
pages[…]: a Markdown or HTML file A page ends in .md or .html.
<field>: Too small: … or Too big: … A value is shorter or longer, or a list has fewer or more items, than the field allows. The message gives the limit; every limit is in pitch.json.

Problems across fields

Problem What to do
apps: app id "…" is declared twice Give each app its own id.
apps: only one app can be the default Mark one app "default": true, or none: without it, the first app opens first.
controls.scenarios: scenario id "…" is declared twice Give each scenario its own id.
controls: a static build has no server to call; controls need kind: container Remove controls, or deploy the demo as a container.
story: step id "…" is declared twice Give each step its own id.
story[…].app: step "…" happens in app "…", which apps does not declare Use the id of an app in apps.
story[…].run: step "…" runs calls, and a static build has no server to call; run needs kind: container Remove the step's run. A static build's story shows and points; it cannot run anything.
story[…].path: step "…" opens …, and <output> has no … A static build's step opens a page the output does not have: neither the path itself nor the index.html inside it. Correct the path (a Next export's pages end in /), or build the page. pitch validate looks in the output as it is on disk; build again if the page is new.

Problems with what it names

Problem What to do
pages: <file>: is outside the project A page is inside the folder that holds pitch.json. Move it in.
pages: <file>: is not there Fix the path, relative to pitch.json, or add the file.
pages: <file>: is longer than a page can be (2000000 characters) Split it into two pages.
Dockerfile: is not here. A container build is built from a Dockerfile at the project root Add a Dockerfile at the root. Container demos is the build contract.
output: <folder> is outside the project A static build's output is inside the project.

Notes

A note is advice. It never stops a deploy, but each one describes something that is likely to go wrong later.

Note What it means
output: <folder>/index.html is not there yet The build has not run, or writes somewhere else. pitch deploy runs the build first, and stops if index.html is still missing.
story: there is none, so the room offers no guided tour Add a story: Writing the tour.
Dockerfile: has no FROM, so it builds nothing The Dockerfile needs a FROM.
Dockerfile: the last stage builds in …; … it needs WORKDIR /app The platform's runner unpacks a build into /app, so the last stage builds there. pitch deploy refuses it otherwise.
Dockerfile: the last stage has no CMD Say how the server starts with CMD. A Node image's own CMD starts a Node prompt, not your server.
Dockerfile: the CMD installs or fetches packages A demo's container has no internet. Install in the build, not at start.
Dockerfile: the last stage is Node … The platform runs every build on Node 24, and a native module built for another version may not load. Use a node:24 image.

pitch deploy

pitch deploy stops at the first thing that fails, and says which. Nothing has been sent unless the stop comes after the rehearsal's summary, What would go live, on a --commit.

Before the build

Stop What to do
pitch.json does not validate: … Fix each problem, as pitch validate lists them.
The project name "…" makes no usable slug; pass --project <slug>. Pass --project with a slug of at least three characters, or give pitch.json a longer name.
Not signed in. pitch login first. Run pitch login, or set PP_TOKEN. Only --commit needs it.

The secret scan

Stop What to do
FAIL secrets: gitleaks found secret(s) in the project: and each rule, file and line Remove the secret, then rotate it: it has been on disk in plain text. Values are redacted in the output. Your .gitleaksignore is not read, so a finding cannot be waved through.
FAIL secrets: gitleaks could not be started (…) or exited … A scan that did not run is not a clean scan. Install gitleaks (brew install gitleaks), or set PP_GITLEAKS to its path.

The scan runs three times: over the project, over a static build's output, and over a container's bundle with the environment it will start with. A secret baked into an image's ENV is found there.

A static build

Stop What to do
FAIL build: <pm> run build exited … and its last lines Your build script failed. Run it yourself and fix it.
FAIL build: output … is outside the project. Point output inside the project.
FAIL build: <output>/index.html is missing after the build. The build must write index.html into output. Check the folder your build writes.
FAIL package: tar exited … The output folder could not be packed. Check it is readable.

A container build

Stop What to do
FAIL build: a container build needs a Dockerfile at the project root. Add one.
FAIL build: Docker is not running (docker info failed). Start Docker and run again.
FAIL the build failed (docker build …) and its last lines Your Dockerfile does not build for linux/amd64. Run docker build --platform linux/amd64 . yourself.
FAIL the image's WORKDIR is … End the last stage with WORKDIR /app.
FAIL the image has no CMD, so nothing says how the build starts. Add a CMD to the last stage.
FAIL reading the image failed, opening the image failed or copying /app out of the image failed Docker could not inspect the image it just built. Run again; if it recurs, prune Docker's disk.
FAIL the bundle would not unpack for its scan The bundle copied out of /app is damaged. Run again.
FAIL building the runner failed or starting the runner failed Docker could not build or start the platform's runner. Check Docker has room and the ports are free.
FAIL the runner never opened its control port The runner did not start in time. PP_HEALTH_TIMEOUT_MS gives it longer.
FAIL the runner could not start the build: … and its last words Your server did not start the way the image says. The runner's last log lines say why.
FAIL <health> answered <status> through the runner The server started but its health path did not answer 200. Make it answer once the server is up.
FAIL story: N steps failed on the rehearsal's copy (…) A step's call was refused by your demo, or its page did not answer. The FAIL story line above it names the call and what the demo said, or the page (its page, GET …, answered 404). A viewer's tour would stop there too.
FAIL story: N pages the story opens are not in the build A static build's step opens a page the build did not write. Each is listed with the files looked for.

Publishing

Stop What to do
FAIL upload: PUT answered <status> The upload was refused. A 411 or 422 means the size sent did not match the size declared: run again.
<METHOD> <path> answered <status>: … The platform refused a call. Its reason is one of the API refusals.
… replied with a shape the contract does not allow The platform and your pitch disagree about the API. Update pitch.

Other commands

Command Refusal What to do
pitch login Sign-in could not start: … answered … The platform pitch is pointed at did not start a sign-in. pitch whoami says which it is.
pitch login Sign-in was denied in the browser. You declined the code. Run pitch login again.
pitch login The code expired before it was confirmed. Run pitch login again and confirm sooner.
pitch login the keychain refused the token macOS would not store it. Unlock your Keychain and try again.
pitch login, pitch logout PP_TOKEN is set, so the Keychain is not in use. Unset PP_TOKEN to sign in or out.
any the keychain could not be read macOS's security tool could not read the Keychain. Unlock it, or pass a token in PP_TOKEN.
pitch whoami The stored token is no longer valid. Run pitch login again.
pitch auth header This prints your token. … pass --print to see it here. It is meant for a program. Pipe it, or pass --print.
pitch init pitch.json already exists. --force replaces it; --print shows what it would write.
pitch init Not deployable yet: this repo is not single-origin. Make the apps one server under their own paths, as the advice it prints says, then run it again.
pitch init Could not tell, and there is nobody to ask: Answer each question with the flag it names.
pitch init --kind is container or static., --device …: no app "…" was found or the device is one of … Fix the flag.
pitch link Usage: pitch link <viewer name> … Name the viewer.
pitch link --expires takes a whole number of days, 1 to 365. Fix the number.
pitch link, pitch open, pitch pages No project "…" yet. pitch deploy --commit creates it. Deploy first, or name the project with --project.
pitch pages add … is not a page: a page is a .md or an .html file. Use a Markdown or HTML file.
pitch pages add … is not there. or is longer than a page can be Fix the path, or split the page.
pitch pages add The title "…" will not do: 1 to 80 characters. Give --title a shorter title.
pitch pages add … already has a page called "…". Nothing was changed. --replace replaces its content; --title adds this one under another name.
pitch pages remove … has no page <id>. pitch pages list shows the ids.
pitch pages push pitch.json lists no pages. Add pages to pitch.json, or use pitch pages add.
pitch logs, pitch rollback … is not built yet Make an earlier build live from the app.

API refusals

The platform answers a refusal with a status and a JSON body whose error says what, in words you can show a person. Most also carry a reason a program can act on:

{ "error": "refused: taken", "reason": "taken" }
Status error When
400 invalid The body does not fit the endpoint's schema. issues lists each field that does not.
400 unreadable: … An accent colour or a room theme a viewer could not read (reason: contrast). problems lists each pair that fails, with the ratio it has, the ratio it needs and a shade that would pass. See Your brand in the room.
401 sign in first No session and no valid token. Sign in, or send Authorization: Bearer.
402 what the plan allows A plan's limit (reason: limit). See Limits.
403 what your role cannot do You are in the workspace, and your role does not allow it (reason: permission, and action names what): only a team's owners manage its people, name, brand, plan and deletion. Ask one of them.
404 not found Nothing with that id in your workspace. Another workspace's id is the same 404, so an id never says whether it exists elsewhere.
409 refused: taken The name is taken.
411 length required An upload was sent without a Content-Length.
422 refused: <reason>, sometimes with a detail Refused on its merits. See Refusal reasons. A 422 with reason: billing is a billing action refused: paid plans cannot be bought yet, and billing is switched off.
423 why it is paused The project or workspace is paused (reason: paused). See Paused.
429 how many a day Too many reports of projects in a day (reason: rate).
500 internal error The platform's fault, not yours. Try again.
503 why it is unavailable A feature that is not switched on, such as suggested wording for a community card (reason: unavailable).

Refusal reasons

A 409 or 422 carries one of these as its reason. Through the connector, Claude hears them in the words MCP tools lists.

reason Refused because What to do
format A project's slug is not 3 to 40 lower-case letters, digits and single hyphens, starting and ending with a letter or digit. Pick another slug.
reserved The slug is one the platform keeps, such as app, api, docs or pitch. Pick another slug.
impersonation The slug contains a word that borrows someone else's trust, such as bank, login or a large company's name. Pick another slug.
taken The slug is another project's, or was deleted less than 30 days ago. Also a community handle already in use. Pick another.
banned The name is not available. Pick another.
size An upload is not the size its start declared, or larger than 512 MB. Upload again, declaring the size you send.
unfinished A build names an upload whose bytes never arrived; or a community card or build is needed first. Upload the artefact before registering the build; write the card, or deploy, first.
used The upload was already received, or already made a build. Start a new upload for each build.
kind The upload was started as one kind (static or container) and the manifest says the other. Start the upload with the manifest's kind.
archive The artefact is not one the platform will serve. The detail says why: see below. Deploy with pitch deploy, which packs it correctly.
order A new page order does not list every page of the project exactly once. Send the whole order.
confirm Deleting a project was not confirmed with its address. Type the project's address to delete it.
range Analytics were asked for days that are not days, end before they start, or span more than the limit. Ask for a range of whole days, oldest first.
file A logo, a cover or a card is not the type or the size it may be. Use a PNG, JPEG or WebP image within the size the detail gives.
spend Demo containers are over the month's spend limit set for the platform. Nothing you can do; containers start again when it is lifted.
revoked A link's address was asked for again, or a reissue, on a link that is revoked. Make the viewer a new link.
expired The same, on a link whose expiry has passed. Make the viewer a new link.
unrecoverable A link's address was asked for again, and the link was made before 1 October 2026, when only a hash of its key was kept. Reissue it (reissueLink, or Reissue link in the app): same viewer, new address.
team A team's rule: your own workspace has nobody else in it, a team always keeps an owner, every seat is taken, or a team's name is empty or too long. The detail says which. Do what the detail says: make or use a team, give someone else ownership first, or free a seat.
unlicensed You asked for another team while you own one that has no plan yet. Give that team a plan, or delete it, first.

A static build's archive is refused as archive when it holds a symlink or a hard link, a path that is absolute or climbs out with .., more than 5,000 files, a file over 32 MB, or more than 256 MB unpacked; or when it is not a gzipped tar at all. A container build is refused as archive when it is not a gzipped tar, or has no start command (deploy it with pitch deploy, which records one).

Limits

A 402 names the limit that was reached, the plan, and the plan that would allow more:

{
  "error": "Free allows 2 deploys in any 24 hours. Pro deploys without a limit — paid plans are coming soon.",
  "reason": "limit",
  "limit": "deploys",
  "plan": "free",
  "upgrade": "pro"
}
limit Reached when
projects Creating a project beyond the plan's number of projects.
deploys Registering a build beyond the plan's deploys in any 24 hours.
links Making a private link on a plan without them (Free), or beyond the plan's links per project. Revoked and expired links do not count.
viewerHours Making a link once this month's viewer-hours are used.
analytics Asking for a viewer's journey on a plan without analytics in full.
pipeline Anything in the pipeline (the board, a stage placed by hand, an outcome, a follow-up, notes) on a plan without it (Free), links made on a paid plan included.
containers A live app (container) demo on a plan that runs static demos only (Free): uploading or registering a container build, making an earlier one live again, or listing a project whose live build is one.

upgrade is null when no plan allows more. Paid plans cannot be bought yet. Plans and limits has every plan's numbers.

Paused

A 423 means nothing can be deployed, and no link made, until it is lifted. The room admits nobody meanwhile.

Why error What to do
Over the plan this project is paused: the workspace has more projects than its plan allows Move to a plan that allows more. Deleting a project does not resume the others: projects are fitted to the plan again only when the plan changes or Roomi restores the workspace.
Suspended this workspace is suspended by Roomi Contact support.
Taken down this project was taken down by Roomi Contact support.

In the room

What a viewer sees when the room will not show the demo. None of these says anything about your plan or your figures: those are your business, not the viewer's.

The viewer sees Because What to do
A plain Not found The link is wrong, revoked or expired; the project is paused by Roomi, deleted or unlisted; the workspace is on a plan without private links; or a one-time pass was spent. The room gives the same answer for all of them, so a link never says which. Check the link in the app. Make a new link if it was revoked or has expired.
"This room is not taking visits right now" Your workspace has used this month's viewer-hours. A viewer already in the room keeps what is open; no new visit starts. The app warns you before this. Wait for the month to turn, or move to a plan with more.
"This demo is full right now" The room is at the number of viewers it takes at once, or the project has as many container copies running as the plan allows. The viewer can try again in a few minutes. Each turn-away is counted in the app.
"This room is paused", with your note You paused the room. The link still works. Resume it from the project's Settings (Pausing a room).
"Are you …?" Every browser is asked once per session, the first to open the link included. Nothing: the viewer says who they are; someone else is added under the same link.
An email asked for The project requires an email to view, or the viewer is commenting or reacting for the first time. Nothing: nothing is sent to it.

A container demo's copy says where it is on the stage:

The viewer sees Because
"Starting the demo…" The copy is starting, which can take a little while.
"The demo is starting — high demand, one moment." The platform is busy; the room keeps trying.
"The demo is still in high demand. Try again in a moment." It kept trying and could not start one yet.
"The live demo is paused right now. The notes are still here." Demo containers are stopped for the platform. The room's pages still work.
"The demo paused while you were away." The copy slept after the viewer left it idle. "Resume the demo" starts it again.
"The demo could not start." The copy did not start or did not answer its health path. Check the build answers with pitch deploy.