Partners — an agent for each of your customers
Updated 2026-10-07
ghosty partner is for when your product wants to give an assistant to each of its customers. You have a partner space; each business using your product is a tenant (identified by your own externalId) and each person writing to that business is an end user (groupId).
- There is one shared agent, in solo-MCP mode: no machine, no disk; its only tools are those of your HTTPS MCP servers.
- Every turn carries a short-lived token that Ghosty passes to your MCP. It never goes into the prompt, memory or logs.
- Memory is per tenant and end user: one business never sees another's.
Your partner space
The space owner runs these, signed in (ghosty login).
createprintsGHOSTY_PARTNER_KEY=gpk_…andGHOSTY_PARTNER_KEY_SECRET=gps_…. Store them on your server right away: the secret is not shown again. To keep it off the screen:--out-env PATH(written with mode 600) or--format env(just the two lines, to pipe into another command).rotatetakes the same.
--out-envused to be--env-file; Node 22 swallows that name as its own flag (node: ./.env.partner: not found). The old name stays as an alias.
rotate --grace MINkeeps the old secret valid for that many minutes (default 60) so you can switch your server without downtime.credsis an alias ofcredentials;revokecan also be spelledrm.- If you own more than one partner space, pick one with
--workspace <slug>on any command.
The shared agent
A partner can have several agents: the default one (no --agent) and others with a slug (for example one for the business owner and a public one). Each has its own template and versions; with a slug, apply publishes directly. Over the API: PUT /api/v2/partner/agent with "slug" in the body, GET …/agent?slug=, and "agent": "<slug>" in POST /api/v2/partner/turns. A business's memory and usage span all its agents (tenants rm deletes it from all of them).
maxSteps (1-12) is how many model↔tool rounds a turn gets; if the model still asks for tools on the last one, it is asked for a final answer without them. If your MCP sends instructions in its initialize (e.g. how to use a catalog with discover_tools/run_tool), they go into the prompt (up to 6,000 chars).
maxTokens (256-8000, default 2000) caps each model reply and thinking (true/false, default false) turns on DeepSeek reasoning: slower and pricier, useful for a multi-step assistant.
With "engine": "claude" (e.g. "model": "claude-sonnet-5-5") the agent runs with a box (claude-worker) instead of solo-MCP: same turns contract and same SSE, but each conversation is sandboxed (one business can't see another's files or history), it has none of the space account's tools, and the turn token reaches your MCP through a Ghosty proxy (it never enters the box). Every MCP server must carry ${turn.token} in a header. The Claude key is the space's (or the house one if the space has it enabled).
Turn robustness. If your MCP doesn't answer at startup it is retried once and, if still down, the turn ends with error (it doesn't answer without tools). A provider 429/5xx is retried before the first chunk. If a tool answers 401 mid-turn (expired token) and the turn started in Ghosty (WhatsApp) or carries agent, a fresh token is exchanged at /api/nik/turn-token and the call is retried once. Read tools (get_, list_, search_, find_) in the same step run in parallel, and the ids each turn saw stay in its memory for the next one.
Limits. Besides the per-partner limit there is one per business (8 concurrent turns) and one per conversation (20 messages per minute); past them, turns answers 429.
agent.json carries only what you want to change:
HTTPS MCP servers only. Headers take no fixed keys: only the ${turn.token} and ${tenant.externalId} placeholders. If nothing changed, no new version is created.
Tenants
A tenant is config only (name, tone, timezone, locale, notes): it has no machine of its own. The first turn for a new externalId also registers it.
Usage per business
Over the API, signed with the credential (GET with an empty body): GET /api/v2/partner/usage?externalId=&from=&to= → per business turns, tokens, costUsd, a per-day breakdown (Mexico City days) and errors by reason (mcp_down, provider_429, token, timeout, cancelado, reinicio…). from/to as YYYY-MM-DD or ISO; 92 days max.
Try a turn
--agent <slug>tries another agent of the partner (default: the default one).--groupis the end user (defaultcli); each one gets its own conversation.--turn-tokenis the token Ghosty will pass to your MCP as${turn.token}. Here--tokenworks too and means the same (it is not your session).--verboseshows each tool's arguments (without secret-looking keys) and how long it took:↳ denik__buscar_citas {"fecha":"2026-10-01"}·✓ denik__buscar_citas 340 ms.- With
--jsonit prints one line per turn event (chunk,toolwithargsandms,error…).
Test as your server
With the credential in the environment, the CLI signs every request the way your backend will (HMAC of ${ts}.${keyId}.${body}):
An agent's WhatsApp groups
One of your agents (e.g. nik-admin) can have its own WhatsApp number (paired with ghosty channels wa …). Each group it is in gets linked to a business, and from then on it answers everything there with that business's tools:
Over the API: GET|POST /api/v2/partner/wa/groups and PUT|DELETE /api/v2/partner/wa/groups/{jid}. On each turn Ghosty asks your server for the token with {"externalId","aud":"<agent slug>","groupId":"<jid>"}; your answer may also carry appendSystemPrompt (≤ 4,000) and config (name, timezone…). If you answer 403/404, that message gets no reply (it shows in ghosty turns ls <id> --skipped). The sender's name goes in the text (María: …); attachments arrive as signed URLs and voice notes transcribed.
A business's WhatsApp Business number
One of your businesses' WhatsApp Business number can be answered by one of your agents (e.g. nik-public). If you register the number in Formmy yourself, ask Ghosty for the link and give it to Formmy; none of your keys travel:
Formmy gets url as externalAgentUrl and secret as externalAgentSecret, and posts each message to ${url}/message with Authorization: Bearer <secret>. Over the API: GET|POST /api/v2/partner/waba/links and PUT|DELETE /api/v2/partner/waba/links/{id}.
If the number is already in Formmy and you move it from Ghosty:
On each message Ghosty asks your server for a short-lived token (POST /api/nik/turn-token signed with your credential: X-Ghosty-Key, X-Ghosty-Ts, X-Ghosty-Sig = HMAC of ${ts}.${keyId}.${body}; body {"externalId","aud","groupId"}) and runs the turn with it. The groupId is wa:<phone>. If your server doesn't return a token, the message is recorded without a reply (ghosty turns ls <id> --skipped).
GHOSTY_PARTNER_SECRET still works (the old name), but don't mix it up: Ghosty's server uses that same name for a secret of its own. Use GHOSTY_PARTNER_KEY_SECRET.
That works for agent, tenants and try. ls, enable, credentials and audit always use your session: a credential cannot mint other credentials.
A boxed agent for your users
If instead of the shared MCP-only agent you want a normal agent (Claude with a box, shell and files) that serves each user of your product with THEIR key:
Each turn: POST /api/v2/fleet-agents/<agent>/message-stream with
{groupId, text, turnToken} (one groupId per end user; turnToken 8 to 4096 characters, no
whitespace) and the headers X-Ghosty-Key: gpk_…, X-Ghosty-Ts (epoch seconds) and
X-Ghosty-Sig = hex HMAC-SHA256, keyed with gps_…, of ${ts}.${keyId}.${body}. The response is
SSE (chunk, tool, done, error). The token only reaches your MCP: it never enters the box and
is never stored. Without turnToken, the turn fails with error. The credential only reaches the
normal agents of your workspace.
Landing pages for your businesses
Your server can ask Ghosty for a business's landing page built from its real data, section by
section, to paint it in your own editor. Same signature as turns (X-Ghosty-Key, X-Ghosty-Ts,
X-Ghosty-Sig). Nothing is stored or published on Ghosty: the HTML is yours.
POST /api/v2/partner/pages/build (SSE):
Events, each one data: {…}:
- Real data: no invented figures, years, reviews, testimonials or services. Without services
there is no services section; testimonials, stats, team and pricing are never requested. Every
service with a
urlgets a «Agendar» (book) button to that link. - Photos: yours fill the image slots first (in order); the rest come from free stock banks,
picked with vision. CC photos carry
data-credit: render the credit in the footer. dialect:gs(default) uses Ghosty's builder tokens;denikleaves classes thatbakeTailwindHtmlcompiles and that follow your brand kit (nosurface-deep, fonts asstyle,httpsphotos withoutcrossorigin, nodata-id). Both keepdata-animate.- Limits: 4 builds at a time per partner and 20 per business in 24 h (
429). Spend is recorded in your agent's usage, perexternalId.
POST /api/v2/partner/pages/review with {html, viewport?, theme?, externalId?, dialect?} opens it in
a real browser (desktop 1280 and mobile 390) and returns
{problems: [{sectionId, kind, detail, viewport, nodeId}], fixes, sections, skipped, seconds};
kind ∈ broken-image, overflow, clipped, overlap, contrast, contrast-unsure, dead-link.
sections holds only the ones the review fixed. If the page could not be rendered it answers 503,
never an empty list.
Animation: copy
(3 KB, CSS + JS) once before </body> of your published page. Without it the page is complete, just
still.