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-deviceA 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. |