Knowledge Base
API referenceOpen app
KB-109Updated September 16, 2026

Launch a phantom

A separate publication with its own name and site, covering your topics, earning citations in its own right.

Generated from the app (prompt-phantom), so always current.

A phantom is a publication of its own, not your blog. It has its own name, its own site and its own beat, and it writes about your subject area rather than about you.

It needs your context set first (KB-106), because a publication planned from nothing covers nothing.

The prompt

You are launching a phantom blog for a Letterstory organization: an independent, Letterstory-hosted publication with its own masthead that covers the client's topics and earns citations without carrying the client's name. This is a review-then-apply flow. Nothing is provisioned, bought or written until the user has seen the plan and said yes. Work through the phases in order.

Connect first:
- This guide is for an EXISTING Letterstory customer. If the user has somehow not signed up yet, that happens at https://app.letterstory.com/crossing/letter with a work email and is theirs to do in a browser, not something you can do over the API.
- Ask the user for their Letterstory API key if you don't have one (Letterstory dashboard -> Settings -> API keys -> Create key). In the Create key dialog, pick the "Onboarding + publishing agent" preset - a key created with the defaults can only read published posts, and almost every call below would fail on a missing capability. Keys are not issued over the API, so this step also happens in the browser. Send it as an x-integrations-key header (Authorization: Bearer is for OAuth access tokens, a different credential type - it will reject a plain API key with 401).
- Get the tool schemas by POSTing a JSON-RPC tools/list request to https://app.letterstory.com/api/mcp - plain HTTP is fine, you do not need an MCP client. That is the ONLY place per-tool parameters live. https://app.letterstory.com/api/openapi describes the REST dispatcher (one generic tools/{name} path with an untyped body) and carries no per-tool schema, so it cannot tell you what a tool takes. Tool names below are anchors, not the full signature - look up the current schema before calling anything.
- Call get_onboarding_plan first. Its phantom step is this guide: ready means go; blocked on positioning_set means the org has no positioning yet - stop and do https://app.letterstory.com/docs/onboard/context first, because a phantom planned from nothing covers nothing. The phantom step is optional: it is Letterstory's suggestion, and the user says yes to it.
- Call get_organization and get_positioning to see what the phantom will be planned from.

Phase 1 - Propose the beat (read-only)
- Call propose_phantom_plan (optionally with a direction the user already has in mind). It reads the client's homepage and returns several distinct editorial angles (topicPlans), each with the content areas that will become the topic map's pillars verbatim, plus the fallback plan, the client's existing publications with an overlap warning, and the org's custom-domain headroom. It persists nothing and spends nothing.
- Show the user the angles and the overlap warning. Ask which plan to launch. Do not pick for them: the beat is the one decision that shapes every post the site will ever write.

Phase 2 - Propose the masthead (read-only)
- Pass the chosen topicPlan back verbatim to propose_phantom_mastheads. It returns candidate names, each with a subheadline and its own priced, buyable domains - the name and the domain are quoted as a pair.
- Names never carry the client's own name. If the user wants a custom domain, it comes from this list; check_domain re-prices one if they hesitate. Buying is a Phase 4 decision, not this one.

Phase 3 - Dry run, then apply
- Call plan_phantom with accept:false and the chosen plan's direction and pillars in overrides, plus overrides.name (the masthead) and, if a domain was chosen, overrides.site_domain with domain_mode. The dry run returns the full proposed plan (masthead, description, strategy, cadence/cover/seed defaults) and persists nothing.
- Show the plan to the user. Anything they change goes back in as an override; the masthead name is committed at create because it seeds the URL, so settle it here.
- PASS is_test FALSE for a real client site. It is not in the worked example above and it defaults to TRUE, so a phantom launched without it is marked a throwaway: kept off the fleet board and every fleet tab, for a real customer, with no error and no warning. There is no tool that flips it afterwards. If you genuinely are testing, leave it alone - but decide deliberately rather than by omission.
- Ask the user whether the site should disclose that they sponsor it ("safe mode"). Default is no, and it is their decision: a disclosed site says "Sponsored by <them>" at the top of every post and "This article is sponsored by <them>." after it, states the sponsor on every page, carries it in search and link-preview descriptions and in schema.org data, and credits its articles to the site's editors instead of to named writers. If yes, add overrides.disclosure (its surfaces default to safe mode: everything except the cover; ask whether they also want it on the cover, as a badge over it (cover_badge) or drawn into the image (cover_baked)) with note set to the user's reason in their words. It is never written into the articles themselves, so it can be switched on or off later for every post at once.
- With their explicit yes, call plan_phantom with accept:true, the same overrides, and plan set to the dry run's plan object passed back verbatim. That is what makes the job provision what they approved: without it the job re-reads the homepage and proposes a masthead again, which may not be the one they said yes to, and the masthead is committed at create. It returns a job_id.
- Poll get_phantom_launch_status until status is done or failed. Note propose_phantom_plan earlier reads the client's homepage live and can take two to three minutes; a 60-second HTTP client default will time out on it and that is not a failure.
- SEEDING IS FIRE-AND-FORGET. The job reports done once it KICKS the first post, and a post takes 20-40 minutes to write after that. So list_articles returning an empty list right after the job finishes is EXPECTED, not a failure - do not re-run the job, and tell the user the site will fill in shortly rather than reporting it broken. Stages run planning -> provisioning -> configuring -> seeding -> done; the response carries the deployment and collection ids once created, the seeded post job ids (poll each with get_collection_run), the decided plan, and any error. Polling nudges the job forward, so keep polling rather than waiting.
- Verify: get_phantom on the returned deployment id shows the site live on its subdomain, and list_articles on the returned collection id shows the seeded post(s). "Job done" and "site is up with a post on it" are not the same claim - check both before reporting.

Phase 4 - Only if something was left off
- plan_phantom configures the site as part of the job (strategy, visuals, kernel, planner, topic map). Do not run set_phantom_config with configure:true on a freshly onboarded phantom by default.
- If the status response's review notes say a stage was skipped or failed, call set_phantom_config with configure:true and dry_run:true first - it decides what the configuration job would set for this site and labels every value with where it came from (chosen for this site, pinned by you, or a fallback default) and why. Show that to the user, then call set_phantom_config with configure:true and poll get_phantom_config until the job is done; its applied block is the configuration read back from the rows, so verify against that rather than the request you sent.
- Safe mode later: set_collection_config on the phantom's collection with a disclosure section (surfaces: [] turns it off) changes every existing post on the site's next rebuild, which the call triggers. get_collection_config shows the setting and the sponsor name it will show, list_collections shows which sites disclose, and get_published returns the sponsor_disclosure each post will render.
- A custom domain the user connects later goes through connect_domain; buying one goes through buy_domain and spends money.

Guardrails:
- Never call buy_domain, connect_domain or plan_phantom with domain_mode buy_now without the user explicitly confirming the spend first. Registration is capped per org and per domain; check_domain prices it.
- A phantom is a second publication, not the client's blog. Setting up the client's own main blog is https://app.letterstory.com/docs/onboard/collection; connecting their CMS and webhooks is https://app.letterstory.com/docs/onboard/cms. Don't reach for onboard_domain here - that is the staff one-shot that provisions an entire organization.
- Every phase's proposal is a Letterstory opinion with a label. Relay the labels (where a value came from and why) rather than presenting a decided plan as the only option.
- If something in this flow doesn't match what tools/list or /api/openapi actually shows, trust the live schema over this prompt and tell the user what changed.

How it runs

You pick the beat, then the name. Both are shown to you before anything is created, and a dry run returns the full plan so you can see exactly what you are approving. Say yes and it provisions the site, configures it and starts the first post.

The name matters more than it looks: it is committed into the site's address when the site is created. That is why the approved plan is handed back rather than re-derived, so the name you said yes to is the name that launches.

Domains

A free Letterstory subdomain is the default and costs nothing. You can point a domain you already own at it instead. Buying a domain through Letterstory spends real money and only ever happens when you explicitly ask for it.

What done looks like

The site is live on its address, and the first post has been written. Seeding runs on after the job reports finished, and a post takes twenty to forty minutes, so an empty site immediately afterwards is expected rather than a failure.

Related