Skip to content
Hypamoon
API docs

Start for free. Sign up without a card and you'll get a test project and a test key right away. Once your company (or, for a personal account, you) is verified, you can create a live key in the portal.

Hypamoon for agents: the MCP server

The Hypamoon API is also an MCP server (Model Context Protocol). Add it to your chat app or coding agent and the agent builds on Hypamoon for you: it publishes a site, stores form entries, finds or connects a domain, sets up a number with the voice AI assistant and a call plan, sends SMS, adds a chat to your site, runs agents and server code, and hands you the code to build the integration into your own app. It can simulate a call and read the transcript before anything is real.

The MCP URL (referred to below as HYPAMOON_MCP_URL):

https://api.hypamoon.com/api/v1/mcp

It is the same API with the same key: Authorization: Bearer hm_test_... or hm_live_.... The consumer apps (Claude.ai, ChatGPT, Grok) sign in with OAuth instead of a key; see below. Start with your test key. In test mode nothing costs money, numbers are test numbers and calls are simulated; the agent can run the whole quickstart without a phone. Swap in the live key when everything works.

The examples read two environment variables:

curl
export HYPAMOON_MCP_URL="<the MCP URL above>"
export HYPAMOON_API_KEY="hm_test_..."

What the key allows

The key needs the scope project:read; without it the server returns 403 missing_scope. Its other scopes determine which tools the agent sees. A test key has all scopes. A read-only live key sees get_project, get_balance, search_numbers, list_numbers, get_call_plan, list_calls, get_call, list_messages, get_message, list_conversations, get_conversation, list_sites, get_site, list_collections, query_documents, get_form_snippet and get_integration_code. simulate_call and release_number only exist for a test key.

There are sixteen scopes, and every tool needs one of them. The tools that came later use the same scopes: apps, domains, site versions and store exports use sites:read and sites:write; agents and secrets use project:read and project:write; email uses messages:read and messages:write; the brain, payments on sites and paid access use data:read and data:write. A key or a connection with all scopes sees every tool that is available, also new ones, without asking again.

Every tool carries all four annotations: readOnlyHint, destructiveHint, idempotentHint and openWorldHint. A tool that changes anything has readOnlyHint: false, so an app that asks before a write action (ChatGPT does) asks for it.

Add it to your tool

Lovable

In your Lovable project, open Connectors, choose + and then MCP server (a custom or personal connector). Fill in:

  • URL: the MCP URL.
  • Authentication: a header Authorization with the value Bearer hm_test_... (some versions call this "Bearer token" or "API key"; in that case, paste only the key).

Then ask Lovable in the chat, for example: "Use the Hypamoon connector. Call get_project and follow next_steps to add an AI phone receptionist to this app. Use get_integration_code with environment lovable for the code."

The connector key is for the agent. The app itself also needs the key, as a secret of its Edge Functions (HYPAMOON_API_KEY); get_integration_code tells Lovable to ask you for it through its secrets form. Never paste a key into the chat.

Claude Code

Claude Code signs in with OAuth, like the apps, so there is no key to copy:

curl
claude mcp add --transport http hypamoon "$HYPAMOON_MCP_URL"

Then type /mcp in Claude Code, pick hypamoon and choose Authenticate. Your browser opens; log in at Hypamoon and pick the project.

Where nobody can sign in (CI, a script), use a key instead:

curl
claude mcp add --transport http hypamoon "$HYPAMOON_MCP_URL" \
  --header "Authorization: Bearer $HYPAMOON_API_KEY"

This stores the key in your own Claude Code settings. For a shared .mcp.json in a repository, keep the key out of the file and let Claude Code read it from the environment:

JSON
{
  "mcpServers": {
    "hypamoon": {
      "type": "http",
      "url": "${HYPAMOON_MCP_URL}",
      "headers": { "Authorization": "Bearer ${HYPAMOON_API_KEY}" }
    }
  }
}

Cursor

In ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):

JSON
{
  "mcpServers": {
    "hypamoon": {
      "url": "${env:HYPAMOON_MCP_URL}",
      "headers": { "Authorization": "Bearer ${env:HYPAMOON_API_KEY}" }
    }
  }
}

Cursor can also sign in with OAuth instead of a key: leave out headers and give only the url. Cursor then opens the Hypamoon consent screen in your browser, as described below for the apps. During registration Cursor sends cursor://anysphere.cursor-mcp/oauth/callback as one of its callbacks; that exact address is allowed, no other custom scheme is.

Use a smaller set if your editor limits tools

Cursor shows about 40 tools per server, and Hypamoon has over 100. Add ?tools= to the MCP URL to load one or more sets, separated by commas: ?tools=sites, ?tools=sites,data. Every set also brings get_project, get_capabilities, list_activity, get_recent_errors and list_toolsets (which lists the sets and how to switch). Leave it out, or use ?tools=all, for every tool. Your key's scopes still decide what you can do; a set only hides tools.

set what it covers
sites websites: deploy, uploads, versions, addresses, WordPress import, and forms with their collection (create_collection, get_form_snippet, create_chat_widget for the public token, query_documents)
media the media library: upload pages for photos and files, list and organise them, and put them on a site
domains check, register and connect domains, DNS, a new secret link for an unlisted site (rotate_site_secret_url), and Google Search Console verification (verify_search_console)
data collections and documents, secrets, the business brain
phone numbers, the AI receptionist, calls, SMS, webhooks, chat
mail email from your own domain
mail_automation email that goes by itself: templates, automations on events, scheduled email, sequences, and lists (lists not available yet)
commerce payments, orders, paid access, shipping an app to the stores
apps server code for a site: deploy, logs, users, files, database
agents agents on a schedule and their approvals
team members, roles, four-eyes approvals (approving or rejecting a waiting deploy)
agency for agencies: client projects, access, owner invitations, who pays, budgets, usage, invoice lines, activity, and payment links and subscriptions for clients

ChatGPT ignores ?tools=. An optional ?via=<channel> on the URL only tells us where you found Hypamoon; it changes nothing about what you can do.

Claude, ChatGPT and Grok (the apps): sign in with OAuth

The consumer apps add a remote MCP server by URL and sign in with OAuth; they do not let you set your own Authorization header. Hypamoon is an OAuth 2.1 authorization server for exactly that. You paste the MCP URL, the app sends you to a Hypamoon consent screen, and there you choose:

  • the project: your test project or your live project (start with the test project);
  • what the app may do (the scopes; project:read is always on).

The app gets a token for that project with those scopes. It works like a key with the same scopes: the same tools, the same test mode, the same errors. You need to be an owner or admin of the project. The connection is listed in the portal at https://app.hypamoon.com/koppelingen (Connect your chat), with the app, the project (test or live), the scopes and when it was last used. Revoke it there; the app stops working immediately and has to ask for access again.

The token also works on the REST API (every /api/v1 route), not only on the MCP URL. It is for the assistant, though, not for your app: when the assistant builds an app for you, create a separate API key for that app at https://app.hypamoon.com/api-sleutels.

  • Claude.ai (and Claude Desktop): Customize, Connectors, Add custom connector. Name Hypamoon, URL: the MCP URL. Leave the OAuth client fields empty; Claude registers itself. Choose Connect and sign in. On the free plan there is room for one custom connector. Tip: sign in at app.hypamoon.com in the same browser first; then the connector connects in one step.
  • ChatGPT (on the web, also on the free plan): open Plugins, choose Add, then Add custom MCP server. Name Hypamoon, server URL: the MCP URL. Choose OAuth and leave the client fields empty; ChatGPT registers itself (dynamic client registration). Confirm the warning, choose Create as a plugin, install it, then connect and sign in. In a work account, an admin may have to allow custom MCP servers first. If the dialog asks for an icon, use https://hypamoon.com/downloads/hypamoon-icon.png. Tip: sign in at app.hypamoon.com in the same browser first; then it connects in one go. ChatGPT reads the list of tools when you connect and keeps it. When Hypamoon adds tools, refresh the plugin or connect it again. ChatGPT asks for your confirmation before a tool that changes something (publishing a site, deploying an app); tools that only read are marked as such and run without asking. If ChatGPT offers only the tools that read, your plan or workspace limits custom servers to reading. ChatGPT sees the same tools as Claude with the same scopes, except buying or transferring a domain, payments setup, refunds and secrets, which the account owner does in the Hypamoon portal.
  • Plugin archive (ChatGPT and Claude): download https://hypamoon.com/downloads/hypamoon-plugin.zip. In ChatGPT: Plugins, Add, Upload plugin archive. In Claude: Customize, Plugins, Add, Upload plugin. Then connect Hypamoon and sign in. The archive holds only the MCP URL, the name, the icon and short instructions for the assistant; no key or token.
  • Grok (on the web, iOS and Android; connectors work on every Grok plan, the free one included): go to https://grok.com/connectors, choose New Connector, then Custom. Paste the MCP URL and choose OAuth; Grok registers itself (dynamic client registration), so there is no client ID to fill in. Connect and sign in with the email link. Tip: sign in at app.hypamoon.com in the same browser first; then the connector connects in one go. Grok reaches the MCP URL from its own servers, which is why it has to be a public address like ours. On Grok Business or Enterprise, an admin adds the connector first in https://console.x.ai under Grok Business, Connectors, Add Connector, Other; members then connect and sign in as above.
  • Grok Build (the command line): grok mcp add --transport http hypamoon <MCP URL> with the MCP URL above, then sign in when it asks.
  • xAI API: add a remote MCP tool with server_url set to the MCP URL and authorization set to your hm_ key (hm_test_... or hm_live_...).

Signing in is by email link. If the link opens in a new tab, the app's window picks it up by itself and continues there; the new tab only tells you to go back to that window. If the link opened in the app's window itself, choose Continue here on that page. If you closed that window in the meantime, add the connector again: you are signed in now, so it connects in one step.

The menus of these apps change often; if a name differs, look for "custom MCP server", "custom connector" or "plugin" and give it the MCP URL. The APIs of these vendors (the Claude Messages API with mcp_servers, the OpenAI Responses API with an mcp tool, the xAI API) can still send a key as the Bearer token, as before.

How the OAuth part works (for client builders)

It follows the MCP authorization specification (revision 2025-11-25, which includes 2025-06-18): OAuth 2.1 with PKCE, Protected Resource Metadata (RFC 9728), Authorization Server Metadata (RFC 8414), Dynamic Client Registration (RFC 7591), resource indicators (RFC 8707) and token revocation (RFC 7009).

  1. POST to the MCP URL without a token returns 401 with WWW-Authenticate: Bearer resource_metadata="https://api.hypamoon.com/.well-known/oauth-protected-resource/api/v1/mcp", scope="project:read ...".
  2. The resource metadata (also at /.well-known/oauth-protected-resource) names the MCP URL as resource and https://api.hypamoon.com as the authorization server.
  3. https://api.hypamoon.com/.well-known/oauth-authorization-server lists the endpoints: authorization_endpoint https://app.hypamoon.com/oauth/authorize (the consent screen, where your dashboard login lives), token_endpoint /oauth/token, registration_endpoint /oauth/register and revocation_endpoint /oauth/revoke on the API host. code_challenge_methods_supported is ["S256"].
  4. Register with POST /oauth/register (JSON). Redirect URIs must be https, or http on localhost, 127.0.0.1 or [::1] (any port). Public clients use token_endpoint_auth_method: "none"; a client that asks for client_secret_post or client_secret_basic gets a secret once.
  5. Authorization code flow with PKCE S256 (required) and the resource parameter set to the MCP URL. The redirect carries code, state and iss.
  6. Tokens: an access token hm_oat_... for one hour and a refresh token hm_ort_... for 30 days. The refresh token only comes when the client registered refresh_token in grant_types (the default). Every refresh gives a new pair and retires the old refresh token. Using a retired refresh token again (after a ten-second grace period for parallel requests) revokes the whole connection.
  7. Errors at the MCP URL: an invalid, expired or revoked token gives 401 with error="invalid_token"; a token without project:read gives 403 with error="insufficient_scope".

Client ID Metadata Documents (the newer alternative to registration) are not supported yet; the metadata says so (client_id_metadata_document_supported: false), and clients fall back to registration.

Any other MCP client

Transport Streamable HTTP, stateless: every request is one POST with a JSON-RPC 2.0 message and gets one JSON response. No session id and no SSE stream (a GET returns 405). Protocol versions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. Send the key on every request, including initialize.

The tools

tool scope what it does
get_project project:read The project, its mode (test or live) and a setup checklist with next_steps. Always the first call.
get_capabilities any key What Hypamoon can do, per rail (sites, data, apps, domains, phone/SMS/chat, payments, email, agents (and the brain once it is available), advice/SEO/stores): what it is, its status (live, early_access, coming or not_on_this_connection), the tool to start with, the tools this connection has for it and which plan includes it (no amounts). The status follows the feature flags, the price source and the tools the key can see, so it never drifts. Not in ChatGPT while the app is in review.
get_balance project:read The prepaid balance, this month's spending against the cap, automatic top-up and the latest 20 transactions. With a test key: the live project's balance, read-only. Topping up is done in the portal.
set_company_details project:write A business (company name, registration number, address, contact person) or, with account_type personal, your own name, address and phone number; required before ordering a number. The registration number is kvk_number for a company in the Netherlands, registration_number elsewhere. A country we do not serve gives country_not_supported. In test mode no registration number is needed. For a business the contact can be a role (like Domain administration); a phone number still needs a person. To keep a role on the domains while the contact is a person, send registrar_contact_name; left out, the saved value stays. With domains registered or transferred through Hypamoon, the contact, email, phone and address also go to the registry: domain_contacts says per domain updated, unchanged, skipped (a test domain, or a change of holder, which needs support) or failed.
search_numbers numbers:read Available numbers to order (test numbers with a test key). What it returns is what can be ordered right now.
list_numbers numbers:read The numbers the project has, with their ids.
order_number numbers:write Orders a number. Safe to retry: without idempotency_key the key is derived from the arguments, so the same call returns the same number. Pass a new idempotency_key to order a second number with the same arguments.
release_number numbers:write, test key only Removes a test number from the test project, so another one can be ordered. A live number cannot be released through the API.
get_assistant assistant:write The voice AI assistant.
set_assistant assistant:write Language, voice, languages, instructions and website of the assistant.
get_receptionist_test_link assistant:write A link to the Assistant page, where the user talks to their own assistant through the microphone in the browser, in test mode too and without a number, verification or payment (up to 3 minutes per call, 5 calls or 10 minutes a day per project). Behind HYPAMOON_TELEFONIST_BROWSER; not in ChatGPT.
get_call_plan numbers:read The call plan of a number, with its version.
set_call_plan numbers:write What happens when someone calls: a small graph (forward, ai_agent, hours, menu, announcement, voicemail, hangup).
list_calls calls:read Calls, inbound and outbound, newest first, with a cursor; direction filters by one kind.
get_call calls:read One call with transcript and summary.
start_call calls:write The assistant calls one person who expects it (an appointment, a callback they asked for). Never cold calling or sales: telemarketing to consumers needs their prior consent under telemarketing law, and national do-not-call registers must be respected. The assistant always says it is a digital assistant. Test numbers give every outcome in test mode; in live mode the agent asks you to confirm every call first. Safe to retry: without idempotency_key the key is derived from the arguments, so nobody is called twice.
record_call_consent calls:write Records that one person agreed to marketing calls from this project (source: form, call, import, manual; evidence: a reference to the proof; consented_at). Needed before start_call with purpose: "marketing". Stored as a keyed hash plus the last two digits. Only the owner or an API key. Not in ChatGPT while the app is in review.
list_call_consents calls:read Lists the recorded consents, active and revoked; phone_number narrows it to one number. Not in ChatGPT while the app is in review.
revoke_call_consent calls:write Revokes consent by consent_id, or every active consent for a phone_number. Nothing is deleted; from then on a marketing call to that number returns consent_required. Only the owner or an API key. Not in ChatGPT while the app is in review.
list_webhooks webhooks:write The webhook endpoints.
create_webhook webhooks:write Registers your app's webhook URL; returns the secret once.
delete_webhook webhooks:write Deletes an endpoint.
enable_webhook webhooks:write Switches an endpoint back on after it was switched off.
send_test_webhook webhooks:write Sends a signed webhook.test and reports how it went.
send_message messages:write Sends one transactional SMS (order confirmation, shipping update, reminder) to a mobile number. A destination that is not supported gives destination_not_allowed with a reason. Never marketing: that needs the recipient's prior opt-in under marketing law. Safe to retry: without idempotency_key the key is derived from the arguments.
list_messages messages:read SMS messages, newest first, with their status and a cursor.
get_message messages:read One SMS message with its status: queued, sent, delivered or failed.
start_conversation chat:write Starts a text chat with the project's assistant: the same assistant that answers the phone, with its instructions, languages, opening hours and website. Returns the conversation with the fixed greeting. Safe to retry: without idempotency_key the key is derived from the arguments.
send_chat_message chat:write Sends one customer message and returns the assistant's reply. In a chat the assistant does not book, transfer or send anything: it takes a message for the summary. Safe to retry: without idempotency_key the key is derived from the conversation and the text.
list_conversations chat:read Chat conversations, newest first, without messages, with a cursor.
get_conversation chat:read One chat conversation with all its messages and, once closed, its summary.
close_conversation chat:write Closes a chat conversation; the summary follows as conversation.summary.ready. Closing twice is harmless.
create_chat_widget chat:write Creates a public chat token for the chat widget on a website, for the exact origins you give (like https://www.example.com). Returns the token and embed_code, the script tag for every page. The token is public by design: it only chats, only from those origins, with a bot check and limits. A test key gives a test token that also works from localhost.
list_chat_widgets chat:read The public chat tokens with their origins and embed_code, including revoked ones, and the daily spending limit of the public chat.
update_chat_widget chat:write Changes a public chat token in place, so the snippet keeps working: display_name (the business name in the chat window and its greeting; null falls back to the site name, then the project name), the label, allowed_origins, turnstile or the daily limit.
revoke_chat_widget chat:write Revokes a public chat token immediately: the widget stops, including in open conversations. Revoking twice is harmless.
deploy_site sites:write Publishes a static website at https://<slug>.hypamoon.app and returns its url. Free, in test mode too. The agent sends complete files for a new site (at least /index.html, css and js inline or as files, images, fonts, PDFs, video (mp4, webm) and audio (mp3, m4a) preferably as source_url that Hypamoon fetches, base64 only without a URL and never for video or audio), no build step. A PDF opens inline in the browser; video and audio play with seeking (Range requests) and count as one site request per MB served. To change an existing site it sends site_id, keep_existing: true, if_match_version and only the changed files; the url stays the same. Without keep_existing the site gets exactly the files sent, and the response lists removed paths in warnings. With password the site asks every visitor for that one shared password (once per browser); null removes it. With media it puts files from the media library on the site without uploading them again: folders like portfolio/, file ids or paths like /media/logo.webp, each with its 640 version; private originals never go on a site, and a file you also send in files wins.
create_upload_urls sites:write Upload links for files too large to send inline: per file path, sha256 and bytes; the agent PUTs the bytes to each upload_url (15 minutes, checked on size, SHA-256 and the type of the bytes), then names the file in deploy_site with path, sha256 and bytes instead of content. Limits per plan: free 200 files, 5 MB per file (a PDF 10 MB, video or audio 10 MB), 50 MB per site; paid (a paid plan or credit) 2,000 files, 25 MB per file (a PDF 25 MB, video or audio up to 90 MB, only via an upload link), 1 GB per site.
create_drop_link sites:write A one-time upload page for the user, https://app.hypamoon.com/drop/<token>, valid for 24 hours, for this project (and a site, with site_id). For when the agent's environment cannot reach an upload_url (a sandbox in a chat app may block the PUT), or the user has the files. The user opens it without signing in, drags or chooses files (images including SVG, pdf, fonts, video and audio, within the plan's limits) and sees "Done. Go back to your chat." The answer has the url (shown once), a user_message to give with it and the id for get_drop. Not in ChatGPT while the app is in review.
get_drop sites:read A drop link: status (open, done, expired), files (path, sha256, bytes, type, the original name) and deploy_files, the same list in the form deploy_site takes (no content; another path is fine, keep sha256 and bytes). Not in ChatGPT while the app is in review.
create_media_link sites:write An upload page for the project's media library (https://app.hypamoon.com/drop/<token>, 24 hours or until Done), with an optional folder, site_id (Done also puts the files on that site), images_only and strip. Everything lands in Media; the chat then calls list_media. Not in ChatGPT while the app is in review.
list_media sites:read The media library, newest first: folders with counts, and per file id, path under /media/, size, width and height, the 640 version, alt, url (null while it is on no site: private) and the sites that use it. folder, site_id, limit and cursor. Never the bytes. Not in ChatGPT.
get_media sites:read One file from the library by media_id or path. Not in ChatGPT.
update_media sites:write Rename (name), move (folder) or set the alt text of a library file; the 640 version moves along, sites keep their copy until the next deploy. Not in ChatGPT.
delete_media sites:write Delete a library file with its versions; refused (409 conflict) while a site uses it, with the site and path. Not in ChatGPT.
get_confirmation project:read The status of an action a chat set up for confirmation in the portal (see "Confirm in the portal" below): pending, processing, done, cancelled, failed or expired, with confirm_url (and group_confirm_url when several wait) and, when done, a short result. Only the user's own confirmations in this project. Not in ChatGPT while the app is in review.
propose_plan sites:write Asks the user once to approve a whole goal (site_on_domain, email_from_domain or move_domain) instead of a link per step; returns one confirm_url. After the user confirms, the steps of that plan (connect_domain, set_dns_record, import_zone_file, switch_domain_nameservers) run without a new link for 24 hours; a step outside the plan gets its own link. Only for chat connections, behind HYPAMOON_PLAN_BEVESTIGEN. Not in ChatGPT while the app is in review. See "Approve a goal at once" below.
list_sites sites:read The static websites of the project with their ids, urls, files (without content) and password_protected.
get_site sites:read One static website with its url, files (without content) and password_protected.
delete_site sites:write Takes a website offline right away and removes it from list_sites. For 30 days it can be restored as it was with restore_site (or in the portal under Sites & apps, Recently deleted); after that its files and versions are erased for good. permanent: true erases everything right away, also for a site that is already deleted; only when the user explicitly asks for it (not in ChatGPT while the OpenAI review runs). An app on the site is deleted with it and a custom domain is disconnected. The slug stays taken.
restore_site sites:write Puts a site deleted in the last 30 days back live at its old address, with its files, password, visibility and versions. Without site_id it lists what can be restored, with restore_until. An app on the site and a custom domain do not come back; restore_notes says how to set them up again. The plan's site limit applies. Not in ChatGPT while the OpenAI review runs.
suggest_site_address sites:read Checks the address the user wants and suggests up to three free ones from the name. Names that look like a bank, login, payment or support page, a brand, or anything with hypamoon in it are refused. Nothing is reserved.
update_site_address sites:write Renames a site (the old address redirects with 301 for 90 days and then stays reserved for the project for 30 more days; at most 2 old addresses redirect, and at most 3 renames per site and 10 per project per 30 days, past that rate_limited with a user_message that says when; a connected domain and public tokens keep working) and/or makes it public or unlisted (only at a secret link of 26 random characters, not indexed, the readable address returns 404). A secret link is not a login; use password for real protection. Four-eyes approval applies.
rotate_site_secret_url sites:write A new secret link for an unlisted site; the old one stops working within seconds.
list_site_versions sites:read On every plan (the plan sets the retention: Free and Online 30 days, Reachable 90, Business a year). The history of a site, newest first: every deploy that changed files (including partial, import, approved, rollback, promoted preview) with number, time, author (user, via, client: ChatGPT, Claude, API, portal, wizard), message and the changed paths. Previews are listed with preview_url. auto_label marks good versions to go back to: before big change (live when a deploy hit the guardrail, kept like a label) and stable (live for at least 24 hours and not rolled back); a preview the guardrail made has held by guardrail. Paginated with cursor.
get_site_diff sites:read What changed between any two versions (from and to, by id or number; default: the one before and the current one): per path added, modified, removed or replaced; a unified diff for text files, old and new size for binary files, with limits on size.
rollback_site sites:write Puts the files of an earlier version live again right away, as a new version. Deploy checks and four-eyes approval apply; idempotent; if_match_version.
restore_site_files sites:write Puts only the files in paths back as they were in an earlier version, live right away; every other file stays. A new version with cause file_restore and a message. Deploy checks and four-eyes approval apply; idempotent. Not in ChatGPT while the OpenAI review runs.
promote_site_version sites:write Publishes a preview (deploy_site with preview: true, or one the guardrail held back, at a secret link with noindex) as a new version. Four-eyes approval applies.
label_site_version sites:write Gives a version a name, like "before launch"; labeled versions are kept beyond the retention of the plan, the plan sets how many labels a site can have.
create_collection data:write Creates a collection: a named list of JSON documents, the project's own data layer. For the entries of a contact form, sign-ups, reservations, or facts for the assistant. access: private (default), public_write (a form on a website, with the public token, a bot check and limits), public_read (a page shows the newest documents), assistant_read (the assistant gets the newest documents added through the API). triggers: email_on_insert (live only), webhook_on_insert (document.created). email_routing: who gets that email instead of the billing email (at most 5 addresses, optionally per value of a field). Optional JSON Schema. Where the shop settings are available, role (products or orders) makes it a collection the shop uses.
list_collections data:read The collections with their access, triggers, schema and document_count.
update_collection data:write Changes name, description, access, triggers, email_routing or schema; fields you leave out stay. email_routing is replaced as a whole, null sends the email to the billing email again.
delete_collection data:write Deletes a collection: its documents can no longer be reached and a form that posts to it stops. Deleting twice is harmless.
insert_document data:write Adds a JSON document (at most 64 kB) to a collection; with a schema it must match. The collection's triggers fire.
query_documents data:read The documents of a collection, newest first, with limit, cursor, before and a filter on top-level fields.
update_document data:write data replaces a document, merge changes some fields (JSON Merge Patch: null removes a field).
delete_document data:write Deletes one document for good.
get_form_snippet data:read The HTML of a working form for a collection with public_write, with the fields you name, a honeypot and the form.js script tag, and the project's public token filled in. Never a mailto link.
simulate_call calls:read, test key only Runs a simulated call with transcript and summary and sends the real webhooks. A simulated caller (optional caller_goal) talks to the project's own assistant with its real instructions, knowledge and tools; tools are not executed. 10 free per day per test project. The first thing to try.
get_integration_code any Code for lovable, supabase-edge-function, nextjs-route, express or python-flask: the key as a server-side secret, API calls, and a webhook receiver with the signature check. part: "sms" gives a function that sends order updates by SMS. part: "calls" gives a function that lets the assistant confirm an appointment by phone, and how to read the outcome in the webhook receiver. part: "widget" gives the chat widget for a website: where the script tag goes in that environment, its options (data-color, data-position, data-title, data-welcome, data-lang, data-open) and a button of your own with window.Hypamoon.chat.open().

Sites

deploy_site gives a project a website without separate hosting: the agent writes plain HTML, CSS and JavaScript, sends the files, and the site is live at https://<slug>.hypamoon.app right away. Ask for a page for your business, and then put the number on it (order_number, set_assistant) or the chat widget (create_chat_widget with the site's url in allowed_origins, then deploy_site again with the embed_code in index.html). There is no build step, so the agent sends what the browser loads, never React or Vite source. A project can have as many sites as it likes; on Free an account can create up to 5 new sites a day, each with up to 200 files and 50 MB (5 MB per file). A change is live for everyone as soon as deploy_site answers, all files at once.

To share a site with a few people, ask for a password: the agent sets password on deploy_site, and every visitor sees a password form first, on every path, until they enter it once per browser (ten tries per address per ten minutes). One shared password, no accounts; the response only says password_protected: true. A new password logs everyone out, and null opens the site again.

Moving a site from another static host: two plain-text files go along with deploy_site. /_redirects has one rule per line, from to [status] (301, 302, 307 or 308): to is a path on the site or a full https:// URL, and :splat in its path takes the part under a * in from (/docs/* https://docs.example.com/:splat 301). The domain always comes from the file, never from the visitor, and the query is not passed on. /_headers works like Netlify's (a path line, indented Name: value lines) for Content-Security-Policy, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cache-Control, Link, Strict-Transport-Security without preload, Access-Control-Allow-* and your own X- headers; deploy_site refuses any other header with the line number (Set-Cookie, Location, Content-Type, anything on /_hypamoon/), and allows at most 100 paths and 300 header lines. A Next.js static export (output: "export") deploys as it is, /_next/ included.

Sites live at https://<slug>.hypamoon.app. The old address, https://<slug>.apps.hypamoon.com, now returns a permanent redirect (301) to the new one. Always use the url from the response, not a hand-built address.

A static site has no visitor accounts: for sign-in, build an app with sign-in. Without code, password keeps a site private, paid access gives each buyer a personal sign-in link, and the agent is told never to build a password screen in JavaScript (the files are public, so it protects nothing). Server code on the same address is an app.

Media

The media library holds the project's photos and files, apart from any one site: the user uploads them in the portal under Media, or through an upload page from create_media_link that works on a phone without signing in. A file in the library is private until it is on a site. list_media shows the folders and the files with their path under /media/, size, alt text and the sites that use them; deploy_site with media puts them on a site, with a 640 version for fast pages. update_media renames, moves or sets the alt text, and delete_media refuses while a site still uses the file.

An SVG is welcome for a logo or an icon, up to 1 MB, as long as it only draws: anything that can run (a script, an event attribute, a link or a style that reaches outside the file) is refused with one short line on that file, never cleaned up quietly. On a site, an SVG under /media/ or /uploads/ is served so that nothing in it can run, also when someone opens it directly.

The media tools are in Claude, Grok and other MCP clients, and through the API (GET /media, POST /media/links); not yet in ChatGPT.

Your own domains

A site can run on the user's own domain. Connecting a domain the user already owns works today. Buying a new domain with register_domain is simulated with a test key, and real with a live key or a normal connection as soon as the project is verified for it; until then it answers 403 verification_required with a link to what is missing. The tools use the sites:read and sites:write scopes.

Tool Scope What it does
check_domain sites:read Whether one domain is available, its price for the first year and for renewal, can the user register it here, and the link to the registry's terms (registry_terms_url). Some names cost more at the registry than usual for their extension: the check then gives that price, and such a name can be bought for 1 year at a time up to a ceiling, with accept_price_eur on register_domain.
suggest_domains sites:read Available domains, like a domain search: for a few words (the words themselves on .nl and .com first, then short variants like getbakery or bakeryhq), or for up to ten names the agent thought of itself (names), checked in one call. Taken names come back in unavailable, invalid ones in rejected. Nothing is sponsored.
register_domain sites:write Registers a new domain in the user's own name, with DNS here. The agent must show the user the domain, the total price and the registry's terms and get an explicit yes first (accept_registry_terms, max_price_eur, and for a name with a higher registry price accept_price_eur). Safe to retry; after a failed attempt use a new key. Not available in ChatGPT: there the account owner registers a domain in the portal.
connect_domain sites:write Connects a domain the user already owns. Default: the user points the domain's nameservers to the two we return; existing records, mail included, are copied first, so email keeps working, and mail_check warns when something is missing (pass dkim_selectors then). Fallback: only www through a CNAME at the user's own DNS provider. With site_id the site goes live on it. Connecting your own domain is free on every plan; buying one with register_domain costs the registration price, and the CNAME fallback carries a small surcharge. The Hypamoon badge stays on the site unless the project has a paid plan. If another project is waiting on the same domain, a TXT record proves ownership. An unused connection expires after 7 days.
get_domain sites:read One domain (or all of them) with its status, the next step and, once live, its url. For a registered domain also the expiry date, whois privacy and whether the registry asks the user to verify their email.
activate_domain sites:write Checks whether the nameservers (or the CNAME records) are in place, scans the previous DNS again while it waits, and puts the site on the domain once it is active. get_domain itself only reads.
update_domain sites:write Changes a setting of a connected domain. hsts tells browsers to always open the domain over https: off (default), on (for six months) or on_with_subdomains. Once on, a browser that visited the site only opens the domain over https for months, even after turning it off; include subdomains only if every subdomain works over https. Only possible once a site is live on the domain with a valid certificate.
set_domain_auto_renew sites:write Turns automatic renewal on or off for a domain bought or transferred through Hypamoon. On (default): renewed for a year before it expires, from the project's balance. Off: it expires on its expiry date. Not shown in ChatGPT while the app review runs.
get_domain_authcode sites:write The transfer code of a registered domain, to move it to another registrar. Only for the account owner (connected through OAuth as owner, not with an API key); logged, and the owner gets an email.
list_dns_records sites:read The DNS records of a connected domain.
set_dns_record sites:write Adds, replaces or deletes one DNS record. A new A, AAAA or CNAME replaces the one already on that name. On an active domain a change that replaces or deletes a live record first answers confirmation_required with a preflight; confirm_overwrite: true after the user agreed. The records that serve the site are managed by Hypamoon.
disconnect_domain sites:write Takes the site off a connected domain and removes its DNS here; the user then sets the previous nameservers back. A registered domain stays.
diagnose_domain sites:read What is set up on a domain or subdomain right now, with one suggested fix per problem: nameservers (at Hypamoon or not), the records here with cleanup labels, what answers on the domain, www, the subdomain and the main names (status, server, certificate), email (MX, SPF, DKIM, DMARC, Microsoft 365 or Google Workspace complete), conflicts, a connection or transfer in progress. Read-only, for any domain. Not in ChatGPT.
undo_dns_change sites:write Reverses the last DNS change made through Hypamoon on a domain, within 30 days: a record set, replaced or deleted, an old record replaced by connect_domain, a cleanup, a zone file import or a restore. Again goes one change further back. Without confirm: true it only shows what it would do. Not in ChatGPT.

Solving a domain problem without asking us. Every error of a domain tool (connecting, transferring, DNS records) carries, next to type and user_message, a diagnosis (what is there exactly: which record, where it points, what answers there now if a quick HTTP check can see it) and a fix (one suggested action: a tool with arguments, or a portal link). The user_message is plain words the chat can pass on, like "portal.example.com still points to your old host (192.0.2.10). Replace it with your new site?". When an error is unclear, diagnose_domain names each problem with a fix.

Nothing live is overwritten silently. connect_domain on a subdomain that still points elsewhere, set_dns_record that replaces or deletes a live record, switch_domain_nameservers and transfer_domain (with dns: hypamoon) while the DNS here does not match what is live, all answer confirmation_required with preflight: what changes, what is live there now, and whether email is affected. Nothing changed; the fix is the same call with the confirmation flag (replace_existing, confirm_overwrite, ignore_preflight). Send it only after the user agreed. Every DNS change through Hypamoon keeps the previous state in the audit log, so undo_dns_change (or Undo last DNS change under Domains in the portal) can put it back for 30 days. In the portal, Check this domain runs the same diagnosis.

In the test project. Connecting a domain to a site through the nameservers is real in the test project too: connect_domain with site_id creates a real DNS zone, set_dns_record, import_zone_file and preflight_domain work on it, and the site goes live on the domain once the nameservers point here. The same limits apply as in live (a verified phone where that is required, three domains waiting at a time, five new ones a day, the ownership check). Without a site_id, or through the CNAME fallback (it carries a monthly surcharge, and a test project never pays), the connection is simulated: nothing changes in real DNS and activate_domain simulates the switch. Buying (register_domain) and transferring (transfer_domain) a domain always stay simulated in test. When the site moves to live with move_to_live, its real domain comes along.

A registered domain is always in the user's name, never ours. Complaints about a domain go to hello@hypamoon.com with "Abuse:" in the subject; we answer within five business days.

Watching and parking domains

Included in every plan: Free watches up to 3 domains, Online and Reachable up to 25, Business up to 100. Not in ChatGPT.

Tool Scope What it does
watch_domain sites:write Watches a domain the user owns, registered anywhere and not connected to a site. Checked once a day: DNS that points to a host where the domain is not set up (anyone could claim it there), spam or a changed title on the homepage, a changed host, expiry in 30 or 7 days, the certificate, and mail advice. Ownership is proven with the TXT record in ownership.challenge; a domain already active here or bought through Hypamoon needs none. park: page moves the domain to Hypamoon's nameservers and shows "This domain is reserved"; park: redirect sends visitors to redirect_to with a 301. A parked domain without mail gets null MX, SPF -all and DMARC p=reject. Included in the plan, up to a number of domains per plan.
list_domain_health sites:read The watched domains with status ok, warning or problem, the reason in one sentence, the findings, and whether a domain is parked. dns_cleanup lists every domain of the project on Hypamoon's nameservers, watched or not, that still holds records left over from a previous host, with the counts. Read-only.
get_dns_cleanup sites:read The cleanup suggestion for one domain on Hypamoon's nameservers: every record with a label (safe_to_remove, ask or keep), a group and the reason; the previous host (the address the control panel names point to); spf_advice; and the cleanups of the last 30 days. Names at the previous host are checked over https. Read-only.
clean_up_dns sites:write Removes the safe_to_remove records in one step, plus the ask records the user chose (include_record_ids); exclude_record_ids keeps some of the safe ones. Records labeled keep are never removed. In a chat it waits for one confirmation in the portal.

The owner of the project gets one mail per domain per day at most, only for a new problem.

Cleaning up after a move. A zone copied or imported from a previous host often carries what that host made by itself: the names of a hosting control panel (cpanel, whm, webdisk, webmail, cpcalendars, cpcontacts, autoconfig, autodiscover, ipv6, and the SRV and TXT records of _autodiscover, _caldav and _carddav), old _acme-challenge tokens, and names that still point to the old server. Once that hosting ends and its address goes to someone else, those names show their server. get_dns_cleanup labels every record:

  • safe_to_remove: a control panel name that points to the previous host, or an old certificate token. If the domain's own site or its email still runs at that host, or the name answers over https with a valid certificate, it becomes ask instead.
  • ask: a decision for the user, with the reason: another site at the previous host, ftp, a DKIM key of another mail service (Postmark, SendGrid and the like, or the control panel's default._domainkey).
  • keep: never in a cleanup. Microsoft 365 (MX to *.mail.protection.outlook.com, autodiscover to autodiscover.outlook.com, selector1 and selector2 DKIM, MS=), Google Workspace, everything Hypamoon manages (the site, Hypamoon Mail, verifications), SPF, DMARC and the domain's MX. How the SPF could be tightened is in spf_advice; a cleanup never changes it.

clean_up_dns removes the chosen records in one go. In a chat the user confirms it once in the portal, with the passkey step; in the portal it is the button under Domains, Cleanup suggestion. Before anything is removed, the records go into the audit log as a zone file; the answer has the same zone_file. For 30 days Restore removed records under Domains puts them back (only what is missing), and records put back are not suggested again unless the user chooses them. Records imported with import_zone_file carry their label in the record comment, and the import answer has the counts in cleanup. Not in ChatGPT: there the user cleans up under Domains in the portal.

Moving to Hypamoon

For a user who already has a domain and a site elsewhere.

Tool Scope What it does
import_zone_file sites:write Imports the zone file (BIND format) the user exported from their current DNS provider into the domain's DNS here. More complete than the automatic copy of connect_domain. What cannot be imported is listed. Every import keeps a restore point.
preflight_domain sites:read Before switching the nameservers: queries every known record at the current nameservers and at ours, and lists the differences. ready only when mail (MX, SPF, DKIM, DMARC), the domain itself and www match. Read-only.
restore_domain_dns sites:write Sets the DNS here back to a restore point (by default the previous DNS as it was found), and returns it as a zone file with the previous nameservers, so the move can be undone.
transfer_domain sites:write Moves the registration of the user's domain to Hypamoon with the transfer code from the current registrar. The agent must show the user the amount and the registry's terms and get an explicit yes first (accept_registry_terms, max_price_eur). The nameservers switch to the DNS here when it completes, or stay (dns: keep). Follow it with get_domain. Not available in ChatGPT.
switch_domain_nameservers sites:write For a domain registered or transferred here: points the nameservers to the DNS here (only when the preflight is ready, unless the user accepts the differences), or back to the previous ones.
import_site sites:write Copies an existing static site from the user's own domain into a site here, through the same checks as deploy_site. Only for a domain this project has proven it owns; someone else's site is refused. Same host only, robots.txt respected, within the site limits; the answer lists what did not come along (like forms or WordPress parts that need a server).
inspect_wordpress sites:read Step 1 of moving a WordPress site: reads what any visitor can see (the public content API and the home page) and reports pages, posts, media, forms, products, the permalink structure, and what cannot move (drafts, form entries, users, orders). Read-only, on any public address, limited per project and per site. Never uses or accepts a login.
import_wordpress sites:write Moves the published pages and posts of a WordPress site on a domain this project has proven it owns into a site here, through the same checks as deploy_site: same addresses (a /_redirects file for any that change), images within the site limits, one collection per form, products to the product collection where payments are available, and posts that do not fit in the private collection wordpress_posts. When the Hypamoon Connect plugin already sent the pages and posts of that site, it uses those (also when the content API is closed). Never touches the domain or DNS.
connect_wordpress data:write For data behind the WordPress login, without passwords or screenshots: a one-time code of 8 characters (15 minutes) that the user enters in the Hypamoon Connect plugin (Tools > Hypamoon). Not available in ChatGPT.
get_wordpress_inventory data:read The inventory the plugin sent. detail: compact (the default) gives the structure only: per kind of data the group, count, period, a few field names, personal data yes or no and why, relations, the current choice and the open suggestion; never values. detail: full adds every field name, statuses, collections and the selection rules. Read-only.
suggest_wordpress_selection data:write Helps the user choose: without items it returns the compact inventory; with goal and items (per key on or off with a short reason) it saves a suggestion. Never applied by itself: the user applies it in WordPress (Tools > Hypamoon > Move data), or says yes in the chat. Personal data is marked separately and never ticked automatically.
select_wordpress_import data:write Applies a choice, only with confirm: true after the user agreed (without it: a preview). from_suggestion applies the open suggestion; per key on or off, a period, statuses, fields to leave out, include_meta and names (for options). Personal data only with include_personal_data: true and confirm_text (the user's yes, word for word, kept in the audit log); otherwise a 409 with the question to ask. start_round: true lets the plugin start sending at its next check-in.
set_wordpress_embeds data:write Puts the project's chat window and forms on the connected WordPress site, without pasting code: the chat on every page (on or off, colour, position, title, welcome), and forms that post to collections with public_write (a missing one is created with an email per entry). Makes sure a public token allows the site. The plugin applies it when the admin opens Tools > Hypamoon; a form goes on a page with the Hypamoon form block or [hypamoon_form id="..."]. Without chat and forms it only reads. Not available in ChatGPT.

The safe order: connect_domain, import_zone_file (if the user can export one), lower the TTL at the current provider a day ahead, preflight_domain until ready, then switch the nameservers. restore_domain_dns and the previous nameservers are the way back.

Moving a WordPress site follows a fixed plan that the server gives the agent: inspect_wordpress and tell the user what moves and what does not; connect_domain without a site (nothing switches yet); prove the domain if asked; import_wordpress; show the new site at <slug>.hypamoon.app and wait for approval; connect_domain with the site and preflight_domain until ready; only after an explicit "go" in the conversation switch the nameservers (or transfer the domain); then tell the user what is left for them (export form entries, keep the old hosting for a week, move email first if it is hosted by the same company).

Data behind the WordPress login (drafts, private posts, custom post types with ACF fields, form entries, WooCommerce products, orders, customers, subscriptions, users) comes along with the Hypamoon Connect plugin, never with a password: connect_wordpress gives a code, the user enters it in the plugin, the plugin sends an inventory (counts and field names, no content), the agent asks what the user needs and shows the inventory (get_wordpress_inventory), proposes a selection and saves it (select_wordpress_import). Personal data stays off until the user explicitly says yes. The user clicks Send in the plugin; the data lands in private collections (wp_posts, wp_pages, wp_orders, wp_customers, wp_form_<name> and so on), and a later round adds more without connecting again.

Since plugin 1.2.0 the inventory also lists post types that are not public (public: false, like hours or applications), the plugin's own database tables (table:<name>, with their columns, private and off until chosen) and settings from wp_options (options, chosen by name with names; names that look like a password, key, token, secret, salt or auth setting never move). With include_meta on content or users, every other field comes along too, and fields that point to a user or a post (like opdrachtgever_id) become relations with the collection, the wp_id and the document_id of the target.

A WordPress portal with logins (members, clients, candidates, hours) becomes a Hypamoon app: Hypamoon is the backend, so no export scripts and no other backend. deploy_app with auth and the portal's roles, apply_app_migration for a D1 schema of the imported data, import_app_users to turn wp_users into app users (no passwords; silent first, invite later), and set_table_owner_column so each person sees only their own rows. get_advice with type: wordpress_portal gives the full plan.

The same connection puts Hypamoon on the WordPress site itself: set_wordpress_embeds turns on the chat window and chooses the contact forms. The plugin picks this up when the admin opens Tools > Hypamoon and stores it, so the chat and the forms keep working after the plugin's token expires. Visitors talk to the public chat and form endpoints with the project's public token; form entries land in the collection and are emailed to the owner.

Data and forms

A project keeps its own data in collections: lists of JSON documents that the agent shapes itself, without fixed components like "form" or "knowledge base". The contact form on a site is the first use. Ask for a contact form and the agent calls create_collection with "access": {"public_write": true} and "triggers": {"email_on_insert": true}, then get_form_snippet, puts the HTML on the page and calls deploy_site again. form.js shows a small bot check in the form and posts the fields to the collection with the project's public token (the same token as the chat widget, so the site's origin must be in its allowed_origins). A visitor's entry is stored and, in live mode, emailed to the project's billing email address with Reply-To set to their email field; document.created goes to your webhooks, even in test mode. Read the entries with query_documents.

To send that email somewhere else, set email_routing on the collection: {"to": ["info@example.com"], "rules": [{"field": "subject", "equals": "Jobs", "to": ["jobs@example.com"]}]}. The first rule whose field matches the entry wins, then to, then the billing email. At most 5 addresses per collection. An address must be confirmed before it gets anything: the billing email, a project member's email and addresses on a domain connected to the project count right away; any other address gets one email with a link (live projects only) and receives nothing until it clicks. Until then its entries go to the billing email. email_routing.recipients in the result shows each address with status confirmed or pending. The same settings are in the portal under Data, "Email on new entries". ChatGPT does not see email_routing while the OpenAI review of the app runs.

For knowledge, create a collection with "access": {"assistant_read": true} and add documents with insert_document: every new chat gets the newest of them (8 kB at most) in the assistant's instructions. Only documents added through the API reach the assistant, never form entries. The receptionist will follow later.

Limits: 20 collections per project on Free (50 on Online, 100 on Reachable, 200 on Business), 10,000 documents per collection, 64 kB per document; a form accepts 30 entries per hour per visitor and 500 per day per collection.

Secrets

These tools are not currently offered in ChatGPT.

tool scope what it does
request_secret project:write Asks the user for a secret an integration needs, like their online store's API key. Takes name, purpose, allowed_hosts (exact host names, https only, each optionally with a path prefix like api.example.com/v2) and optionally auth_scheme (bearer, basic or header with header_name). Returns a one-time link to the portal (/secrets/new), valid for 30 minutes. The user signs in, sees what is asked and where it may go, pastes the value and confirms.
list_secrets project:read The secrets with name, purpose, allowed_hosts, auth_scheme, status (active or pending) and last_used_at. Never the value.
delete_secret project:write Revokes a secret immediately: the stored value is erased and an open link for that name stops working. Revoking twice is harmless.

The rule: a secret never passes through the chat. Chat history and model context live with the assistant's provider, a prompt injection can make an agent repeat what was said earlier, and assistant stores do not allow a connector to ask for keys or passwords. So the agent asks with request_secret, the user pastes the value in the portal, and the agent only ever gets the name and the status back. There is no get_secret.

Only an owner or admin of the project can fill in a link, and when the link came from an OAuth connection, only the user behind that connection. The page quotes the purpose as what the assistant says, shows which key or connection asked and when, and puts the registrable domain of every host in bold, so a look-alike address stands out. Shared hosting, tunnel and request-inspection hosts (like webhook.site, ngrok or netlify.app) are refused. Asking again for the same name gives a new link and voids the old one; the current value keeps working until the user confirms the new one, and if the hosts change, the page shows old and new side by side with a warning.

Using a secret: server code (a webhook receiver, a function) calls POST /api/v1/secrets/{name}/proxy with the API key and a body like {"method": "GET", "url": "https://api.example.com/v1/orders"} (optional headers and body). Hypamoon adds the secret (Authorization: Bearer, Authorization: Basic, or the named header) and returns status, a few headers and the body (body_encoding is utf8 or base64). Only https to a host that is exactly on the secret's list (and under its path prefix, if it has one), no redirect to another host or outside the prefix, at most 60 requests per secret per minute, 1 MB each way, 15 seconds for the whole request including redirects. Responses come back uncompressed and whole: the proxy asks for identity, refuses Accept-Encoding, Range and If-Range from the caller (422), and does not pass on a compressed response (502).

The protection is that the secret only ever goes to the host the user chose. As an extra safety net, if the response contains the secret (plainly or encoded: JSON or HTML escaped, url-encoded, base64, double base64, hex, UTF-16 or reversed), the whole response is withheld and you get 502 upstream_error. Do not rely on it: a service can always return the secret in a form nobody can recognize. A path prefix also refuses ; and %25 in the path. Never call the proxy from browser code: the API key does not belong in a page.

REST, same rules as the tools: GET /api/v1/secrets, POST /api/v1/secrets/requests, DELETE /api/v1/secrets/{name} and the proxy above. They are not in the OpenAPI document yet.

Payments on sites (early access)

Payments are in early access: everything below works, and some parts are still limited. Whether a project takes real payments or test payments is one answer, the same everywhere: get_payments_status returns availability (early_access) and mode (live or test), with a one-line note to pass on. A test project always pays in test mode. The full guide, with the checkout, the quote and the shop settings, is at Payments.

A buy button on a site the user builds. The user connects their own Mollie account in the portal and is the seller: the money of every sale goes straight to their account, and Hypamoon never holds it. Hypamoon takes an application fee per successful payment, only after the owner has agreed to it in the portal. Outside Europe the portal connects the seller's own account at a second payment provider; the tools and the checkout work the same.

tool scope what it does
connect_payments project:read Returns the link to the Payments page of the portal, where the owner connects Mollie (or creates a Mollie account), picks a profile and agrees to the fee. Only the owner can do this, in the portal. Not offered in ChatGPT.
get_payments_status project:read connected, profile, fee_consent, test_mode, availability, mode, note, ready and missing. Reads only Hypamoon's own records.
list_payment_profiles project:read Only for a provider with several payment profiles per account: the profiles of the connected account, the default profile, and per site the profile its payments go to (profile null: the default; fallback true: the site's own profile is gone or blocked, so the default is used). Not offered in ChatGPT.
set_site_payment_profile project:write Only for a provider with several payment profiles per account: site_id and profile_id send one site's payments to that profile; profile_id default removes the choice. The profile must be on the connected account and not blocked. Owner only. Not offered in ChatGPT.
create_product_collection data:write Creates the product collection (default name products), or makes an existing collection with that name the product collection. One per project; calling again is harmless. Public read on by default, public write always off. Where the shop settings are on, this is create_collection with role: "products", and this name is a hidden alias until 12 December 2026.
get_shop data:read The shop settings with every default filled in, the seller as buyers see them (from set_company_details) with what is missing, feed (the public product feed for Google Merchant Center, XML and JSON) and buyer_orders_path (/_hypamoon/orders, where buyers see their orders). Not offered in ChatGPT. Where available.
configure_shop project:write Changes the shop settings: send only what changes, null clears a field. Currency, prices with or without VAT, shop name, contact email, links to terms, privacy and returns, order number prefix, extra checkout questions, the right of withdrawal, stock rules, buyer emails, download links (days and times), the orders page for buyers and the product feed (link pattern, brand). Not offered in ChatGPT. Where available.
get_invoice data:read The invoice of a paid order in the seller's name and its credit notes: number, lines, VAT per rate and country, totals, notes, pdf_url and buyer_url. Says why there is none (no VAT setting, VAT calculated by the payment provider, seller details missing). Not offered in ChatGPT. Where available.
create_invoice data:write Makes the invoice of a paid order that has none yet, with the next number of the seller's gapless series. Not offered in ChatGPT. Where available.
export_orders data:read A CSV of invoices and credit notes over a period: invoices, vat (per rate and country) or oss (sales with the VAT of other EU countries). The seller files their own returns. Not offered in ChatGPT. Where available.
issue_gift_card data:write Issues a gift card without a payment (sold at the desk, or as an apology), with amount_cents and a reason; with email the person gets it as a PDF, now or on send_on. Valid at least 24 months. Owner or builder only. Not offered in ChatGPT. Where available.
check_gift_card data:read A gift card by code or id: balance, valid until, status and its full history. Not offered in ChatGPT. Where available.
redeem_gift_card data:write Takes amount_cents (or "all") off a card; part is fine. Idempotent with idempotency_key, never below zero. Owner or builder (a viewer only at the desk, if allowed). Not offered in ChatGPT. Where available.
list_gift_cards data:read The gift cards of the project, newest first, with status, limit and before. Only the last four characters of each code. Not offered in ChatGPT. Where available.
void_gift_card data:write Cancels a card with a reason; the balance goes to zero and this cannot be undone. Owner or builder only. Not offered in ChatGPT. Where available.
render_pdf data:read Turns one HTML document with its CSS into a PDF in a sandbox: no JavaScript, nothing loads from the internet. With site_id the files of that site (stylesheets, images, fonts by path) are put in and its colors and fonts are ready as --hm-* CSS variables. At most 2 MB of HTML and 20 pages, 10 seconds. Returns pages, bytes and blocked_resources; include_pdf: true adds the PDF as base64 (up to 1 MB). Nothing is stored. The same as POST /api/v1/pdfs and env.HYPAMOON.renderPdf in an app. Not offered in ChatGPT.
get_checkout_snippet data:read The html of a buy button for one or more product ids, plus the script tag (https://api.hypamoon.com/checkout.js). Uses the project's public token, like forms. Optional shipping, button_label, return_path, and where available variant_id per product.
get_cart_snippet data:read The drop-in cart: one script line (https://api.hypamoon.com/cart.js), <hypamoon-cart> for the header and <hypamoon-add-to-cart> per product (a variant picker appears for a product with variants), plus an example of window.Hypamoon.cart for a cart in your own design. Every total comes from the quote. Optional products, shipping, return_path, floating, public_token. The default for a shop with more than one product. Not offered in ChatGPT. Where available.
create_download_upload_urls data:write Upload links for files sold as a download (name, sha256, bytes per file, at most 20). PUT the bytes, then put the returned file objects on the product with "delivery": "download". The files are never public; buyers get links that expire, after ticking that they lose their right of withdrawal. Not offered in ChatGPT. Where available.
list_orders data:read The orders, newest first, with status (open, paid, shipped, canceled, refunded), items, amount, customer and shipping address, and where available number, fields, amounts, amount_refunded and tracking. Filter with status; page with before.
get_order data:read One order. An open order older than a minute is first checked with the payment provider.
update_order_status data:write shipped (only a paid order) or canceled (only an open order). Moves no money. Where available, tracking_number and tracking_url with shipped: the buyer gets a shipping email.
refund_order project:write Refunds a paid or shipped order from the user's payment account: everything that is left, or where available only amount_cents. Needs confirm: true, after an explicit yes from the user; without it nothing happens. Calling it twice refunds once. The answer has stock_restore (see below). Not offered in ChatGPT or Claude.
get_refund_link project:write A link to one order in the portal with the refund form open (order_id, or order_number like QS-1001; optional amount like 12.50 and reason to fill in). Read-only: it moves no money and prepares nothing. The user checks the amount there and confirms the refund themselves; the portal refunds through the same code as refund_order. Not offered in ChatGPT. Where available.

A product is a document in the product collection: name, price_cents (a whole number of cents, VAT included), optional stock (leave it out for unlimited; it goes down when an order is paid) and optional active (false takes it off sale). The button sends only product ids and quantities. The server reads the amount from the product collection, takes the fee from the owner's consent and the account from the connection; a site cannot set any of them.

The checkout (POST /api/v1/public/checkout?token=hm_pub_..., done by checkout.js) asks the visitor for an email address (and an address with shipping), runs the same bot check as forms, and sends the visitor to the payment page. Afterwards the visitor comes back to the site with ?hypamoon_order=<id>. Each click carries an idempotency_key: sending the same checkout twice gives one order. Limits: 20 checkouts per network per hour and 300 per project per day. A project with a site that was taken offline cannot take payments at all. Only products added with the API or MCP can be sold.

When a payment succeeds the order becomes paid, the stock goes down, the buyer gets an order confirmation by email (in the language of the site, with the lines, the total, the seller and the right of withdrawal; it is always sent, once; who it comes from: see "Who emails to buyers and app users come from" below), the owner gets an email (if two buyers paid for the last item at the same time, both orders are paid and the email says to check the stock) and endpoints subscribed to order.paid get the order. Where the shop settings are available, create_webhook also takes order.shipped, order.refunded, order.canceled and stock.low. Orders also appear as documents in a private collection orders, which can never be made public; that copy is for reading, the order itself stays the source.

When a paid order is refunded in full, the stock that went down for it goes back up, once. That also happens when the owner refunds in full in the dashboard of their own payment account (a partial refund changes no stock). Only what really went down comes back: if two buyers paid for the last item at the same time, the second order only returns what was left. Products without stock (unlimited, like a course or a download) are never touched, and a product that was deleted or lost its stock in the meantime is skipped. A shipped order gets its money back but not its stock, because the item is gone. refund_order and POST /api/v1/orders/{id}/refund return the order with an extra field stock_restore: restored (true or false), reason and items (the product_id and quantity put back by this call). reason is restored, already_restored (an earlier call or the dashboard did it), shipped, nothing_to_restore (no product in the order had a stock), not_lowered (the stock did not go down when it was paid, so nothing goes back; correct it by hand if needed) or failed (the money is refunded, but the stock could not be updated; calling again tries once more). It is left out for an order of paid access.

No tool ever returns a payment link: a payment is started by a visitor on the site, never in the chat. In ChatGPT these tools are for physical goods the user sells on their own site, and the connect step stays in the portal.

Test projects pay in test mode; whether a live project takes real payments, get_payments_status says with mode.

REST, same rules as the tools: GET /api/v1/payments, POST /api/v1/payments/product-collection, GET /api/v1/orders, GET and PATCH /api/v1/orders/{id}, POST /api/v1/orders/{id}/refund, GET and PATCH /api/v1/shop, and the public POST /api/v1/public/cart/quote and POST /api/v1/public/checkout. They are described in Payments, not in the OpenAPI document.

Built on payments, so it runs in test mode the same way. The server instructions have the fixed recipe "Turning a tool into a business" (steps a to g: look at what is there, explain what the user does themselves, address and domain, product and price, paid access on the site, a test purchase in test mode, live only after an explicit yes).

The user is always the seller, on their own Mollie account (Mollie is for sellers in the EU/EEA, the UK and Switzerland). Hypamoon takes only the fee the owner agreed to. A buyer pays once or per month or year, checks "I want access now and I understand I lose my right of withdrawal", and gets a personal sign-in link by email (it works once, for 30 minutes). The site then only lets in visitors with a valid pass; revoking one takes effect within seconds.

Who emails to buyers and app users come from. Sign-in links and invitations of an app, the emails of paid access and the emails of a shop (order confirmation, shipping, refund, downloads, gift cards) follow one rule. When the site runs on the project's own domain and that domain is a verified sending domain of the Mail module (which is on and not paused), they come from that domain with the name of the shop or app, like "Black Bird Bakery" <orders@blackbirdbakery.nl> for a shop and noreply@ for paid access and apps, in the look of the site; they then count for the Mail module and follow its recipient rule: a buyer or the holder of an access pass only gets such an email about their own order, gift card or pass, and an app user is known through the app. Otherwise (no verified sending domain for that domain, or the site is on hypamoon.app) they come from "Hypamoon", with the name of the shop or app in the subject and text and "on behalf of" at the top, without the site's colours. Replies always go to the seller or the owner. If sending from the own domain fails (daily limit, a provider outage), the same email still goes out from Hypamoon.

tool scope what it does
check_business_readiness project:read Step a of the recipe: the sites (address, paid_access, password, unlisted), the access products, and the payments status with subscriptions_ready and missing. With a shop (where available, not in ChatGPT) also shop: downloads (every download file uploaded), buyer_account (the orders page for buyers) and feed (the product feed, with products left out and why). Verification and mode come from get_project.
create_access_product data:write A product with type: "access" in the product collection: name, price_cents (set by the user), interval (none, month or year) and site_ids. Does not lock the site yet. In ChatGPT the answer leaves the price out.
set_site_access data:write Paid access on or off for one site. On: within seconds visitors without a pass see a short page with a buy button and "I already paid, send me my link". Not on a site with a password.
list_access data:read The passes, newest first: status, kind, sites, valid_until, the buyer's email and name, and the subscription (pending, active, past_due, canceled, ended, paid_until). Plus counts, and revenue (not in ChatGPT).
revoke_access_pass data:write Ends one buyer's access within seconds and stops a subscription on it. Gives no money back (that is refund_order). Needs confirm: true.

refund_order on an order of paid access also ends the pass, and stops a subscription. A chargeback ends the subscription at Mollie and never collects again. A failed collection sends the buyer an email with a link to pay; every email about a subscription has a link to cancel it. In ChatGPT the same steps apply, without amounts in tool descriptions or answers and without any link that starts a purchase: buying happens on the user's own site.

Shipping an app to the App Store and Google Play

The server instructions have the fixed recipe "Shipping an app to the App Store and Google Play" (steps a to f: readiness, the user's own accounts, privacy answers, listing, export, and that building and submitting in the cloud comes later).

The app is always published under the user's own developer account (Apple guideline 4.2.6). No tool signs in to Apple or Google, and no tool asks for a password, API key or certificate. App Privacy and Data safety exist only in the consoles: the tools prepare the answers, the user submits them.

tool scope what it does
check_store_readiness sites:read Per store ready with blocking, warnings and next_steps, every check (online, complete content, privacy policy URL, support contact, a 1024x1024 icon, native value for guideline 4.2, no external payment pages for digital goods, no advertising trackers without permission), native_value (present and can add), and accounts (the user's own steps; fee and enroll_url are left out in ChatGPT).
generate_store_listing sites:read Draft texts within each store's limits (Apple name, subtitle, promotional text, description, keywords, categories; Google title, short and full description, category), review notes, and the screenshots and graphics per store. In English; source_language says when the site is in another language.
get_privacy_answers sites:read The answers for Apple App Privacy and Google Data safety from what the site and project use on Hypamoon (forms and fields, chat widget, bot check, payments, paid access, push registration, sign-in, location, third-party scripts), each with its reason, plus not_collected and a click-by-click list per console. Cautious: when in doubt, it counts as collected.
export_app_project sites:write A Capacitor project as a zip (download link valid for 15 minutes): it opens the site at its domain or hypamoon.app address, with icon and splash from the site, an app id proposal, an offline screen and push registration, plus a README for Android Studio and Xcode. No secrets in the zip.

REST, same rules as the tools: GET /api/v1/sites/{id}/store-readiness, GET /api/v1/sites/{id}/store-listing, GET /api/v1/sites/{id}/privacy-answers and POST /api/v1/sites/{id}/app-export. The app registers its push token at POST /api/v1/public/app-devices (no key: an app cannot keep a secret); sending notifications is a later phase.

From the test project to live

New projects since October 2026 are one project: sites, data and apps are always real, and there is no separate go-live step. Phone, outbound calls, SMS, email to other people, payments and buying domains become real the first time the user asks for them, once the project is verified for that function (see Going live). Older projects can still have a separate test project. Merging it into the live project (a Merge test project button on that page, with a dry run that first shows what moves) is rolling out. Until then, move_to_live moves sites from such a test project to the live project. Not available in ChatGPT for now.

tool scope what it does
move_to_live project:write Moves the chosen sites (sites, by id or slug; default all) to the live project with every version, label, password and address; the collections they use (a site uses a collection when its files name it) with all documents; an app on the site with its sign-in users, files and database; the agents on those collections (they arrive paused); and domains connected for real. The test form token (hm_pubtest_) in the files is replaced by a new live token with the same allowed origins, as a new version labelled "moved to live". A collection that a site staying in test also uses is refused, unless shared_collections is copy (the live project gets its own copy). The answer lists what moved, what stayed, warnings and the next step.
undo_move_to_live project:write Within 24 hours: moves everything of a move back to the test project (move_id, default the latest). The sites get the test token back as a new version. A copied collection stays in live. After 24 hours, support can still move it back from the move log.

Only an owner can move to live (through a connected chat or the portal; an API key without a person behind it cannot), and only when the live project is verified: otherwise 403 verification_required lists what is missing (company details, a payment with SCA, a verified phone). The plan limits of the live project apply. The database part is one transaction, and a move log keeps the rows from before for the undo.

The chat connection stays on test. A chat connected through OAuth points to the project the user chose when connecting. After the move, the sites are in live, and a test connection no longer sees them. The user reconnects and chooses the live project, or switches the connection in the portal under Connect your chat (the Switch to live button under Your connected chats, with a fresh confirmation of the access). The portal also has a button: Sites & apps, Move to live.

Going live: there is no switch

Wanting a function is enough; what we check is who asks and whether they are verified. When a chat, an app or an agent asks for a phone number, a call, an SMS, an email to someone else, a payment or a domain:

  1. Who asks. The scope of the key or connection, the role in the project, any access limit on the member or key, and the confirmation that money and anything that cannot be undone already need.
  2. Verified for this. Every function needs the company level (or the personal level): details, a spending cap, a payment method or balance, and a verified phone. On top of that: outbound calls need the full level; SMS needs the business register check (or approved identity documents); payments need your own payment provider connected; buying a domain needs the business register check (or the documents).

When both are in place it is real right away, and stays real. When something is missing, the request answers 403 verification_required:

JSON
{
  "type": "https://hypamoon.com/docs/errors/verification_required",
  "detail": "Sending text messages needs a verified phone number on your account. Nothing was done.",
  "function": "sms",
  "missing": ["a verified phone number on your account"],
  "next_step": "Ask the user to finish this at https://app.hypamoon.com/verify-phone, then call this again with the same arguments.",
  "verify_url": "https://app.hypamoon.com/verify-phone"
}

Nothing was sent, ordered or charged. Give the user the link; when they are done, call the same tool with the same arguments (and the same Idempotency-Key).

What never becomes real: a test key (hm_test_), a connection limited to test, and review accounts. simulate_call tries a call without cost. Spending caps, the balance and the growth limits still apply. In ChatGPT this applies after the app review; until then a function there is simulated until it is in use.

Advice for beginners: blueprints and a readiness check

For someone who has never built a shop, a booking page, a restaurant site or a course, "make me a webshop" easily ends as a nice-looking mockup: a form that saves nowhere, a cart without a checkout, a password in JavaScript. Two tools and a short block in the server instructions keep the agent honest, without turning the chat into a questionnaire.

tool scope what it does
get_advice project:read The blueprint for one kind of application: webshop, bookings (appointments), restaurant or course_membership (events, portfolio, internal tools and SaaS come later). Optional country (an ISO code like NL, or a name; default the Netherlands) and audience (consumers, businesses, both). Returns ask_first (questions with why, a default, and blocking for the two or three that stop a working result), build_plan (the Hypamoon tools in order, including an honest order request while payments are not on), go_live_checklist for the region (EU in detail, UK and US basics, a general fallback; each point usually_required or recommended with why and how_on_hypamoon), pitfalls, and summary_markdown. Guidance, not legal advice.
check_readiness sites:read Reads the live files of a site (site is its id, slug or address), detects the kind or takes type, and checks what can be checked automatically: terms, privacy, returns and contact pages with company details; forms that save to a real collection (not nowhere, not only mailto, not localStorage); buy buttons and a cart that lead to a real checkout; prices with currency; no placeholder text; no test wording on a live project; internal links that work; a 404 page; no password checked in JavaScript. Also findability: a title (10 to 60 characters) and meta description (50 to 160) per page, exactly one h1, html lang, Open Graph title, description and image, alt text on images, images over 500 KB, a viewport meta and a favicon; and structured data (JSON-LD): existing blocks must parse and have their required fields, and without one it offers the snippet for the kind. Returns works, missing (each with a plain explanation and next_step with the tool) and check_with_owner (what only the owner can confirm).

Search engines (9 October 2026). Every public site gets /robots.txt and /sitemap.xml from the sites worker when it has none of its own (its own file wins). robots.txt welcomes search engines and the crawlers of AI search and answer services, and disallows the crawlers that collect data for AI model training (each by its published name; the list is in the robots.txt itself), with a Sitemap line. An unlisted, password, paid, offline or AI-doubted site gets Disallow: / and X-Robots-Tag: noindex. Every html page has a Link: <...>; rel="canonical" header on the primary host, and a site with an active custom domain sends <slug>.hypamoon.app to it with a 301 (except /.well-known). After a deploy of a public site in a live project (or a test site on an active custom domain) the changed pages are sent to IndexNow, at most once per 10 minutes and 30 times a day per site.

tool scope what it does
verify_search_console sites:write Google Search Console verification with the user's token (site_id or domain, method, token). dns adds the TXT record google-site-verification=<token> to a domain connected with nameservers (a domain property); meta adds the meta tag to /index.html as a new version labelled "search console verification" (a URL prefix property). Refuses unlisted, password, paid and offline sites. Returns property, next_steps for Search Console and the sitemap to submit. REST: POST /search-console/verify.
get_site_stats sites:read Page views per day, the top pages and origins (direct, internal, search, social, email, shortener, external) for a site of this project, last days (default 30, at most 90), from Analytics Engine without cookies; bots left out. REST: GET /sites/{id}/stats.
fetch_site_page sites:read Fetches one path (path, default /) or a full url of a site or app of this project live, through Hypamoon's server, and returns status, the main headers (content-type, cache-control, location, x-*; never set-cookie), final_url, the redirects followed and the start of the body as text (20 KB, truncated). Only the site's own addresses and its connected domains; redirects only to addresses of this project (else redirect_blocked), at most 3. GET or HEAD, 10 s per hop, private addresses refused, 30 per minute per project. The way to prove a deployed site or app endpoint works live. REST: GET /sites/{id}/fetch.

Never mandatory for the user. The agent may call get_advice quietly to inform itself. It asks at most the questions marked blocking, uses the defaults for the rest and says briefly which ones, and builds; "just build it" is enough. The checklist and check_readiness are an offer ("Want me to check what you might want to arrange before going live?"), never a condition for a deploy, and their texts say "before you go live, you might want to", not "you must".

Build hints on every deploy. deploy_site returns warnings with code build_hint (and hint, message, paths, next_step) when it sees a form without a real target (no collection, only mailto, only localStorage, or a collection that does not exist), localStorage used as a database, a password checked in JavaScript, a button to # or javascript: that promises to buy, order or book, a cart without a checkout, or test wording or the test token on a site of the live project. They are advice: the deploy has succeeded. A theme toggle in localStorage and a form to a collection are fine. A partial deploy that does not include a page's own script says nothing about that page's forms and buttons.

Rules for the agent itself (in the server instructions): everything a visitor submits is saved in a collection; localStorage is not storage; no fake login; no payment or order button that goes nowhere; no invented reviews, customers or prices; and at the end, tell the user in plain words what really works and what does not yet.

REST, same rules as the tools: GET /api/v1/advice?type=&country=&audience= and GET /api/v1/sites/{id}/readiness?type=. ChatGPT does not get these tools, the build hints or the new block in the instructions while the app is in review.

Agents

An agent keeps working for the project after the chat. It has a trigger, plain instructions, a fixed set of actions and limits, and every run is logged.

tool scope what it does
create_agent project:write plus the scope of each action Creates an agent: name, instructions, trigger, actions, optional limits and status.
list_agents project:read The agents with their trigger, actions, limits, status and pending_approvals.
get_agent project:read One agent with its five newest runs and their actions.
update_agent project:write Changes name, instructions, trigger, limits or status; actions replaces the whole set.
pause_agent project:write Stops new runs until update_agent sets status back to active.
delete_agent project:write Deletes the agent: no new runs, pending approvals expire, its own key is revoked.
list_agent_runs project:read The log: per run the trigger, a short thought, every action with its status, the outcome and the tokens used. Kept for 90 days.
approve_agent_action project:write plus the scope of the action Approves or rejects an action that is waiting for the owner. An SMS or a call can only be approved in the portal; here it can only be rejected.

Triggers: document.created in one collection (a form entry, for example), call.completed, conversation.closed (a chat conversation once its summary is ready), or schedule with every_minutes (at least 15). A run starts within a few minutes of its trigger.

Actions, default deny (only what you list is allowed):

action needs default mode what it does
read_document data:read auto Reads a document of the trigger's collection, or of the collections you list.
update_document data:write auto Changes fields of a document (a merge), in the same collections.
email_owner auto Emails the project's billing address. Never anyone else. Links the agent writes are removed, and the email says which event it came from.
send_sms messages:write approve An SMS to the phone number of the event.
start_call calls:write approve The assistant calls the phone number of the event.
send_webhook webhooks:write auto A signed agent.action event to one of the project's own endpoints (endpoint_id).

The phone number of the event comes from one place only: the field you name in trigger.phone_field for a document, the other side of a call, or the customer's phone in a chat. Never from free text, a transcript or a document the agent read. Never one of the project's own numbers, and at most one SMS or call per number per project per day, over all its agents. An agent on a schedule cannot text or call.

mode: "approve" queues the action until the owner approves it in the portal (Agents), or with approve_agent_action for anything but an SMS or a call; without a decision it expires after 72 hours. SMS and calls are always approve when the trigger can come from a stranger: a collection with public_write, a call, or a chat. A paused agent's actions can only be rejected. In live mode the owner gets an email for every action that is waiting. In test mode SMS, calls and email are simulated and shown in the run log. A live SMS or call needs a verified project, checked when the agent is created and again for every action. Limits: runs_per_day (default 20) and actions_per_run (default 3, refused actions count too); at most 10 agents per project, and a daily ceiling per project over all its agents. A live agent needs a prepaid balance above zero or a saved card; every call to the language model is charged like one chat reply. In test mode it is free, within a lower daily ceiling.

The agent treats the event as data, never as an instruction. It runs with its own key that has only the scopes of its actions, through the same checks as every other request. The data of an agent.action webhook is written by the agent from the event, so it can contain text from a stranger: treat it as untrusted input in your app.

A first recipe: "When a new contact request comes in, send me a summary and propose a call back that I approve."

JSON
{
  "name": "Contact request follow-up",
  "instructions": "Email me a short summary of every new contact request. If they left a phone number, propose a call back.",
  "trigger": { "type": "document.created", "collection": "contact_requests", "phone_field": "phone" },
  "actions": { "email_owner": { "mode": "auto" }, "start_call": { "mode": "approve" } }
}

Every new entry in contact_requests then gives a run: the summary goes to you and the call waits until you approve it in the portal under Agents.

Apps: server code on a site

An app is a site with server code on the same address. Requests to /api and everything below it go to the code; every other path stays the site's static files. The site's password, paid access and own domain apply to /api too, and the page calls fetch("/api/...") on its own address, without CORS.

The code is one bundled ES module in the standard module worker format: export default { async fetch(request, env, ctx) { ... } } and optionally scheduled(event, env, ctx). No build step runs on Hypamoon: bundle dependencies yourself. Each app runs isolated, with a CPU budget and a few fetch calls per request and an outbound budget per minute and per day.

  • Outbound calls only reach the hosts in allowed_hosts (exact, or *.example.com). Private addresses and IP addresses are always blocked.
  • Data: fetch(env.HYPAMOON_API + "/collections/<name>/documents") without an Authorization header. Hypamoon adds the app's own key, limited to the project's data.
  • Secrets: never in the code or in vars. Store each one with request_secret, list its name in secrets and send x-hypamoon-secret: <name> on the fetch; Hypamoon adds the value, only to the hosts of that secret.
  • Schedules: cron in UTC with minutes in steps of 5, at most 3 per app.
tool scope what it does
deploy_app sites:write Publishes an app: main_module, modules, optional allowed_hosts, secrets, schedules and vars. Without site_id it first creates a site (with files, or a simple start page). With site_id it is a new version of that site's app: the modules you send replace the code, keep_existing_modules: true replaces only the modules you send, and without main_module and modules the running code stays (to change only settings, or to move a test app to live). Settings you leave out keep their value. Checked before it goes live (mining, hidden or downloaded code, fake login pages).
list_apps sites:read The apps with their url, api_url, version, allowed hosts, secrets, schedules and limits.
get_app sites:read One app with its kept versions (the newest 10) and its newest errors.
get_app_logs sites:read The newest requests, errors, scheduled runs and outbound calls of the last 7 days. Never headers, cookies, query strings or bodies.
rollback_app sites:write Puts a kept version live again with the settings it had; without version the previous one.
delete_app sites:write Deletes the code, versions, schedules and key. The site stays.

A small example: a contact form in index.html posts to /api/contact. The app reads the form, looks up the weather for the visitor's city at api.weatherapi.com with x-hypamoon-secret: weather_key, stores the entry with POST env.HYPAMOON_API + "/collections/contact_requests/documents", and answers with JSON.

Node.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === "/api/contact" && request.method === "POST") {
      const form = await request.formData();
      const city = String(form.get("city") || "Delft");
      const weather = await fetch("https://api.weatherapi.com/v1/current.json?q=" + encodeURIComponent(city), {
        headers: { "x-hypamoon-secret": "weather_key" },
      }).then((r) => r.json());
      await fetch(env.HYPAMOON_API + "/collections/contact_requests/documents", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ data: { name: form.get("name"), email: form.get("email"), city } }),
      });
      return Response.json({ ok: true, temperature_c: weather.current?.temp_c ?? null });
    }
    return new Response("Not found", { status: 404 });
  },
};

Apps with sign-in, uploads and a database

An app can give the people who use it their own sign-in, keep private data per person, let them upload files and keep its own database. These tools are in tools/list alongside the other app tools.

  • Sign-in for the people who use the app. deploy_app with auth (signup: invite or open, roles like ["admin", "member"], default_role). People sign in on the site itself at /_hypamoon/auth/login with a one-time link by email (a bot check guards the form). Hypamoon keeps only their email address and name; there are no passwords. The page can call GET /_hypamoon/auth/me, and signs out with a form that POSTs to /_hypamoon/auth/logout.

  • A code next to the link. The same email has a 6-digit code; after sending, the sign-in page shows a code field right away (valid 10 minutes, 5 attempts, only in the browser that asked). An invite has a code too, for another device ("I have a code").

  • Account page. /_hypamoon/account on the site: name, email (changed with a code to the new address), devices, sign out everywhere, download my data, and delete account (after 7 days, with an email). Link to it from the app.

  • auth.protect and auth.session_days. protect: "site" keeps every page and file behind the sign-in (the default for a new app with signup invite; existing apps stay open until they set it), none keeps pages public; /api stays with the code. session_days (1 to 90) shortens how long a sign-in lasts after the last visit. Leaving either out keeps the current setting.

  • Shorter sign-ins for the admin role. Whoever has the admin role (the first of roles) signs in again after 14 days without a visit, everyone else after 90 (7 in a sensitive app). auth.admin_session_days (1 to 90) sets it per app, never longer than session_days. A change applies from the next sign-in: sessions that are already open run out as they were.

  • Not in a frame on another site. The pages of a site whose app has sign-in get Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so another site cannot load them in an iframe and trick a click (clickjacking). To allow a partner to embed it, set your own frame-ancestors (or X-Frame-Options) in /_headers; then Hypamoon sets neither. A site without sign-in stays embeddable as before.

  • auth.sensitive and auth.retention_months (not available yet). Not switched on yet, and not for ChatGPT. When it is on, sensitive: true is for health, money, children or HR: sign-ins last 7 days and the code gets no email address. retention_months (1 to 120) is how long someone may be inactive before they get an email to sign in and keep their account; after 30 more days without a sign-in their account and data in the app are deleted. Default 24, or 12 for a sensitive app; your own retention_months always wins. The last admin of an app (the first role) never gets that email and is never deleted: of the admins that are not blocked, the most recently active one always stays. Blocked people and buyers with a valid entitlement are skipped too.

  • Passkeys (not available yet). Not switched on yet: when it is on, the sign-in page also shows "Sign in with a passkey", and after the first sign-in by link people are offered to add one (fingerprint, face or device PIN). A passkey works on the address where it was made. Nothing to build in the app.

  • What the code sees. Hypamoon checks the sign-in before a request reaches the code and hands the person over ready to use in env.HYPAMOON:

    • env.HYPAMOON.user is { id, email, name, roles, entitlements, groups, is(role), has(entitlement) }, or null when nobody is signed in.

    • env.HYPAMOON.requireUser(request) returns that user, or a ready 401 JSON response.

    • env.HYPAMOON.requireRole(request, "admin") (or a list of roles) returns the user, or a ready 401 or 403 JSON response.

    • await env.HYPAMOON.users.list() (or list({ role: "member" })) gives the people of the app, and await env.HYPAMOON.users.get(id) one person or null, each { id, email, name, role, created_at, last_sign_in_at }. Only for a signed-in admin (the first role in roles); otherwise it throws an error with .status 401 or 403. Read users this way rather than with your own fetch to /app-users: it is usually answered next to the app, without a round trip to the API. Inviting, changing a role and removing someone still go through /app-users (see the People screen).

    • await env.HYPAMOON.fetchPage(url) fetches one public web page for the signed-in user, from any site, without listing it in allowed_hosts (for example to import a recipe). It is off until you deploy the app with fetch_pages: true (which needs auth). It returns { url, status, contentType, body, truncated }, with url the final address after redirects. Only GET over http or https, only text, HTML or JSON (also JSON-LD), at most 2 MB (truncated: true when cut off), 10 seconds and 3 redirects; private addresses and Hypamoon hosts are blocked, also after a redirect, and no cookies or x-hypamoon-* headers are sent. Each user can fetch 20 pages per hour, and fetches count against the app's outbound budget. A refusal throws an error with .status: 401 not signed in, 403 blocked address, 415 not text, 429 limit reached, 501 off for this app, 502 or 504 the page failed or was too slow. The app log shows only the host and the duration.

    • await env.HYPAMOON.sendEmail({ to, subject, text }) sends one email from the app's server code (also from scheduled(), no signed-in user needed). Optional: cc, bcc, html, attachments, reply_to, category (transactional for a reply to something the recipients did, otherwise an unsubscribe link with the project's postal address is added, which needs a business address in the company details, and the email goes to one recipient), idempotency_key (the same key never sends twice) and from. to is one address or a list. An attachment's content may be a Uint8Array, like the PDF from renderPdf: attachments: [{ filename: "invoice.pdf", content: pdf }]. It follows exactly the rules of send_email for the app's project: the Mail module must be on, the sender is noreply@ on the project's verified sending domain unless from is an address on that domain, and every recipient must be known to the project (a user of the app, a form submission, a document, a chat conversation) or confirmed by the owner. The daily and per minute limits and the content check apply, and every recipient counts for the Mail module. await env.HYPAMOON.sendEmailBatch([{ to, subject, text }, ...]) sends 1 to 100 separate emails in one call (each person sees only their own; no attachments), all or nothing; a refusal also has .index. With a test key nothing goes out (status: "simulated"). It returns the email ({ id, status, ... }); a refusal throws an error with .status, .code (the error type, such as mail_not_enabled, email_domain_not_verified, recipient_not_allowed, email_limit_reached), .userMessage (a sentence to show the person) and sometimes .nextStep. The app cannot confirm a recipient itself; that stays with the owner. An existing app gets sendEmail with its next deploy_app.

    • await env.HYPAMOON.getEmail(id) reads back an email this app sent, with the id that sendEmail or sendEmailBatch returned (or null): status (queued, sent, delivered, bounced, complained, failed, simulated), reason (plain words, like "The mailbox is full."), recipient_status (one entry per recipient, so with several people one can bounce while the others are delivered) and timeline (each step with its time: queued, sent, delivered, delayed, bounced, complained). await env.HYPAMOON.listEmails({ to, status, since, limit, cursor }) lists this app's emails, newest first, as { data, next_cursor }. An app only ever sees its own emails, never those of another app or of the API. To react to a bounce, subscribe a webhook to email.bounced (see Webhooks for email below). An existing app gets getEmail and listEmails with its next deploy_app.

    • const pdf = await env.HYPAMOON.renderPdf(html, { format: "a4" }) turns one HTML document with its CSS into a PDF (render_pdf) and returns it as a Uint8Array: return new Response(pdf, { headers: { "content-type": "application/pdf" } }). Options: format (a4, a5, a6, letter, legal), landscape, margin ({ top: "12mm", ... }), max_pages (at most 20) and filename. Files of the app's own site load by path (<img src="/logo.svg">, a <link> to a stylesheet, fonts), and the site's colors and fonts are ready as --hm-* CSS variables; anything else does not load and JavaScript does not run. At most 2 MB of HTML, 10 seconds, 120 PDFs an hour per project. Nothing is stored. A refusal throws an error with .status, .code and .userMessage. An existing app gets renderPdf with its next deploy_app.

      Example, a timesheet app that mails on submit and on approval:

      Node.js
      // POST /api/hours/:id/submit, by the employee
      const user = env.HYPAMOON.requireUser(request);
      if (user instanceof Response) return user;
      // ... mark the row as submitted ...
      await env.HYPAMOON.sendEmail({
        to: env.MANAGER_EMAIL, // in vars; the manager is a user of the app
        subject: `Hours submitted: ${user.name} (week ${week})`,
        text: `${user.name} submitted the hours for week ${week}. Review them at ${env.HYPAMOON_SITE_URL}/review`,
        idempotency_key: `submitted-${id}`,
      });
      
      // POST /api/hours/:id/approve, by an admin
      const admin = env.HYPAMOON.requireRole(request, "admin");
      if (admin instanceof Response) return admin;
      // ... mark the row as approved, read row.email and row.week ...
      try {
        await env.HYPAMOON.sendEmail({
          to: row.email,
          subject: `Your hours for week ${row.week} are approved`,
          text: `${admin.name} approved your hours for week ${row.week}.`,
          category: "transactional",
          idempotency_key: `approved-${row.id}`,
        });
      } catch (e) {
        // the approval stands; tell the admin why the email did not go out
        return Response.json({ approved: true, email_sent: false, message: e.userMessage ?? e.message });
      }

    Use it as const user = env.HYPAMOON.requireUser(request); if (user instanceof Response) return user;. Never check the signature yourself and never use HYPAMOON_AUTH_SECRET in the code: that is how apps broke (a check without the right prefix turns every signed-in person into a 401). The request still carries the signed headers x-hypamoon-user and x-hypamoon-user-sig, only to forward them unchanged to the Hypamoon API (/app-files, /app-users). A header without a valid signature never reaches the code, and a request from another site that could change something arrives without a user (no cross-site requests in someone's name). The first role in roles is the app's admin role everywhere (the People screen, files of other users). A session lasts 90 days after the last use and at most a year after signing in.

  • Its own database. database: true gives the app a SQLite database as env.DB, only its own. apply_app_migration creates and changes the tables: DDL and DML only, no ATTACH, PRAGMA, VACUUM, triggers or transactions. The database counts toward the data of the plan; above it the app can only read and delete until there is room.

  • Private data per user. set_table_owner_column marks a table as per-user, with the column that holds the owner (user_id by default, the sub of the signed-in person). From then on Hypamoon itself keeps each person's rows apart: the code uses env.DB.mine("items") with select({ where, orderBy, limit, offset, columns }), get(where), count(where), insert(row), update(where, patch) and delete(where), always for the signed-in person (the owner is filled in and cannot change), and plain SQL that names the table is refused, even inside a string (pass values as parameters). Migrations may still add an index or a column and change the table's own rows, but no view or copy that reads around it. admin_role (optional) lets people with that role pass { all: true }; without it nobody sees other people's rows, admins included. Tables shared by everyone keep plain env.DB.prepare. An app with per-user tables gets its bindings only through the env of fetch (no global import of env). Plan this from the first table: personal data goes in tables with an owner column (one row per record), never in one JSON row for the whole app. Only per-user tables are covered by export_app_user and delete_app_user_data; deploy_app and check_readiness warn when an app with sign-in and a database has none ("export and delete per user won't cover your data"), and check_readiness lists tables that look like a backup or copy (backup, bak, copy, legacy, old, _v1, a date) with the question whether they can go. Drop a copy after an import once the user agrees, instead of leaving it behind.

  • Uploads. The code asks POST env.HYPAMOON_API + "/app-files" (name, type, exact size) and hands the upload_url to the page, which PUTs the file there once. Forward the x-hypamoon-user and x-hypamoon-user-sig headers of the request on that call to make the file the person's own: it is stored in their own folder, GET env.HYPAMOON_API + "/app-files/<id>" with the same headers gives a download link for 15 minutes that only opens for them (or the admin role, the first of roles), and the file goes along with export and deletion. Images, PDF, audio, video, plain text, CSV and JSON; the bytes must match the type, PDFs and text go through the abuse check, and files count toward the files of the plan.

  • A person asks for their data, or to be forgotten. export_app_user returns everything of one person as JSON: their account, role and passkeys, every row in the per-user tables and every file with a link for 15 minutes. delete_app_user_data deletes all of that for good, after the owner confirms. remove_app_user asks whether the data should go too (delete_data). Both list unmarked_tables: tables that are not per-user, so Hypamoon cannot tell which rows there belong to the person. When the app has no per-user table at all, app_tables is not_included and the answer says so: no row of the app's own database was exported or deleted.

  • Looking at the app as another user. Hypamoon has no "view as" or impersonation: an admin signs in as themselves. If the app's own code lets an admin act or look as someone else (a header or a switch in the app), write every start of that to the app's own audit table, also when nothing is changed, with who, as whom and when; ask for a reason before acting for someone (approving, signing); and show a clear banner while it is on.

  • Retries. A webhook from another service can go to https://<site>/_hypamoon/queue/api/<path>: Hypamoon answers 202 at once and delivers POST /api/<path> to the app, again after 30 s, 1, 2, 4 and 8 minutes while it fails (x-hypamoon-attempt says which try). Scheduled runs retry the same way. Every attempt, retry and give-up is in get_app_logs.

  • Four-eyes approval. With Team and four-eyes approval on, a live deploy_app or rollback_app waits for another owner or builder (approve_deploy), like a site.

tool scope what it does
list_app_users sites:read Who may sign in to the app, with role and last sign-in. Email addresses are masked (s***@example.com); reveal: true shows them in full and is logged.
list_app_mails sites:read The sign-in links, codes, invitations and account emails the app sent, with their delivery status (queued, sent, delivered, bounced, complained, failed) and the reason. Recipients masked; never the link or the code. For "Check your email, but nothing arrived".
invite_app_user sites:write Lets someone sign in with a role and emails them a sign-in link (send_email: false for no email).
import_app_users sites:write Makes the people in a collection (default wp_users, from the Hypamoon Connect plugin) users of the app, with role_map from WordPress roles to app roles. No passwords: people sign in with a link or code by email. Silent by default; send_email: true invites each new person within the invitation limits (the rest comes back as remaining), invite_existing also invites people added earlier who never signed in. Idempotent on the WordPress id and the email address; the app user id is written into each document (hypamoon_app_users). dry_run shows the plan.
set_app_user_role sites:write Gives a user another of the app's roles; the app sees it within seconds.
remove_app_user sites:write Stops someone from signing in; open sessions end within seconds. delete_data true also deletes their data.
export_app_user sites:write Everything of one person as JSON, with 15-minute links to their files.
delete_app_user_data sites:write Deletes everything of one person for good (confirm: true after the owner agrees).
list_app_files sites:read The files the users uploaded through the app, with the total size.
delete_app_file sites:write Deletes one of those files for good.
apply_app_migration sites:write Runs new SQL migrations on the app's database, in order, each all or nothing.
set_table_owner_column sites:write Makes a table private per user (or shared again with remove: true).
create_app_from_template sites:write A working app in one call (template accounts-data-uploads): a new site with sign-in, one table private per user with the fields you choose (text, long_text, integer, number, boolean, date, list), uploads per row (photos including HEIC, voice memos, video including MOV, PDF) and a share link per row that opens without signing in. It runs deploy_app, apply_app_migration and set_table_owner_column for you; extend it from there. Not in ChatGPT.
get_app_code sites:read The server code of an app: main_module and every module of the running version, or of a kept earlier one with version; module for one module. Up to about 200,000 characters per answer; a module that does not fit comes back truncated. Not in ChatGPT.
get_app_schema sites:read The tables of the app's database with columns, indexes, number of rows and whether they are per user, the views, the applied migrations and the name for the next migration. Not in ChatGPT.
query_app_db sites:read One read-only SELECT on the app's database, at most 50 rows; email addresses and phone numbers masked, every query in the activity log. Not in ChatGPT.
create_app_group sites:write Creates a group (team) in an app with auth.groups: true: a key the code checks with env.HYPAMOON.user.inGroup(key), a name, and optionally a role and an entitlement everyone in it gets too (never the admin role). Not in ChatGPT.
add_app_group_member sites:write Puts a user of the app in a group; the app sees it within seconds. Not in ChatGPT.
list_app_groups sites:read The groups of an app with their role, entitlement and members (email addresses masked). Not in ChatGPT.

Rolling out from October 2026, so not every project has these yet:

  • The admin page in the app. /_hypamoon/admin on the site is a ready page for the app's first role: members with last active, invite, change roles, block and unblock, approve or reject requests, and invite links with a maximum number of uses and an end date that can be revoked. The owner sees the same on the Users page of the portal.
  • More ways to join. Besides invite and open, auth.signup can be approval (anyone asks to join, also as a waitlist; an admin approves) or domain with allowed_domains. A shared invite link (/_hypamoon/auth/join?t=...) works with every sign-up mode.
  • Buyers as users. With auth.buyers: true, someone who buys paid access to the site (create_access_product with entitlement) becomes a user of the app with that entitlement: env.HYPAMOON.user.has("pro") or env.HYPAMOON.requireEntitlement(request, "pro"). A refund or cancellation takes it away. They sign in once, with the app's sign-in.

The ChatGPT app is in review at OpenAI: until it is approved, ChatGPT does not get export_app_user, delete_app_user_data or set_table_owner_column, and remove_app_user there works as before.

A small app with accounts and private data per user: a wardrobe. Deploy it with auth: { signup: "invite", roles: ["admin", "member"] } and database: true, apply 0001_init.sql, mark items with set_table_owner_column (column: "user_id", admin_role: "admin"), then invite people.

sql
CREATE TABLE items (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, photo_file_id TEXT, created_at TEXT NOT NULL);
CREATE INDEX items_user ON items (user_id, created_at);
Node.js
// Forward the signed user to the Hypamoon API, so a file belongs to that user.
const asUser = (request) => ({
  "content-type": "application/json",
  "x-hypamoon-user": request.headers.get("x-hypamoon-user"),
  "x-hypamoon-user-sig": request.headers.get("x-hypamoon-user-sig"),
});

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    // Hypamoon checked the sign-in already: never verify the signature yourself.
    const user = env.HYPAMOON.requireUser(request);
    if (user instanceof Response) return user; // 401 JSON, nobody signed in
    const items = env.DB.mine("items"); // only this user's rows, always
    if (url.pathname === "/api/items" && request.method === "GET") {
      return Response.json(await items.select({ orderBy: "created_at desc", limit: 200 }));
    }
    if (url.pathname === "/api/items" && request.method === "POST") {
      const { name, photo_file_id } = await request.json();
      const row = await items.insert({ id: crypto.randomUUID(), name, photo_file_id: photo_file_id || null, created_at: new Date().toISOString() });
      return Response.json(row, { status: 201 });
    }
    if (url.pathname === "/api/upload-url" && request.method === "POST") {
      const { name, content_type, size } = await request.json();
      const r = await fetch(env.HYPAMOON_API + "/app-files", { method: "POST", headers: asUser(request), body: JSON.stringify({ name, content_type, size }) });
      return new Response(r.body, { status: r.status, headers: { "content-type": "application/json" } });
    }
    if (url.pathname === "/api/admin/items" && request.method === "GET") {
      const admin = env.HYPAMOON.requireRole(request, "admin");
      if (admin instanceof Response) return admin; // 403 JSON for everyone else
      return Response.json(await env.DB.mine("items", { all: true }).select({ orderBy: "created_at desc" }));
    }
    return Response.json({ error: "Not found" }, { status: 404 });
  },
};

A staging for an app (not available yet)

For an app with real users or data, a staging is a second copy to work on without anyone noticing: its own code, its own database and its own address, https://<slug>--staging.hypamoon.app. Every function there is on test (payments, email to other people, phone and SMS are simulated), and search engines never index it (noindex, nofollow, Disallow: /). For an app with sign-in, the staging is only for its own signed-in users unless you create it with protect: false; its users are separate from the app's.

tool scope what it does
create_staging sites:write Creates the staging of an app (app_id) with the app's running version. Its database gets the app's schema plus your seed (INSERT statements, staging only); the app's rows only with copy_data: true, when the owner explicitly asks (at most 20,000 rows). Calling it again returns the existing staging.
promote_staging sites:write Puts the staging on the app: first the migrations that ran only on the staging, then its running code with its settings. Data never moves back. Needs a key in the app's mode; with four-eyes approval on, the code waits for a colleague.
delete_staging sites:write Deletes the staging: code, database, users, files and key. The app does not change.

New code goes to the staging with deploy_app and staging: true (with the app's site_id). The staging's id from create_staging is the app_id for apply_app_migration, get_app_logs and invite_app_user on the staging. Schedules are kept with the staging's versions but only run on the app.

REST: POST, GET and DELETE /api/v1/apps/{id}/staging, POST /api/v1/apps/{id}/staging/deploy (the body of POST /api/v1/apps without site_id) and POST /api/v1/apps/{id}/staging/promote. Not in ChatGPT while the app is in review.

Email from your own domain

An extra module: the owner turns it on in the portal (Mail). Test keys can try everything, and a test email is never sent (status simulated).

  • A sending domain is a domain the project already has (connect_domain or register_domain). If its DNS is hosted here, add_email_domain adds the records itself: an existing SPF record is merged instead of a second one, the user's own mail (MX) is never touched and nothing is deleted, so their mailbox keeps working. If the DNS is hosted elsewhere, records lists what to add there. A DMARC record is only suggested, never added. Then verify_email_domain until the status is verified.
  • Who gets email. Only people the project already knows: an address in a form submission, a document, a chat conversation or a user of one of the project's apps. Anyone else only after the owner confirms the address (allow_email_recipient, in a chat where the owner is signed in, or in the portal). Never a bought, scraped or imported list.
  • One email, several people works like Resend: to is one address or a list, with optional cc and bcc lists, at most 50 recipients together. Everyone in to and cc sees the others, as in any email. When people must not see each other, use bcc, or send_email_batch: 1 to 100 separate emails in one call, each with its own to (a personal invitation per person). A batch is all or nothing: if one email is refused, none goes out and the error names it (index). No attachments in a batch.
  • Attachments. At most 10 files and 10 MB together, each a filename with one of content (base64), media_id (a file from Media) or path (an https address on one of the project's own sites; nothing else is fetched). PDF, PNG, JPG, GIF and WebP, plus ICS, TXT and CSV as UTF-8 text. The type is checked on the file's first bytes; executables, HTML, SVG, archives and PDFs with scripts are refused (attachment_not_allowed, attachment_too_large). Inline content counts toward the request size (keep it under about 4 MB). Only the name, type and size of an attachment are kept, never the file.
  • Limits. Every recipient counts: a daily limit per project, 200 recipients and 20 calls per minute (a batch is one call), and each recipient is one email for the Mail module's bundle.
  • New recipients from documents. People who only appear in documents the app wrote are capped per day, each address counted once (also across cc, bcc and a batch): 25 to start, 100 after 200 sent emails, 500 after 1,000 sent emails and 14 days, as long as the last 200 emails stay under 2% bounces and without spam complaints. A verified business or a paid plan gets there sooner (50 emails for 100; 300 emails and 7 days for 500). A pause or a spam complaint in the last week sets it back to 25. App users who signed in, buyers with a paid order and addresses the owner confirmed never count. list_emails shows the current limit, what is left and what raises it (mail.new_recipients). Every email passes a content check (email_blocked says why). Sending pauses automatically above 5% bounces or 0.1% spam complaints over the last 200 emails.
  • Status and timeline. Every email has a status, a short reason when it bounced, was marked as spam, is delayed or was not sent (no provider jargon), recipient_status per recipient and a timeline with times. app_id says which app sent it. list_emails filters on app_id, to, status and since; the portal (Mail) shows the same, with a filter per app, recipient, status and period.
  • Webhooks for email. create_webhook takes email.sent, email.delivered, email.delayed, email.bounced and email.complained. Each comes once per recipient, signed like every event, with data: email_id, app_id, recipient, recipient_field, status, reason, occurred_at and test. Never the subject or the content. Pass app_id to get only the email events of one app (an endpoint with app_id takes only email events). With a test key email.sent and email.delivered still arrive (test: true), so the receiver can be tested.
  • Unsubscribing. category other (the default) adds an unsubscribe link and a List-Unsubscribe header, is not sent to people who unsubscribed or marked an email as spam, and goes to exactly one recipient (each person gets their own unsubscribe link). Under the unsubscribe link goes the project's postal address from its company details (set_company_details), as the US CAN-SPAM Act asks of marketing email. Without a business address, other is refused with 422 company_details_missing and a user_message asking the user to add it in Settings, Company. transactional is for a reply to something the recipients did, needs no address, and may go to several people. Under CAN-SPAM and the GDPR the user is the sender of their email; Hypamoon delivers it.
tool scope what it does
send_email messages:write One email from a verified sending domain to people the project knows: to (one or a list), cc, bcc, attachments. Returns the email with status sent (test: simulated). Safe to retry. In ChatGPT one recipient and no attachments while the app is in review.
send_email_batch messages:write 1 to 100 separate emails in one call, each with its own to, all or nothing, no attachments. Returns data with each email. Safe to retry. Not in ChatGPT while the app is in review.
list_emails messages:read The project's emails, newest first, with status, reason, recipient_status, timeline, app_id, recipients (to, cc, bcc, recipient_count) and attachments (name, type, size), plus whether Mail is on or paused and the daily limit. Filters: app_id, to, status, since.
get_email messages:read One email: queued, sent, delivered, bounced, complained, failed or simulated, with reason, the status per recipient and the timeline.
list_email_domains sites:read The sending domains with status and DNS records.
add_email_domain sites:write Makes a domain of the project a sending domain and sets or lists its DNS records.
get_email_domain sites:read One sending domain with its records and the last check.
verify_email_domain sites:write Checks the records now: pending, verified or failed.
allow_email_recipient messages:write The owner confirms an address that is not in the project. Only with the owner signed in; an API key cannot.
check_email_address messages:read Checks 1 to 50 addresses before sending: format, the project's suppression list, likely typos (gmial.com), throwaway domains, and whether the domain accepts email. result is valid, risky, invalid or unknown, with reason, suggestion and would_send. Sends nothing. Not in ChatGPT while the app is in review.
get_email_deliverability messages:read Per sending domain over 7 and 30 days: sent, delivered, bounced, complained and the rates against the thresholds (2% bounces, 0.1% complaints), health and advice in plain words, SPF, DKIM and DMARC, and dedicated_ip with its price. Opens are not measured. Not in ChatGPT while the app is in review.
get_email_bimi sites:read The inbox logo (BIMI) of a sending domain: DMARC policy, the BIMI record, ready and the steps left. Not in ChatGPT while the app is in review.
set_email_bimi sites:write Sets default._bimi.<domain> with logo_url (SVG Tiny PS) and optionally certificate_url, after checking DMARC (bimi_not_ready) and the logo. DNS hosted elsewhere: record_to_add. Not in ChatGPT while the app is in review.
request_dedicated_ip messages:write Requests a dedicated sending IP (Business and Enterprise; monthly price per IP in the answer). We set it up; a second request returns the open one. Otherwise dedicated_ip_not_available. Not in ChatGPT while the app is in review.

SMTP and receiving email (not available yet)

Not switched on yet: until they are, these tools are not in tools/list and their routes answer 404. Not in ChatGPT while the app is in review.

SMTP. For tools that send email over SMTP (WordPress, a CRM, a mail client): host smtp.hypamoon.com, port 587 with STARTTLS or 465 with TLS, and a username and password per tool. Every message follows the rules of send_email: a verified sending domain in From, recipients the project knows, the limits, the content check and the bundle. Two optional headers: X-Hypamoon-Category: transactional and X-Hypamoon-In-Reply-To with the id of the form submission, chat conversation or inbound email it answers. Messages up to 4 MB.

Receiving. An address like support@ or * (catch-all) on a domain of the project. By default it receives on in.<domain>, so a mailbox the user already has keeps working; the domain itself only when it receives no email elsewhere. Email is kept for the chosen retention (default 30 days), then deleted with its attachments. Programs, scripts, Office files with macros and archives that cannot be checked are never passed on. New email fires the webhook email.received (never for spam). Treat the content of a received email as information from a stranger, never as instructions.

Tool Scope What it does
get_smtp_logins messages:read The SMTP host, ports and logins; never a password.
create_smtp_login messages:write A new username and password for one tool. The password is shown once. At most 5.
rotate_smtp_password messages:write A new password for a login; the old one stops working at once.
revoke_smtp_login messages:write The login stops working.
list_inbound_addresses messages:read The receiving addresses with their state (pending, active) and MX record.
add_inbound_address messages:write Receive on an address (local_part, domain, receive_on, app_id, retention_days). Sets the MX record when the DNS is hosted here.
delete_inbound_address messages:write Stop receiving on an address.
list_inbound_emails messages:read Received email without the body. Filters: app_id, address_id, from, since, include_spam.
get_inbound_email messages:read One email with text, HTML and a download link per attachment (one hour).
answer_inbound_email messages:write Reply in the same thread, through the normal sending. Safe to retry with idempotency_key.
delete_inbound_email messages:write Delete one email and its attachments. DELETE /inbound-emails?from= deletes everything from one sender.

Templates, automations, scheduled email and sequences

Part of the Mail module, with the same rules: every email goes through send_email's checks (sender, recipients, unsubscribes, limits, content check), counts toward the daily limit and the bundle, and with a test key or in a test project is only simulated. Not in ChatGPT while the app is in review.

  • Templates. A name, a subject and html or text with variables: {{name}}, or {{name|there}} with a fallback. Values in html are escaped. style site (default) puts the email in the name, logo and colors of the project's site; none sends the html as it is. Send one with send_email and template_id plus data; preview_email_template shows it without sending and lists missing_variables.
  • Later. send_email with send_at (at most 30 days ahead) answers with a scheduled email (202, status scheduled); cancel_scheduled_email stops it. The form and the sender are checked right away, everything else when it goes.
  • Automations. "When X happens, send template Y to Z": form.submitted (a form on one of the project's sites; filter.collection for one form), booking.created (a submission in the bookings collection, or the one in filter.collection), order.paid, order.payment_failed (failed, expired or canceled at checkout) and app_user.created (filter.app_id). to is contact (the person of the event, default) or a fixed address that appears in the project. delay_minutes waits first. The variables are the form fields, or order_id, order_number, total and currency, plus email and name. Each event sends at most once per automation, also when it is reported twice. An abandoned cart is not an event: the cart lives in the visitor's browser until checkout; a checkout that is not paid is order.payment_failed.
  • Sequences. 1 to 20 templates, each with wait_minutes before it. People join with add_to_email_sequence (only someone the project knows) or through an automation with sequence_id. A sequence stops for a person when they unsubscribe (always), when an event in stop_on happens for their address (like order.paid), or with remove_from_email_sequence (for example when they replied).
  • The queue. list_scheduled_emails shows everything that goes later or went, with source (api, automation, sequence, list, confirmation), status and, once sent, email_id. A limit per minute or per day moves an email to later instead of dropping it.
tool scope what it does
create_email_template messages:write Saves a template: name, subject, html or text with variables, style.
list_email_templates messages:read The templates with their variables.
update_email_template messages:write Changes only the fields you pass.
delete_email_template messages:write Deletes a template; refused while an automation, sequence or list uses it.
preview_email_template messages:read The template filled with data, without sending: subject, html, text, missing_variables.
create_email_automation messages:write When an event happens, send a template or start a sequence, with delay_minutes.
list_email_automations messages:read The automations.
update_email_automation messages:write Changes an automation, like enabled: false.
delete_email_automation messages:write Deletes an automation; what it already scheduled still goes.
list_scheduled_emails messages:read Emails that go later or went, with status and source.
cancel_scheduled_email messages:write Stops an email that is still scheduled.
create_email_sequence messages:write Saves a sequence of templates with waits and stop_on.
list_email_sequences messages:read The sequences, with how many people are in each.
delete_email_sequence messages:write Deletes a sequence; everyone in it stops.
add_to_email_sequence messages:write Adds one person the project knows to a sequence.
remove_from_email_sequence messages:write Stops a sequence for one address.

Lists and newsletters (not available yet)

Not switched on yet: until it is, these tools are not in tools/list and their routes answer 404.

  • Double opt-in, always. add_email_list_contact sends a confirmation email; the address is on the list only after the person clicks the link. A list can also take sign-ups from a form (form_collection). There is no way to add confirmed addresses, so a bought or imported list can never be sent to. The confirmation email counts toward the cap on new recipients a day; a confirmed contact does not.
  • Sending. send_to_email_list sends one email per confirmed contact as category other: an unsubscribe link and the business's postal address (refused without one). People who unsubscribed or complained get nothing. It goes at 15 emails a minute per project and counts toward the daily limit; what does not fit today goes tomorrow. Unsubscribing uses the project's unsubscribe list, the same as every other email.
tool scope what it does
create_email_list messages:write A list with a sender and, optionally, a form that signs people up.
list_email_lists messages:read The lists with pending, confirmed and unsubscribed counts.
delete_email_list messages:write Deletes a list and its addresses.
add_email_list_contact messages:write Sends the confirmation email to one address that asked to be on the list.
list_email_list_contacts messages:read The addresses on a list with their status.
remove_email_list_contact messages:write Removes one address from a list.
send_to_email_list messages:write One email per confirmed contact, at a fixed pace. Safe to retry.

DMARC reports: who sends mail as your domains (not available yet)

Not switched on yet: until it is, these tools are not in tools/list and their routes answer 404.

Mailbox providers (Gmail, Outlook, Yahoo and others) send a daily DMARC report to the address in the rua of a domain's DMARC record: how much mail claiming to be from the domain they received, from which servers, and whether it passed DKIM and SPF. Every project gets its own report address on our side, so these reports turn into a readable overview. Included with Mail at no extra cost, and also for a connected domain without the Mail module (insight only).

  • Turning it on. enable_dmarc_reports adds the project's report address to the domain's DMARC record. Without a DMARC record (DNS hosted here) it adds v=DMARC1; p=none with the address: this only observes, delivery does not change. An existing record keeps its policy and its other report addresses; changing a live record first returns confirmation_required with a preflight, and only goes through with confirm: true after the user agreed (undo_dns_change reverses it for 30 days). With DNS elsewhere, record is what the user sets there (state: add_yourself). The dmarc_suggestion of add_email_domain already includes the address.
  • Reading it. get_dmarc_report shows per domain the messages, the share that passed DMARC and the policy; the biggest senders (Google, Microsoft 365, Hypamoon Mail and other known senders by name, otherwise the server's domain or IP); what fails, with the DKIM and SPF results; and concrete advice, like turning on DKIM at a sender or moving to p=quarantine.
  • Alerts. Possible spoofing (mail from a sender that never passed for the domain fails, from 5 messages) and a broken setup (a sender that used to pass now mostly fails) send one email to the project and the webhook event email.dmarc_alert, at most once a week per unknown sender and once a day per known sender.
  • Kept. Counts per day, domain and sender for 400 days; the full report for 30 days. Deleted with the project.
tool scope what it does
get_dmarc_report sites:read Per domain: messages, pass_rate, policy and whether reports are on; top senders, failing, advice and alerts. days 1 to 90, optional domain. Not in ChatGPT while the app is in review.
enable_dmarc_reports sites:write Adds the report address to the domain's DMARC record, or returns the record to set elsewhere. Asks first before changing a live record (confirm). Not in ChatGPT while the app is in review.

The brain: how the business works (not available yet)

Not switched on yet: until it is, these tools are not in tools/list and their routes answer 404.

The brain is one place per project for how the business actually works: its processes, the steps in them, the decision rules people apply every day, the exceptions, the words only insiders use, and who decides what. It is not a place to dump documents. The receptionist and the chat read what the owner confirmed; agents and the user's own Claude or ChatGPT read it through these tools.

  • Capturing. interview_process returns a short interview guide: ask one question at a time, write the answers down with save_process (always a draft), show the rules back in plain words, and call confirm_process only after the owner says yes. Every change is a new version; old versions stay readable with get_process and version.
  • Rules. A rule has a name, a condition (when), an outcome (then) and a risk: low, money or legal. A condition is a small JSON shape, never code: {"field": "party_size", "equals": 8}, {"field": "day", "in": ["friday", "saturday"]}, {"field": "party_size", "min": 7, "max": 12}, {"field": "allergy", "present": true}, combined with {"all": [...]} or {"any": [...]}, at most 4 levels deep and 20 comparisons. A rule without when is the default for every other case. The outcome is a flat object of short values.
  • Applying. resolve_rule takes a process and a situation and returns what applies. Each rule decides the keys of its own outcome, so "closed on Mondays" and "book groups up to 8 directly" live side by side; for each key the most specific matching rule wins, and a default rule only fills in what nothing more specific says. needs_input lists the fields to ask for; ambiguous and no_rule come with a suggested open question. Every money or legal rule comes back with action_mode approve: tell the customer what it says, never promise or carry it out without a person approving. Rules show when they were last confirmed, and needs_reconfirmation after 180 days.
  • Learning. When the receptionist, the chat or an agent doesn't know how the business handles something, it becomes an open question (add_open_question; the same question again counts once more). The owner answers it once with answer_open_question (or in the portal under Brain): as a rule, a term or a plain answer the assistants use from then on.
tool scope what it does
interview_process data:write The interview guide for capturing a process in the user's own chat. Changes nothing itself.
list_processes data:read The processes, with the active version and a newer draft.
get_process data:read One process with steps, rules (also as plain sentences), exceptions, the fields its rules use and all versions.
save_process data:write A process with steps, rules, exceptions and roles in one go, as a draft. With process_id a new version.
confirm_process data:write Makes the newest draft the active version. Only after the owner said yes.
resolve_rule data:read Finds the rule that applies in a situation, with outcome, risk, action_mode and last confirmed date.
define_term data:write A word the business uses and what it means, or a new version of it.
lookup_term data:read What a word means here, matched on the term and its synonyms.
add_open_question data:write A question nobody could answer yet. No duplicates; at most 50 new a day.
list_open_questions data:read The questions still open (or answered, dismissed, all).
answer_open_question data:write Turns an answer into a rule (a new draft, or confirmed with confirm when the owner said so), a term or a plain answer.

Example: capturing table reservations

A run of the flow on a test project (6 October 2026), for a fictional restaurant ("Bistro Fenna"). The owner talks in her own AI chat with Hypamoon connected; the tool calls are shown in brackets, and the data they wrote is what the tools returned below.

What the tools returned in that run:

  • save_process stored version 1 as a draft with seven rules ("Closed on Mondays", "Open other days", "Small groups", "Large groups", the two last-seating rules and "Dogs"), the exception for regulars and the role Floor manager; fields was day, party_size, dog. confirm_process made it active.
  • resolve_rule with {"party_size": 10, "day": "saturday"}: matched, outcome book_directly: false, deposit_per_person_eur: 10, last_seating: "21:00", open: true, decided by "Large groups", "Last seating on Friday and Saturday" and "Open other days"; risk money, so action_mode approve, and missing_fields ["dog"] because the dog rule could still add something.
  • With {"party_size": 4}: needs_input for day, because "Closed on Mondays" could still change open. With {"party_size": 4, "day": "monday"}: open: false.
  • A caller asked whether a group can bring their own birthday cake. No rule covered it, so the assistant said it would check with the team and add_open_question recorded it. The owner answered later, in her chat:

That made version 2 active. resolve_rule with {"party_size": 10, "day": "friday", "own_cake": true} now returns "Own cake" first, with own_cake_allowed: true on top of the group rules, and still action_mode approve: the assistant tells the caller about the fee but never agrees to it on its own.

Confirm in the portal

When a chat connected through OAuth (Claude, ChatGPT, Grok or another chat app) asks for something that cannot be undone, the tool does not do it. It sets the action up and answers with one link, https://app.hypamoon.com/confirm/<id>. The user opens it, reads in plain words what happens (for example "Delete the site bouwbot-demo.hypamoon.app. It goes offline right away. For 30 days you can restore it under Sites & apps, Recently deleted; after that its files are erased for good.") and presses Confirm, with the passkey step the portal already asks for critical actions. Only then does Hypamoon run the same tool, through the same route as the API. Cancel changes nothing. Today shows what is still waiting, and /confirm lists it.

  • The tools: delete_site, delete_collection, delete_app, delete_secret, delete_webhook, delete_agent, release_number, remove_member, remove_app_user, delete_app_user_data, revoke_access_pass, disconnect_domain, invite_member, update_member_role, set_member_access, allow_email_recipient, set_dns_record, import_zone_file, clean_up_dns, switch_domain_nameservers, connect_domain, approve_deploy and move_to_live. Three only in some cases: create_webhook for a host the project does not use yet, set_call_plan when a new outside number is added, and approve_agent_action when it approves (a reject runs directly, like reject_deploy). delete_document is not on the list: one document per call, and confirming each one would make cleaning up test entries unworkable.
  • In Claude Code, register_domain and transfer_domain also wait for the owner, with OAuth and with an API key (then the person who created the key confirms). Nothing is bought until then; if no confirmation can be set up, the tool refuses. refund_order there moves no money: it answers with the link of get_refund_link, where the owner checks the amount and refunds.
  • Otherwise only chat connections. An API key (a script, a customer's app, an MCP client with a key) keeps working directly, as before; four-eyes approval still applies where it already did.
  • The answer has confirm_url, id, status (pending) and a user_message to pass on. Asking the same thing again gives the same link. The link works for 24 hours, only for the person who asked, and only in the project it belongs to.
  • get_confirmation with the id tells the chat whether it is done. Not in ChatGPT while the app is in review.
  • set_dns_record that adds or changes a record on a domain that is still pending (the nameservers do not point to Hypamoon yet, also according to public DNS) runs directly: nothing is live there yet. Deleting a record, and everything on an active domain, still waits for a confirmation. import_zone_file follows the same rule: directly on such a pending domain, and a confirmation on an active domain or with replace_existing (then records are deleted).
  • Several at once: when the same chat connection sets up more than one confirmation shortly after each other (at most 30 minutes between two), the answer also has group_confirm_url and pending_in_group. That one page lists them all, checked, with one Confirm all and one second step; each action still runs and is logged on its own. get_confirmation returns the same fields. Not in ChatGPT while the app is in review.
  • With four-eyes approval on, a confirmed delete_site then waits for a second owner or builder, as always.
  • An agency working in a client project (the project argument) still runs directly, except a domain purchase or transfer from Claude Code, which is refused there.

Approve a goal at once

Behind HYPAMOON_PLAN_BEVESTIGEN (with HYPAMOON_BEVESTIG_IN_PORTAAL), off by default. Design: docs/goedkeuren-per-doel.md.

  • propose_plan with goal and domain (and site_id for a site) sets up one confirmation for the whole goal. The portal shows it as one card: the goal, what happens in plain words, the cost, and the technical steps under Details.
  • After Confirm (with the same passkey step), the chat calls the usual tools. For 24 hours a step that fits the plan runs without a new link: the same user, project and chat connection, the domain of the plan, the kinds of records of the plan, and at most a fixed number per kind. Each step is logged (api_plan_stappen and the activity log).
  • Always its own link: deleting or overwriting a record (unless the site plan says it replaces what is on the address), a zone import that deletes records, skipping the nameserver check, switching back to the previous nameservers, another domain, clean_up_dns, disconnect_domain, and any cost the plan did not mention (like the cname surcharge in a nameservers plan).
  • A plan is always its own card; it is never part of a "Confirm all" group.

Team

A project has people with a role: owner (everything), builder (build and deploy, keys and webhooks, approve deploys), editor (change content, not structure), viewer (read only) and billing (only the balance and invoices). An API key or a connected app acts with the role of the person who created it, and stops working when that person leaves the project.

An editor works on content only. Their chat sees the read tools and five tools that write: insert_document, update_document and delete_document in an existing collection, deploy_site on an existing site with keep_existing: true, and create_upload_urls for images. On a site an editor changes html, htm, md, txt and json files that are already there, and adds or replaces images (png, jpg, jpeg, gif, webp, avif, ico). Scripts, event handlers, forms, embeds and stylesheet links in a page stay exactly as they are. An editor never creates or deletes a site or a page, never touches js, css, svg, _redirects, _headers or a manifest, never changes the name, password, address or access of a site, and never creates or changes collections, apps, domains, mail, payments, phone numbers, agents, keys, webhooks, secrets, the team or verification. Anything else answers "Editors can change content, not structure. Ask a builder or the owner." A collection with personal data (the personal_data flag, orders and buyers) stays closed to an editor, also for reading, until an owner turns on personal data for that editor under Team. Products and prices affect payments: an editor reads the product collection (role: "products", with its variants) and the discount codes like a viewer, but adds, changes or deletes a product, a price, a variant, stock or a code only after an owner turns on "Products and prices" for that editor under Team. Without it the answer starts with "Products and prices affect payments." An editor never issues or redeems a gift card and never writes in the gift card collection, with or without these settings. With four-eyes approval on, a live change by an editor waits like any other deploy; an owner can also hold only the changes of editors ("Editor changes need approval").

  • list_members: the people and their roles, and the open invitations.
  • invite_member: emails an invitation with a role. The agent can never give a role above the role of the user it acts for; a builder only invites editors and viewers. In ChatGPT the role list in the tool shows the four roles from before the editor (the app is in review); the tool accepts editor there too. Inviting, changing roles and removing members work with a live key or connection only.
  • update_member_role and remove_member: owner only. Removing someone also revokes their keys and connected apps for the project.
  • set_member_access (not in ChatGPT): limits a member to some sites of the project, owner only. access is { "sites": [...], "apps": [...], "collections": [...] } with ids (a site may also be given by slug or name), or null for the whole project. Each site brings its app. Without collections the tool fills in the collections the sites post to (a form from get_form_snippet, or a call to /public/collections/<name>/) and returns them as suggested_collections. invite_member and update_member_role take the same optional access outside ChatGPT. Owners are never limited. A limited member sees only those sites, apps and collections in list_sites, list_collections, list_apps and the portal, and none of the project-wide tools (new sites or collections, domains, email, payments and orders, phone, team, keys, webhooks, agents, secrets, agencies, go-live). Anything outside answers "You have access to: Newme2day site. Ask the owner for more." Their keys and connected apps follow on the next request, also after a change. The rules of their role still apply on top: a limited editor only changes content.
  • list_pending_approvals, approve_deploy and reject_deploy: with four-eyes approval on (an owner turns it on in the portal under Team), a live deploy_site answers with a pending approval instead of a live site, and the live site keeps its previous version until another owner or builder approves it. delete_site waits the same way, and the site stays online until then. The other owners and builders get an email. Nobody approves their own deploy, not even one their agent made. Test projects never wait.
  • list_activity: read-only log of who did what, in the portal or through an agent, and through which app. Refused calls are in it too (mcp_aanroep_geweigerd): a tool that does not exist for that key or connection, a missing scope or role, or another 401 or 403, with the tool, the reason, the status and the app. Never the arguments or a token.
  • get_recent_errors: read-only list of the failed tool calls and API requests of the project from the last 14 days, newest first: when, which app (chatgpt, claude, grok, oauth, mcp or api), the tool or route, the error type, the status, the detail that was returned, how long it took and the request id. Every error message ends with Request id: req_...; pass it as request_id to find that one error. Arguments are only summarized (field names, types, lengths and counts), never their values; email addresses, phone numbers, keys and URL paths in the detail are masked. At most 1000 errors per project per day are kept (dropped_today counts the rest). Not in ChatGPT while that app is in review. Same as GET /v1/errors.

One chat, more projects

Not live yet (behind HYPAMOON_MULTI_PROJECT, off). When you connect a chat with OAuth, a second step after Allow asks which projects it can use: This project (the default, as before), Selected projects, or All my projects, also new ones. In every project the chat can do only what your role there allows; if you leave a project, the chat loses it too. You change the choice later in the portal under Connect your chat. Not available in ChatGPT, and not for API keys: a key always belongs to one project.

  • list_projects: the projects this connection can use, with the role, the mode (test or live), the default project and the project you connected first.
  • use_project: makes a project the default for this connection. Tools without project_id work there from then on, also in later messages.
  • project_id on every other tool: do this one call in that project.

Every answer of a tool that changes something ends with the project it happened in. On the REST API, an OAuth token of such a connection takes the Hypamoon-Project header with a project id.

Agencies and client projects

For an agency that builds sites and apps for clients. The agency keeps its own Hypamoon project and, next to it, one project per client. A client project belongs to the client: you can build it before the client has an account, and the client takes it over later. From then on the client decides what your agency may do and can revoke your access at any time.

How it goes:

  1. create_client_project starts a project for a new client. You build it in test, from your own chat or code, with the project argument or the Hypamoon-Project header.
  2. invite_client emails the client a link. The client signs in, sees what your agency can do and who pays, and accepts. The project is now theirs and your agency keeps working on it through the relation.
  3. A client who already has a project gets a link from request_client_access instead, and accepts in the portal under Agencies.
  4. You see all clients in your own portal under Clients (app.hypamoon.com/clients): what is being built, what waits for the client, usage and open payments. The client sees your agency under Agencies, with a button to revoke.

In the REST API these are the routes under /clients (the agency side) and /agencies (the client side), in the Agencies section of the API reference. Over MCP they are in the agency toolset. They need the scopes clients:read and clients:write on the agency's own key or connection. Not available in ChatGPT.

  • list_client_projects: your client projects, with status (pending or active), stage (building: you set it up and the client has not taken it over; client_owned) and the limit the client set (test, or live).
  • create_client_project: a new project for a client in the building stage. Your agency is its temporary owner through the relation, it stays in test with Free limits, and the one-project-per-owner limit of signing up does not apply.
  • invite_client: emails the client a link to take the project over, from Hypamoon's own mail. The client then sees what your agency can do, who pays and how to revoke, and accepts.
  • request_client_access: for a client who already has a project. Returns confirm_url; give it to the client, who accepts or declines in the portal under Agencies. Nothing is emailed.
  • revoke_client_relation: stop working on a client project. What you built stays. The client can revoke your access at any time, in the portal or with DELETE /agencies/{id}.
  • get_client_usage: cost per month (by source and by payer: the client's own credit or your agency), the app meters, the current payer and your budget, to invoice with your own markup or to show in your own client portal.
  • get_client_activity: the client's activity log; what your agency did carries via_bureau with your agency's name.

Who pays. By default the client pays usage from its own credit. Your agency can offer to pay: payer: "agency" on create_client_project or request_client_access, or later:

  • set_client_payer: propose who pays (client or agency), or agree to what the client proposed. It only changes when both sides agree; the client answers in the portal under Agencies, and payer_change shows a proposal that waits. When your agency pays, calls, texts, numbers, AI and other metered use of the client project go on your agency's balance, with the client project as reference. Plan, domains and team stay with the client.
  • set_client_budget: the most your agency pays for one client project per month (euros excluding VAT), or null. When it is reached, new paid usage there gets a 402 billing_blocked and your agency gets one email that month.
  • export_client_invoice_lines: the usage as invoice lines per month, payer and source, as JSON or CSV, for your own accounting. Hypamoon sends no invoice.

Getting paid by your client. On your agency's own payment account (connect it on your agency project first with connect_payments). The money goes to your agency; Hypamoon keeps only the transaction fee you accepted.

  • create_payment_link: one payment. Returns checkout_url; give it to your client yourself.
  • create_subscription: monthly or yearly. The first payment through checkout_url sets up the mandate. Not every payment account supports this yet; a 409 says so.
  • list_client_payments and cancel_client_subscription, and cancel_client_payment_link to withdraw an open link. These also work after your relation with the client ended, so you can stop what still runs.

Handing over and cleaning up.

  • transfer_project: hand over a project in the building stage. With to_email the client gets an email and becomes the owner after accepting; agency_keeps_access says whether your agency keeps working on it or stops. With to_agency_project_id another agency gets an email and takes over after accepting in its portal (Clients); your access ends then. Nothing changes until the receiving side accepts. A project the client already owns cannot be handed over: the client chooses its agencies. The answer lists open_payments.
  • cancel_project_transfer: withdraw an open owner invitation or handover.
  • restore_client_project: bring back a project in the building stage that your agency ended and nobody took over. Such a project has no owner: Hypamoon emails your agency, moves it to the trash after 30 days and deletes it 30 days later; until then you can restore it.

When a relation ends or a project is handed over, your agency stops paying for it (the client's owner gets an email that they pay again), its budget and any open payer proposal go away, and open_payments lists your payment links and subscriptions for that client, which keep running on your own account until you stop them.

Both carry the client project in the metadata. Subscribe a webhook (create_webhook) to client_payment.paid, client_payment.failed, client_subscription.active, client_subscription.payment_paid, client_subscription.payment_failed or client_subscription.canceled; the event never contains the payer's email or name.

Working inside a client project. Pass project (the client's project id) to any other tool, or send the header Hypamoon-Project: <id> on the REST API. Hypamoon checks the active relation on every call. Your agency works there as a builder: no team, no payment details, no buying or transferring domains, no moving to live and no deleting whole sites, apps, collections, domains or numbers. The client's four-eyes approval applies to your deploys (the client can also turn it on for your agency alone), and you only work live if the client ticked that box. A viewer of your agency only reads.

Every tool calls the same REST route as the API reference describes, so the checks, the errors and the results are the same. A failed call comes back as a tool result with isError: true and a text like:

Hypamoon API error 422 company_details_missing: Set the company details (for a personal account: your own contact details) first with PUT /api/v1/project/company. Missing: contact.name, contact.phone.
How to fix: https://hypamoon.com/docs/errors/company_details_missing
Request id: req_...

The agent reads the explanation behind the link and fixes the cause.

A session, step by step

What a good agent does with a test key (the server tells it this in its instructions):

  1. get_project: mode test, and next_steps says what is missing.
  2. set_company_details with values it asked you for: a business, or your own details with account_type personal. You do not need a company.
  3. search_numbers, then order_number with one of the results.
  4. set_assistant with instructions it asked you for.
  5. set_call_plan: your phone first, then the assistant.
  6. get_integration_code, then it builds and deploys the webhook receiver in your app.
  7. create_webhook with the receiver's URL; you store the secret as HYPAMOON_WEBHOOK_SECRET. Then send_test_webhook.
  8. simulate_call, and it checks that your app stored the call; get_call shows the transcript and the summary.

Then you go live. There is no switch: ask for the real thing and it happens once you are verified for it. A tool that still needs something answers verification_required with missing, next_step and verify_url, a link to exactly that step in the portal; nothing was done, and after you finish it the agent calls the same tool again. What the portal asks, at https://app.hypamoon.com:

  1. Verification: you are verified for what you use. A business: the company level unlocks the live project, live keys and national numbers. A personal account: the personal level (your name, your address, a payment method and a cap; no registration number) unlocks live keys, and the identity documents (proof of address and an ID) unlock a number with a local area code, inbound calls and SMS. A national number needs a business. Numbers with a local area code need the number documents, outbound calls and a cap above 500 EUR per month need the full level.
  2. Balance (https://app.hypamoon.com/tegoed): top up the prepaid balance (from 10 EUR, by card or bank payment), or save a card so it tops up automatically. Every live SMS, call minute and number is deducted from it; when it is too low, a request gets 402 insufficient_balance. get_balance shows it.
  3. Spending cap (Billing): set it; the Overview shows costs against it.
  4. A key or a connection that is limited to test stays simulated. With a key: create a live key (API keys) and swap the test key for it, in the connector and in your app's secrets. A normal chat connection needs nothing extra.

Voice AI is always included, on the live project too; there is nothing to switch on. Then repeat steps 2 to 7.

When something goes wrong

Every failed tool call and every API error carries user_message: one calm sentence in plain English that the agent can pass on to the user as it is (the tool text says "User message (plain words you can pass on to the user as is)"). Many also carry next_step for the agent itself.

  • Is Hypamoon down? get_status (any connection; GET /api/v1/status without a key) says per component (sites, API, portal, phone, AI receptionist, email, payments) whether it works and whether there is a reported problem, from https://status.hypamoon.com. Its user_message is, for example, "Hypamoon is working normally. The problem is likely in your chat app." Call it before telling the user that Hypamoon is down or blocked.
  • The upload is blocked. create_upload_urls says in its answer (hint) what to do when the PUT to upload_url fails: send images under ~3 MB as base64 in deploy_site content instead, or create a drop link (create_drop_link, then get_drop). A deploy that names a file whose upload link never received anything gets upload_not_received with the same advice.
  • Known problems. Every get_advice answer has known_problems: an upload blocked by a sandbox, an image too large, version_conflict, phone_verification_required, a site nobody else can see (unlisted or with a password) and "is Hypamoon down", each with what to do and what to tell the user. GET /api/v1/advice?topic=<id> returns one.

create_drop_link, get_drop and get_status are not in ChatGPT while the app is in review; ChatGPT does get user_message in its errors, without the drop link.

Rules

  • Every answer says what to do next. A tool result ends with Next: and a failed call carries next_step (the tool that fixes it), except in ChatGPT while the app is in review.
  • The API key is a server-side secret. It belongs in the connector settings and in your app's secrets, never in browser code, a committed file or the chat.
  • A key or password for another service never goes in the chat. The agent asks for it with request_secret and you paste it in the portal.
  • start_call calls a real person with a live key. The agent tells you who it will call, on which number and why, and waits for your yes before every live call. Outbound calls need the full verification level.
  • Every outbound call has a purpose, recorded with the call: transactional (the default: the person's own question, an appointment confirmation or reminder, an order status, a callback they asked for) or marketing (anything that sells or promotes). In the Netherlands a marketing call needs the person's prior consent, since 1 July 2026 also for existing customers, so marketing only calls when consent is recorded with record_call_consent; otherwise consent_required. ChatGPT does not see the field while the app is in review; its calls are transactional.
  • A public chat token (hm_pub_..., from create_chat_widget) is not a key: it is meant for the page source of the website and can only chat from the origins you allowed. It never replaces the API key in server code.
  • Every request counts against the rate limit of the key (or, with OAuth, of the connected app); a tool call counts twice (the MCP request and the API route behind it).
Hypamoon is the infrastructure your chat or agent builds on for a business. Hypamoon API, version 1.hypamoon.com