REST API
Work with sites, files, revisions, domains, and bandwidth from your own tools.
Read as Markdown · AI documentation indexOn this page
Publish a site, make changes, and roll back from your own tools. The REST API is available now at https://app.ironfountain.com/api/hosting/v1.
For ChatGPT or Claude on the web, start with Connect ChatGPT or Claude. This page covers direct REST requests from scripts and other HTTP clients.
The product namespace is /api/hosting/v1. Iron Fountain sign-in and organizations stay shared. Existing /api/v1 requests continue to reach the same Hosting handlers without a redirect; new integrations should use the product URL. Keys grant only their listed Hosting permissions and do not grant access to future products.
Quick start
Create an API key in your organization’s API keys screen. Keys default to the four site, deployment, and domain permissions and never expire; billing and domain-removal permissions require an owner’s explicit selection. You can narrow permissions, choose an expiry, or revoke a key there. Each key belongs to one organization and follows its creator’s membership.
Store the key in your tool’s secret settings or a terminal environment variable named IRONFOUNTAIN_API_KEY. Keep it out of published website files and browser JavaScript.
curl https://app.ironfountain.com/api/hosting/v1/me \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY"
The response includes your organization in accounts. Its id is the accountId used in API paths. The API uses accounts and account_id for what the app calls organizations.
curl https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY"
Test sites are temporary imports, with a one-hour lifetime. Saving one to an organization retires its test URL immediately. Staging is a saved site’s permanent place to review changes. Production is the published site. Revision links open exact saved revisions and follow history retention. The existing preview_url field refers only to a temporary test site; do not call it staging.
Site lists and details return production_url (null before publication), staging_url, and staging_revision_id. Staging has a permanent address, such as my-site.staging.ironfountain.net, showing its current revision; it does not imply publication. Each retained revision also has its own immutable revision_url, including in save and restore responses. Registered sites never use temporary test-site URLs.
Replace ACCOUNT_ID and SITE_ID in examples with the IDs returned by the API. Requests that send JSON require Content-Type: application/json.
Permissions
| Permission | What it allows |
|---|---|
sites:read | Read sites, files, revision history, import reports, DNS status, and storage usage. |
sites:write | Create sites, change site settings, and manage hostname-specific file or path redirects. |
deployments:write | Import, upload, publish, restore, pin/unpin, delete revisions or snapshots, and edit redirects saved with revisions. |
domains:write | Prepare and verify domains and check their connection. Removal requires the separate domains:delete permission. |
domains:delete | Remove main or redirect root/www pairs; opt-in, current owners only, with exact-root-hostname confirmation. |
billing:read | Read shared payment-method summaries and invoices; current organization owners only. |
billing:write | Select complimentary plans, quote and confirm paid subscriptions, manage cards, cancel, and resume renewal; current owners only. Can incur real charges. |
Existing credentials keep their scopes. Create a new API key to add permissions. MCP has the same Hosting workflows and scopes, but its connection belongs to your account and can span explicitly approved organizations and sites. Each REST key belongs to one organization.
Permissions are independent. A tool that reads the current files and publishes changes needs both sites:read and deployments:write. Permanent site deletion, organization and member administration, site transfers, passwords, and API key management use signed-in dashboard sessions. Domain setup permission does not include removal; existing credentials do not gain domains:delete.
Create or read a site
Create a site with POST /accounts/ACCOUNT_ID/sites:
{"name":"My website","slug":"my-website"}
The optional slug gives the site its free address, such as my-website.ironfountain.net. Omit it to choose an available address automatically from the name. Site names can contain spaces; explicit slugs use 3–40 letters, numbers, and single hyphens between words. Sites serve static files, directory indexes, and an optional 404.html for missing pages.
GET /accounts/ACCOUNT_ID/sites/SITE_ID returns site, domains, deployments, revision_history, pinned_revisions, snapshots, redirects, and revision_redirects. site.active_deployment_id identifies the published revision, or is null before the first publication.
Edit and publish safely
First read the current site, then its revision manifest with GET /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID. Read each file with GET /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/file?path=index.html. File responses contain base64-encoded content; decode it before editing.
Send the complete updated file set to POST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments. Files omitted from the upload are absent in that revision.
{
"label": "Update homepage copy",
"expected_deployment_id": "CURRENT_REVISION_ID",
"publish": true,
"files": [
{"path": "index.html", "content": "<!doctype html><h1>Welcome</h1>", "encoding": "utf8"},
{"path": "styles.css", "content": "body { color: #234; }", "encoding": "utf8"}
]
}
Save that JSON as revision.json, then send it:
curl https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/deployments \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @revision.json
Use expected_deployment_id: null for a new site. Supplying the current revision ID makes a stale update fail with 409 instead of overwriting someone else’s changes. Read the latest files and reconcile the edits before retrying. publish: false updates staging without changing production. Include expected_staging_deployment_id to reject concurrent staging edits. Production publication and rollback leave staging unchanged; a first direct publication initializes staging if it is empty. Renaming staging requires sites:write, returns 409 for a taken name, and accepts a valid DNS label of 1–63 characters. Site details include separate staging and production history.
A root index.html is optional. Sites may serve only specific paths, and an empty files array can publish an empty revision. Missing paths return 404. Use unique relative paths and encoding: "base64" for binary files, or upload them separately and reference them by hash (below). A JSON upload accepts up to 20,000 files, 100 MB of supplied content, and 5 MB per supplied file. The complete site can contain 20,000 files and 1 GB. To edit a larger imported site, pass base_deployment_id with the retained revision ID and supply only changed files. Unchanged files are reused on the server. Optional deleted_paths removes files from that base; paths cannot be both changed and deleted. The base must belong to this site. Published changes may take a moment to appear in cached pages.
Upload a binary asset
Images, fonts, video and other binary files can be sent as raw bytes instead of base64. Ask for an upload link, PUT each file to it, then name the returned hash in a revision. This keeps large assets out of your JSON payload, and out of an AI assistant's context when the assistant is the one doing the upload.
LINK=$(curl -s https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/upload-tickets \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY" -X POST -d '{}' \
-H "Content-Type: application/json" | jq -r .upload_url)
HASH=$(curl -s -X PUT --data-binary @logo.png "$LINK" | jq -r .hash)
The link is the whole credential, so the PUT sends no authorization header. It is valid for 30 minutes, names one site, and may be reused for as many files as you like; each PUT returns the hash and bytes of the file it stored. Each file must be 5 MB or smaller. Issuing a link requires deployments:write.
Reference the hash where you would otherwise put content:
{"label":"New logo","files":[{"path":"logo.png","hash":"HASH"}],"publish":false}
Give each file either content or hash, never both. Uploaded bytes that no saved revision references are discarded within the hour, so upload and save in the same session. Text files are better sent as content, where compression and text replacements apply.
A hash may name any file this site already stores, including one from an earlier revision, so a file can be added at a second path without sending it again. To bring a file in from a different site, copy it first:
curl https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites/DESTINATION_SITE_ID/asset-copies \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY" -H "Content-Type: application/json" \
-d '{"source_site_id":"SOURCE_SITE_ID","paths":["logo.png"]}'
The copy happens on the server; the bytes are never sent or returned. source_revision_id defaults to the source site's live revision, and the response gives each file's hash for a revision on the destination site. You need deployments:write and access to both sites, at most 50 files per request. Copied bytes follow the same rule as uploaded ones: reference them from a saved revision within the hour.
Caching and compression
Visitors receive HTML, CSS, JavaScript, JSON, SVG and other text files compressed automatically (zstd or gzip, as each browser supports). Upload files uncompressed; do not add .gz or .br copies or set encodings yourself.
Production files are served with Cache-Control: public, max-age=0, s-maxage=60 and an ETag. Browsers check each file again when it is used, so published changes reach visitors within about a minute.
A file named by its own content hash is served with Cache-Control: public, max-age=31536000, immutable, so browsers keep it for a year without checking. The hash is the first 24 hexadecimal characters of the SHA-256 of the file's exact bytes. It must come directly before the file's extension, either as the whole name or after ., - or _, in any folder: assets/styles.34af0da141c4c9f637141b99.css, fonts/brand-34af0da141c4c9f637141b99.woff2 and imported-assets/34af0da141c4c9f637141b99.css all qualify when the hash matches. Other names, including a build tool's own shorter hashes, do not. The hash is checked on every request, so a file edited in place under an old name is served with normal caching, never stale. New imports name copied builder files this way.
To give a new or edited stylesheet, script, font or image the long lifetime, compute the SHA-256 of its final bytes, save it under a name carrying that hash, and update every reference to it. Pages keep their normal addresses and caching. Missing files and staging, test and revision addresses never receive the long lifetime.
Promote staging to production
Staging is where a change is reviewed before it goes live. Promoting it points production at the exact revision you reviewed. Nothing is copied or rebuilt, so production serves the same files, redirects, and forms you checked on the staging address.
- Read the site with
GET /accounts/ACCOUNT_ID/sites/SITE_ID. Notestaging_revision_id(the revision on the staging address) andactive_deployment_id(production, ornullif the site has never been published). - Review the staging address,
staging_urlin the same response. - Publish that staging revision:
curl https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/staging/publish \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY" \
-H "Content-Type: application/json" \
--data '{"staging_revision_id":"STAGING_REVISION_ID","expected_production_revision_id":"CURRENT_PRODUCTION_ID"}'
staging_revision_id is required. If staging has changed since you read it, the request fails with 409 and nothing is published; review the new staging revision and try again. expected_production_revision_id is optional. With it, the request also fails with 409 if production changed since you read it; use null for a site that has never been published. The key needs deployments:write.
The response has published: true, the revision_id now in production, production_url, and staging_url. Staging keeps the same revision, so both addresses serve identical content until the next staging change. To undo a promotion, publish the previous production revision from History and rollback below.
Reset staging to production
Resetting goes the other way: it points staging at the revision production is serving, so you can start new work from exactly what is live. Nothing is copied, and production is unchanged.
- Read the site with
GET /accounts/ACCOUNT_ID/sites/SITE_ID. Noteactive_deployment_id(production) andstaging_revision_id. - Reset staging to that production revision:
curl https://app.ironfountain.com/api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/staging/reset \
-H "Authorization: Bearer $IRONFOUNTAIN_API_KEY" \
-H "Content-Type: application/json" \
--data '{"production_revision_id":"PRODUCTION_REVISION_ID","expected_staging_revision_id":"CURRENT_STAGING_ID"}'
production_revision_id is required and must be the current production revision. If production has changed since you read it, the request fails with 409 and staging is unchanged. expected_staging_revision_id is optional. With it, the request also fails with 409 if staging changed since you read it. A site that has never been published has nothing to reset to. The key needs deployments:write.
The response has published: false, the revision_id now on staging, staging_url, and production_url. The replaced staging revision stays in staging history; to get it back, revert staging to it from History and rollback below.
History and rollback
| Operation | Request |
|---|---|
| List saved revisions | GET /accounts/ACCOUNT_ID/sites/SITE_ID |
| Read a revision manifest | GET /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID |
| Promote staging to production | POST /accounts/ACCOUNT_ID/sites/SITE_ID/staging/publish with {"staging_revision_id":"STAGING_ID"} (see Promote staging to production) |
| Publish or restore a saved revision | POST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/restore with {} |
| Reset staging to production | POST /accounts/ACCOUNT_ID/sites/SITE_ID/staging/reset with {"production_revision_id":"PRODUCTION_ID"} (see Reset staging to production) |
| Revert staging | POST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/stage with {"expected_deployment_id":"CURRENT_STAGING_ID"} |
| Rename staging | PATCH /accounts/ACCOUNT_ID/sites/SITE_ID/staging with {"slug":"secret"} |
| Pin a revision | PATCH /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID with {"pinned":true} |
| Unpin a revision | The same request with {"pinned":false} |
| Read storage usage | GET /accounts/ACCOUNT_ID/sites/SITE_ID/storage |
History is automatic, with separate allowances for recent revisions and pins:
| Plan | Recent revisions per environment | Pinned revisions per site |
|---|---|---|
| Spring | 3 | 0 |
| Flow | 20 | 10 |
| Cascade | 50 | 25 |
site.history_policy returns plan_name, keep_per_environment, pin_limit, can_pin, monthly_months, session_days, and recovery_until (Unix milliseconds, or null). Pinning does not reduce recent history. Pins are shared across the site's two environments. Spring rejects pin requests with 403; a full pin allowance returns 409. Unpinning remains available. Pinning an already pinned revision does not use another slot.
Flow and Cascade also have monthly snapshots lasting 12 months, plus before-changes snapshots lasting 30 days when changes resume after at least 24 hours of inactivity in that environment. Each snapshot includes snapshot_id, target revision id, channel, kind (monthly or before_edit), as_of, expires_at, revision created_at, and revision_url. Monthly snapshots use the revision that was current at the start of the calendar month (UTC), even if it was created years earlier. Several snapshots can refer to the same revision. Restore a snapshot using its target revision id with the existing stage or restore endpoint.
| Action | Request |
|---|---|
| Delete a snapshot | DELETE /accounts/ACCOUNT_ID/sites/SITE_ID/snapshots/SNAPSHOT_ID |
| Delete an unused revision | DELETE /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID |
Both require deployments:write. Deleting a snapshot leaves the revision available if it is still retained elsewhere. Revision deletion returns 409 while current in staging or production, pinned, referenced by any retained snapshot, uploading, or used by an unexpired test site. Current revisions and all snapshot targets are protected from automatic cleanup.
If a paid downgrade reduces the pin allowance, existing pins stay protected but no additional pins can be added until usage falls below the new cap. Every reduction in history allowances protects existing revisions, pins, and snapshots for a 30-day recovery window. New history follows the new plan. revision_history includes recovered revisions beyond the recent allowance, with a recovery_until deadline; recovered pins and snapshots stay visible too. After that deadline, normal retention resumes. Canceling at period end starts recovery when paid access ends, not when cancellation is requested. See automatic history and snapshots for details.
Import an existing website
Use GET /imports/lookup?inspect_redirects=true&url=https%3A%2F%2Fexample.com before creating another site. It can return action: "new", "resume", or "manage". An existing result includes a browser href; authorized results may include site_id and account_id. Redirect-only addresses leading to a recognized site open that existing site instead of copying it again. Looking up a redirect does not connect or verify its domain.
Import creation and refresh require the initiating user’s verified email, including API keys and MCP connections. Verify email in Your settings before importing; unverified requests return 403 before contacting the source website.
For a new import, create a site and send POST /accounts/ACCOUNT_ID/sites/SITE_ID/imports:
{
"url": "https://example.com/",
"rights_confirmed": true,
"terms_version": "VERSION_FROM_LEGAL_TERMS"
}
Importing requires permission from the content owner. Read the current Terms and GET /legal/terms before affirming it: send its version field as terms_version, and show the user its declaration. A stale version returns 400. Every new import and refresh requires this agreement.
An already connected site rejects new imports with 409 before fetching the source. Import into an empty site; an unfinished migration can explicitly refresh its same source with refresh: true and expected_import_id.
Optional extra_paths lists up to 100 paths, for example ["/downloads/guide.pdf", "/old-page"], for files and redirects absent from links and sitemaps. The dashboard and test forms place this under Advanced import options. The same crawl limits and public-network checks apply. Omitting extra_paths during refresh preserves the previous list; sending [] clears it. Progress responses include stage and, during saving, report.saving.saved_files and report.saving.total_files.
A new job returns 202 with an import id. An already recognized site returns 200 with a resume or manage action. Use GET /accounts/ACCOUNT_ID/sites/SITE_ID/imports to poll progress and obtain the compatibility report, permanent staging_url, and revision_url for the exact imported revision. Import limits are 200 pages, 20,000 files, 1 GB total, 100 MB per file, and fifteen minutes. Paid sites can start ten imports per site per hour, with up to two imports in progress per organization. Free imports also have a shared ten-import hourly allowance. Imports inspect public static content; test interactive features in the preview.
A queued import has waiting_reason (queue with queue_position, retry_wait with retry_at, site_busy, workers_starting or workers_unavailable). Temporary problems (connection_timeout, connection_failed, storage_unavailable, site_busy, import_interrupted, import_worker_unavailable) are retried automatically up to three attempts; while one waits, report.retrying is true with retry_reason, attempt, max_attempts and retry_at. If the attempts run out, that code is the blocking finding, and refreshing the import with refresh: true is likely to work. A source the importer cannot read fails with source_challenge, source_blocked, source_rate_limited, source_auth, source_status or source_not_html.
Temporary test sites require a free account and verified email before preview access. They expire one hour after importing starts, or immediately when saved to an organization. Registered sites have a permanent staging_url plus exact revision_url links. Staging does not have a one-hour deadline; retained revision links remain available while that revision is kept. Both are excluded from indexing. Test the copy and review the report before publishing with POST /accounts/ACCOUNT_ID/sites/SITE_ID/imports/IMPORT_ID/publish and {"reviewed":true}. Blocking findings must be resolved before publication.
To refresh a saved site while its custom domain remains unverified, send another import request with refresh: true and expected_import_id set to the latest import ID. A refresh stages changes on the same site and preserves published files until reviewed and published. Identical content reuses the prior revision. Once verified, edit through file deployments instead of reimporting the hosted website.
Wix and Squarespace imports are not supported yet. Detected sites receive a blocking unsupported_wix or unsupported_squarespace finding, a failed import job, and no new revision. A rejected refresh preserves existing staging and production. Webflow and other public static websites remain eligible, subject to the usual compatibility and size checks.
Import findings retain level, code, message, and the first affected url. Repeated matching explanations may also include count (distinct affected resources) and urls (up to 24 examples); missing count means one. Different messages, including different source HTTP statuses, remain separate groups. Reports retain up to 150 explanation groups and may include omitted_findings if further explanations could not be retained. Form-handler review entries include next_step; use that guidance and the page link when helping an owner resolve an import issue. An external_script_assets finding means a preserved external script references builder-hosted files that the importer cannot rewrite inside that external service.
Single-page applications (SPAs) are not supported yet. Detected client-rendered app shells or client-side routing produce a blocking unsupported_spa finding and no new staging revision. Detection does not execute JavaScript; a successful static scan still requires reviewing the copy. Ordinary JavaScript menus, forms, and animations are not rejected just for using JavaScript.
Custom domains and HTTPS
Custom domains currently support a root domain and its www address as a pair. Either can be the main address; the other redirects automatically with paths and query strings preserved.
Prepare the pair with POST /accounts/ACCOUNT_ID/sites/SITE_ID/domain-setup:
{"hostname":"example.com","primary":"www"}
Read GET /accounts/ACCOUNT_ID/sites/SITE_ID/dns for the root certificate TXT challenge, ownership, and connection state. Add all records returned in certificates; read the supplied names and values rather than constructing them. Iron Fountain obtains the certificates directly from Let’s Encrypt. The fresh root challenge normally also proves ownership of the pair. ownership.method is pending, acme, verified, or txt when an additional fresh ownership record is needed. Checks run automatically; connection.next_check_at is the next check in Unix milliseconds. Repeated requests do not bypass the schedule. When Let’s Encrypt refuses a certificate, the connection's certificate_problem gives the hostname, kind (caa, rate_limited, dns, validation, rejected or other), a detail without secrets, attempts, and action_required, which is true when the customer must change something (such as a CAA record) before it can succeed. A CAA record that doesn't allow Let’s Encrypt is detected before any request to it and rechecked every two minutes, so fixing it takes effect quickly. Other refusals are retried with a growing delay; connection.message says what to do, and the last failure stays visible while a fresh TXT value is pending. Validation starts only once every nameserver that answers for the domain has the TXT value.
Keep the old website’s A/CNAME records until connection.dns_switch_ready is true: ownership and trusted root HTTPS have passed. Then use gateway_ip for the root A record and cname_target for the www CNAME; cname_target is null until then. Both addresses resolve to Iron Fountain’s gateway; website URLs remain unchanged. connection.phase: "active" means DNS and HTTPS passed and the primary address is active. The www certificate is issued automatically over HTTP after its CNAME resolves to our gateway; no www TXT record is needed. www HTTPS may be briefly unavailable during issuance. Keep old hosting available while DNS caches expire. DNS routing alone never verifies ownership. Certificates renew automatically through HTTP after cutover, without recurring TXT updates. connection.certificate_status reports each hostname’s expiry, renewal schedule, retry time, and any renewal error.
Additional redirect domains
In the dashboard, open a site → Domains → Add redirect domain. Enter another root domain, such as example.net. Both example.net and www.example.net redirect to the existing site’s main address. No second website or import is created. A site supports up to 20 additional root/www pairs.
The API requires domains:write:
POST /api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/redirect-domains
Content-Type: application/json
{"hostname":"example.net"}
Publish the site and connect its main custom domain first. Repeating this request resumes the same connection without replacing its ownership token or restarting the check countdown. Read redirect_connections from GET …/dns for each root’s status, certificate challenges, next_check_at, dns_switch_ready, and hosting_ready. Each redirect connection includes its own ownership instructions. Its fresh root certificate TXT verifies ownership of that pair; every additional domain needs its own proof.
Follow the same order as the main domain: add the displayed root certificate TXT record, then the root A and www CNAME after ownership and root HTTPS pass. www HTTPS is issued automatically after its CNAME points here; no www TXT record is needed. Use that redirect connection’s cname_target and the shared gateway_ip. Keep existing website DNS records until dns_switch_ready is true. Each pair progresses and renews independently.
Connected redirect domains return HTTP 308 to the site’s current main hostname, preserving the path and query string. They follow later main-address changes. They cannot be selected as the primary hostname or have individual path redirects.
Remove a pair in the dashboard, or with an owner credential explicitly granted domains:delete: send DELETE …/redirect-domains/example.net with JSON {"confirm_hostname":"example.net"}. HTTP 202 means removal is queued. The entry remains phase: "removing" until disconnection finishes; wait for completion before reconnecting the domain. Both root and www are removed together. The main domain, site content, and revision history remain attached.
File and path redirects
Send POST /accounts/ACCOUNT_ID/sites/SITE_ID/redirects with the ID of an attached hostname:
{
"domain_id": "DOMAIN_ID",
"source_path": "/old-page",
"destination": "/new-page",
"status_code": 301,
"preserve_query": true
}
Rules match an exact path on that hostname and take effect immediately. They are not part of any revision, so publishing, staging and rollback leave them unchanged, and they never run on staging, test site or revision addresses. Destinations can be a local path or an external HTTPS URL. Status codes are 301 or 302. With preserve_query: true, the visitor's query string (for example utm_source) is added to the destination; a parameter the destination already sets keeps the destination's value. Without it, the visitor's query string is dropped and only the destination's own query is sent. See Choosing a redirect type. Edit a rule with PATCH /accounts/ACCOUNT_ID/sites/SITE_ID/redirects/REDIRECT_ID, sending the same fields as creation. Remove it with DELETE at that address. Whole extra-domain redirects are not automatically created by importing a redirect address.
Claude, ChatGPT, and other tools
Use your own AI account with a tool that can make authenticated HTTP requests. Give it this reference, the organization and site IDs, and access to the API key through its secret settings. Ask it to read the current revision, preserve every unchanged file, make your requested edits, and publish with expected_deployment_id. Every published revision remains a rollback point while retained.
For ChatGPT or Claude on the web, use the hosted MCP connector at https://app.ironfountain.com/mcp/hosting. It uses Iron Fountain sign-in and OAuth access to the organizations you approve instead of a pasted API key. See Connect ChatGPT or Claude for setup and the tool reference. An organization owner may need to enable the connector in Claude; code execution egress settings do not govern enabled MCP connections. Other HTTP tools can continue using the REST API above.
Redirects in revisions
The site detail response includes revision_redirects for the current production and staging revisions, alongside the hostname-specific redirects list. Each revision entry contains environment, revision_id, and redirects (from, to, status). Rollback changes this list to the selected revision’s rules. The dashboard lists production and staging separately, each with its own Edit and Remove controls. Editing or removing a rule saves a new revision only in the selected environment, preserving its other files and leaving the other environment unchanged. The old revision remains available for rollback. API clients can do the same using the existing deployments endpoint: choose that environment’s revision as base_deployment_id, replace __ironfountain/redirects.json with the remaining rules (or delete that file via deleted_paths when none remain), set publish for the selected environment, and include its expected_deployment_id or expected_staging_deployment_id to reject stale edits.
The importer preserves discovered path redirects, including those found through extra_paths. It cannot discover unlinked paths that were not supplied. Root/www normalization remains part of domain setup. Local redirect destinations stay on staging or the current production hostname; external destinations remain external. External redirect targets are not crawled as new websites.
Redirect definitions are retained in __ironfountain/redirects.json. API and MCP file editing can manage this file with the other revision files:
{"version":1,"redirects":[{"from":"/old-page","to":"/new-page/","status":301},{"from":"/downloads/current.pdf","to":"https://files.example.com/current.pdf","status":302},{"from":"/privacy","to":"/legal/?section=privacy","status":301,"preserve_query":true}]}
These rules apply to that revision in test sites, staging, revision URLs and production. Publishing and rollback switch content and redirects together. Rules use exact paths (with an optional exact query string), support 301/302/303/307/308, and cannot contain redirect loops. By default the visitor's query string is dropped: /old-page?utm_source=news goes to /new-page/. Set "preserve_query": true on a rule to add the visitor's query string to the destination, so /privacy?utm_source=app goes to /legal/?section=privacy&utm_source=app. A parameter the destination already sets keeps the destination's value. Each rule's preserve_query appears in revision_redirects when set. The file allows up to 2,000 rules and 2 MB. Existing hostname-specific rules configured through /redirects take precedence. Normal root/www and secondary-domain redirects still run first.
Choosing a redirect type
Iron Fountain has two kinds of path redirect. Use one kind for each path; if both define the same path, the hostname rule wins on its hostname and the revision rule only applies elsewhere, such as staging.
Revision redirects (__ironfountain/redirects.json) | Hostname redirects (…/redirects) | |
|---|---|---|
| Applies to | Every address serving that revision: production hostnames, staging, test sites and revision links | One hostname only, such as www.example.com; not its root pair, other domains or staging |
| Takes effect | When the revision is published (or staged) | Immediately |
| Rollback | Restoring a revision restores its redirects | Unaffected by publishing and rollback |
| Visitor query string | Dropped unless the rule sets preserve_query | Dropped unless the rule sets preserve_query |
| Status codes | 301, 302, 303, 307, 308 | 301, 302 |
| Source | Exact path, optionally with an exact query string | Exact path |
Prefer revision redirects so changes can be tested on staging and rolled back with the content. Use a hostname redirect when one hostname needs a different rule from the others, or a change must bypass publishing.
Errors
Errors return JSON containing error. Common statuses are 400 for invalid input, 401 for missing or invalid authentication, 403 for insufficient permission, 404 for unavailable resources, 409 for a conflict or an already-hosted source, 413 for size limits, and 429 for request or import limits.
Where the server knows more, the error also has code, and a limit has retry_at (epoch milliseconds) with a Retry-After header. A declined card returns 402 with the card issuer's message. The MCP connector passes code, retry_at and retry_after_seconds through in tool errors.
A website already hosted by an organization you cannot access returns 409 with code: "already_hosted" and does not expose that organization’s identity. Sign in with the appropriate login to manage it. Organization IDs, site IDs, and revision IDs stay stable; a site transfer changes which organization’s keys may access it.
For concurrency-safe publication or rollback through the restore endpoint, send {"expected_deployment_id":"CURRENT_LIVE_REVISION_ID"} (or null for an unpublished site). A mismatch returns 409. Omitting the field preserves the earlier restore behavior.
Import compatibility and download limits
The importer evaluates whether public page output can be captured as a usable static snapshot. A database or server-rendered source alone does not disqualify a site. Public HTML from server-style routes and finite query URLs can be saved; query pages receive static paths and captured links are rewritten. Incoming query URLs from outside the copy need migration review because they do not automatically map to those new paths.
Imports support up to 200 pages. Account pages discovered after the public starting page are skipped, including login, registration, and password reset. report.skipped_pages lists their URLs and reasons; the import saves the remaining site with a needs_review verdict. Skipped pages and redirect aliases are removed from copied sitemap entries, and links to them are flagged for review. An account/login starting page, unsupported SPA, exceeded budget, or incompatible resource content remains a blocker. Secondary pages/files returning an HTTP error are omitted and listed as missing_resource warnings. This gives complete: false with verdict: needs_review; a reviewed copy can still be published.
Forms, runtime network calls, and embedded services are flagged for review. They do not by themselves prove that the visible page cannot be hosted statically. Test which behaviors need a live service, user state, new searches, transactions, or fresh data before publishing. Review the imported site’s interactive features before publishing; the compatibility report cannot verify every behavior.
Imports support 200 pages, 20,000 files, 100 MB per file, 1 GB of saved content, and a fifteen-minute time limit. An import that exceeds these limits cannot complete.
Site bandwidth
GET /api/hosting/v1/accounts/ACCOUNT_ID/sites/SITE_ID/bandwidth?month=YYYY-MM requires sites:read. The month is optional and defaults to the current UTC month. The response includes monthly totals, daily rows grouped by hostname and source, and sources showing collection timestamps and errors. Site list responses also include month_bandwidth_bytes and month_bandwidth_requests for dashboard cards.
Bandwidth includes traffic served from your site’s public addresses, including cached responses. Use the reported monthly total to track usage against your plan.
Usage graphs normally trail traffic by ten to fifteen minutes and may take longer to update during service delays. The API’s sources metadata reports when usage was last updated and any collection errors.
Staging and temporary addresses block indexing. Free production addresses retain their own robots rules; once the primary custom domain is active, the free address redirects to it instead of serving an indexable mirror.
Forms and submissions
Forms work with ordinary HTML and any site builder. Compatible forms are detected during import and connected to Iron Fountain. See the Forms guide for HTML examples, manifest authoring, custom browser clients, and dedicated MCP tools. Form definitions are stored with each immutable revision in __ironfountain/forms.json. Publishing or rolling back changes the form definitions with the site. Saved submissions remain independent of revision retention. Custom third-party form actions remain configured with their original service and require review.
Submission fields are JSON objects; multiple selections use arrays. Attachments are private and require authorized access to the site.
All routes below are relative to /api/hosting/v1/accounts/:accountId/sites/:siteId.
| Method | Route | Permission | Behavior |
|---|---|---|---|
| GET | /forms | sites:read | List forms, current revision availability, per-environment form_handlers, unread counts, and usage. |
| POST | /forms/connect | deployments:write | Connect recognized or explicitly opted-in HTML forms, synchronize fields, and refresh the form handler review in staging. Optional body: expected_staging_revision_id for concurrency. Does not recrawl or publish. |
| PATCH | /forms/:formKey | sites:write | Set enabled to pause or resume a form. |
| GET | /form-submissions | sites:read | List up to 50 submissions, their fields, and attachment metadata. |
| PATCH | /form-submissions/:id | sites:write | Set read (boolean) to mark read or unread. |
| DELETE | /form-submissions/:id | sites:write | Delete a submission and revoke attachment access immediately. |
| GET | /form-attachments/:id | sites:read | Download an attachment as a file. Authentication is required. |
| GET | /form-submissions.csv?form=:formKey | sites:read | Download all matching submissions for one form. Each field has a column, with original JSON included to preserve older field formats. |
List and CSV filters: form, environment=production|test, unread=true, and q to search field values. Lists return a next cursor; pass it as before for the next page.
Staging and saved revision addresses save to the Staging submissions inbox. Temporary test sites simulate submissions and file selection without storing submitted data. Staging and test sites remain blocked from indexing. Production submissions use the Production inbox. Forms share the site's organization membership, including after a site transfer.
A form supports up to 100 fields and 20 attachments, with 10 MB per file and supported document, image, audio, video, and ZIP extensions. The site has a 10 GB attachment allowance and room for 10,000 saved submissions; submissions are retained until deleted. JSON submissions are limited to 64 KB. Production forms support up to 2,000 submission attempts per site per day. Testing has a separate daily allowance of 200 attempts. Additional rate limits protect forms from excessive requests.
Each environment has one inbox. Form validation and spam protection are included; no CAPTCHA setup is required. Verified members can enable production form email alerts in the dashboard’s Forms tab. Alerts link to the private inbox and are batched: each member gets at most one email per site every 15 minutes, and submissions that arrive in between are counted into the next one. Staging submissions do not send email. Existing third-party submissions and notification settings are not imported.
Imported pages submit to same-origin /__ironfountain/forms/:formKey/submit using the bundled form script. The script obtains a short-lived signed session and uploads each attachment separately before submitting its IDs. Never put an account API key in a public form. Standard API and MCP file editing can update the versioned form manifest and HTML together.
Organization billing
The dashboard’s Billing navigation lists every site plan, the organization’s shared card, and its invoices. Choose Manage plan for a site, then Change plan. Plans are per site; the saved card is shared across the entire organization.
GET /accounts/ACCOUNT_ID/billing returns sites with their billing state and profile with the shared payment method summary and recent invoices. It requires current organization ownership and billing:read for API credentials. MCP connections additionally need all-site access for that organization because the profile and invoices are shared. Payment and complimentary-plan changes require ownership and billing:write. Existing keys and connections do not receive these scopes automatically.
For a site that already has complimentary coverage, an owner with billing:write can select an eligible plan using POST /accounts/ACCOUNT_ID/sites/SITE_ID/billing/complimentary-plan with {"plan_id":"plus-50"}. It changes the plan without creating a charge. The response’s complimentary_max_plan_id gives the highest eligible plan, or null if coverage is uncapped. Plans above that limit return 403. This endpoint cannot grant complimentary coverage. Bandwidth increases take effect immediately; a reduction preserves the allowance already granted for the current calendar month. History follows the selected plan.
Organization billing also returns complimentary, either null or {max_plan_id,max_plan}, when the organization has complimentary coverage. Sites in a covered organization start on Flow. Check each site’s billing response for its current coverage and eligible plans.
Form handler detection preserves explicit external destinations. Current-server and matching-provider dependencies, and unrecognized submission code, are reported for review instead of automatically connected. See the forms guide for provider coverage and the handlers metadata.
Import addresses must be a valid root domain or its www address, with an optional http:// or https:// prefix and trailing slash. Other subdomains, paths, query strings, credentials, ports, IP addresses, and malformed hostnames are rejected before source inspection or import creation. This checks address syntax; DNS and site availability are checked afterward.
Plan selection and subscription lifecycle
GET /plans returns the published plan IDs, bandwidth allowances, monthly and annual prices in USD cents, and the current Terms version. GET /accounts/ACCOUNT_ID/sites/SITE_ID/billing needs sites:read and returns the effective complimentary ceiling, subscription status, period_end, cancel_at_period_end, and billing_url. New sites in a paying organization start on Spring; each site needs its own subscription. Complimentary organizations include new sites on Flow, with plan selection up to the effective grant maximum.
All paths below begin with /accounts/ACCOUNT_ID/sites/SITE_ID/billing. Browser owners may use the same handlers; API credentials must have the listed scope and current ownership.
| Method and suffix | Scope | Purpose |
|---|---|---|
GET /profile | billing:read | Organization payment-method summary and recent invoices. |
POST /complimentary-plan | billing:write | Select plan_id within complimentary coverage without a charge. |
POST /quote | billing:write | Quote plan_id, interval (month or year), and optional promotion_code. No charge. |
POST /confirm | billing:write | Confirm the reviewed quote as shown below. May incur a charge. |
POST /payment | billing:write | Inspect or resume an already approved payment. |
POST /refresh | billing:write | Reconcile subscription state with the payment provider. |
POST /cancel | billing:write | Cancel at period end; an incomplete first subscription ends immediately. |
POST /resume | billing:write | Undo scheduled cancellation and continue recurring charges. |
POST /card/setup | billing:write | Begin secure card setup; returns a SetupIntent and billing_url. |
POST /card/save | billing:write | Save a completed setup_intent_id with save_for_recurring: true. |
A quote includes id, amount_due, currency, recurring amounts, interval, promotion details, expires_at, and terms_version. Show the full charge and recurring terms to the user. After approval, confirm with the same credential that requested the quote:
{
"quote_id": "QUOTE_ID",
"accepted_terms": true,
"expected_amount_due": 13200,
"expected_currency": "usd",
"terms_version": "VERSION_FROM_QUOTE"
}
Copy the amount, currency, and version from the actual quote. Wrong amounts or terms, expired quotes, and changed prices are rejected before starting a payment. Quotes are bound to the site, organization, user, billing mode, and credential. A different key, MCP connection, or browser cannot confirm that quote. After an uncertain response, inspect billing and pending payment instead of creating and confirming another quote.
A successful confirmation can still return requires_action: true. Send the user to billing_url to complete secure payment confirmation or bank authentication; do not report the subscription as paid until the state confirms it. Card numbers and security codes belong in Stripe’s secure fields, never API or MCP arguments. MCP access limited to selected sites can manage those sites’ plans using the organization’s saved card, but cannot read shared profiles/invoices or set/change the shared card; those require all-site access.
Cancellation returns the actual period_end. Paid access continues until then. When a lower plan takes effect, existing history has a 30-day recovery window; new history follows the lower plan. Canceling today does not immediately downgrade an active paid subscription.
Site and domain removal
Domain removal is separate from setup. An organization owner must explicitly grant domains:delete to an API key or MCP connection; it is unchecked by default and never added to existing credentials. The server checks current ownership and the approved organization/site on every removal.
For the main root/www pair, send DELETE /accounts/ACCOUNT_ID/sites/SITE_ID/domains/example.com. For an additional redirect pair, send DELETE …/redirect-domains/example.net. Both require JSON with the exact root hostname: {"confirm_hostname":"example.com"} for the first example. Confirming the www hostname, a different name, or omitting confirmation returns 400. The main endpoint accepts either root or www in its URL, but confirmation always names the root because both addresses are removed. domains:write alone returns 403.
Removal disconnects both root and www from this site. It can stop those addresses serving or redirecting here. Customer DNS records are not edited. Poll GET …/dns for completion before reconnecting the domain. Use this only after explicitly deciding to disconnect that pair.
Permanently deleting a populated site remains dashboard-only, including for owner API keys and MCP connections. Use Settings → Delete site. GET /accounts/ACCOUNT_ID/sites/SITE_ID/deletion with sites:read can inspect a deletion requested there, including after completion. The existing DELETE /accounts/ACCOUNT_ID/sites/SITE_ID with sites:write is limited to cleaning up an empty site that has no imported or saved content, custom domains, or subscription.