Skip to content

REST API

Work with sites, files, revisions, domains, and bandwidth from your own tools.

Read as Markdown
On this page

Publish a site, make changes, and roll back from your own tools. The REST API is available now at https://app.ironfountain.net/api/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.

Quick start

Create an API key in your organization’s API keys screen. Keys default to all permissions and never expire; 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 WW_API_KEY. Keep it out of published website files and browser JavaScript.

curl https://app.ironfountain.net/api/v1/me \
  -H "Authorization: Bearer $WW_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.net/api/v1/accounts/ACCOUNT_ID/sites \
  -H "Authorization: Bearer $WW_API_KEY"

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 import preview 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

PermissionWhat it allows
sites:readRead sites, files, revision history, import reports, DNS status, and storage usage.
sites:writeCreate sites, change site settings, and manage file or path redirects.
deployments:writeImport, upload, publish, restore, pin, and unpin revisions.
domains:writePrepare and verify custom domains and check their connection.

Permissions are independent. A tool that reads the current files and publishes changes needs both sites:read and deployments:write. Organization and member administration, site transfers, passwords, and API key management use signed-in dashboard sessions.

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, and 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 snapshot to POST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments. Files omitted from a snapshot 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.net/api/v1/accounts/ACCOUNT_ID/sites/SITE_ID/deployments \
  -H "Authorization: Bearer $WW_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.

Include index.html at the root. Use unique relative paths and encoding: "base64" for binary files. A JSON upload accepts up to 20,000 files, 100 MiB of supplied content, and 5 MiB per supplied file. The complete site can contain 20,000 files and 1 GiB. 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. Unchanged files share storage between revisions; text is compressed. Publication queues CDN invalidation, so cached pages may take a moment to update.

History and rollback

OperationRequest
List saved revisionsGET /accounts/ACCOUNT_ID/sites/SITE_ID
Read a revision manifestGET /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID
Publish or restore a saved revisionPOST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/restore with {}
Revert stagingPOST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/stage with {"expected_deployment_id":"CURRENT_STAGING_ID"}
Rename stagingPATCH /accounts/ACCOUNT_ID/sites/SITE_ID/staging with {"slug":"secret"}
Pin a revisionPATCH /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID with {"pinned":true}
Unpin a revisionThe same request with {"pinned":false}
Read storage usageGET /accounts/ACCOUNT_ID/sites/SITE_ID/storage

The current staging and production revisions and pinned revisions are protected from automatic history cleanup. Organization owners configure how many recent revisions per environment to retain in site settings.

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.

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": "2026-09-11"
}

Importing requires permission from the content owner. Read the current Terms and GET /legal/terms for the current version and declaration before affirming it. 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 and staging_url for the imported revision. Import limits are 200 pages, 20,000 files, 1 GiB total, 100 MiB 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.

Anonymous temporary previews expire after 48 hours or immediately when saved to an organization. Registered sites use retained revision staging links, which block indexing and remain available while the revision is retained. 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.

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. Normally the fresh root _acme-challenge TXT proves ownership of the root/www pair as well as preparing its certificate; no separate ownership TXT is needed. ownership.method is pending, acme, verified, or txt for a fallback that needs the displayed fresh ownership code. Read the supplied names and values rather than constructing them. Ownership checks and HTTPS preparation run automatically. connection.next_check_at is the next scheduled check in Unix milliseconds. Ownership checks retry after 10 seconds, while root certificate checks generally retry after 30 seconds and CNAME/WWW checks retry after 10 seconds; repeated requests do not bypass that schedule.

Keep the old website’s A/CNAME records until connection.dns_switch_ready is true: ownership and root HTTPS are ready. Then use gateway_ip for the root A record and cname_target for the www CNAME. No WWW TXT is required. Once the CNAME matches, Iron Fountain requests the WWW certificate automatically through HTTP validation. WWW HTTPS may be briefly unavailable while it is issued. connection.hosting_ready becomes true after both HTTPS endpoints pass. connection.phase: "active" means DNS and HTTPS passed and the primary address is active. Keep the old hosting available while DNS caches expire. DNS routing alone never verifies ownership.

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/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 that pair’s ownership and root HTTPS checks pass. Use that redirect connection’s branded cname_target and the shared gateway_ip. Keep existing DNS website records until dns_switch_ready is true. WWW HTTPS is prepared automatically after the CNAME changes; there is no WWW TXT step. Each pair progresses 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 with DELETE …/redirect-domains/example.net. HTTP 202 means removal is queued. The entry remains phase: "removing" until its Bunny hostnames are detached; failures retry automatically while the names stay reserved to prevent reassignment during cleanup. 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. Destinations can be a local path or an external HTTPS URL. Status codes are 301 or 302. Remove a rule with DELETE /accounts/ACCOUNT_ID/sites/SITE_ID/redirects/REDIRECT_ID. 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.net/mcp. 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 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}]}

These rules apply to that revision in temporary previews, 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. Queries are not automatically forwarded. The file allows up to 2,000 rules and 2 MiB. Existing hostname-specific rules configured through /redirects take precedence. Normal root/www and secondary-domain redirects still run first.

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.

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.

Navigation and sitemap discovery stop as soon as more than 200 distinct page URLs are found, before downloading the remaining pages and assets. Sitemap discovery reads up to twenty files, each limited to 2 MB. Password-protected content and pages whose visible content requires JavaScript execution cannot currently be captured. These failures stop the import without saving a staging snapshot.

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. A public crawl cannot establish that every hidden dependency is absent; this importer parses fetched HTML and does not run a headless browser.

Every crawl is bounded by 1.25 GiB of downloaded response bodies, 25,000 fetch operations, 200 HTML pages, 20,000 saved files, 100 MiB per file, 1 GiB per snapshot, and fifteen minutes. Redirect hops and headers can add a small amount of network traffic beyond the response-body allowance. Streams are aborted when limits are exceeded.

Site bandwidth

GET /api/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.

CDN usage includes response headers and bodies served at the edge, including cache hits. Direct gateway usage records response body bytes written by Caddy. Origin fetches are excluded to avoid counting CDN delivery twice. These measurements differ from a provider invoice: transport overhead, other service traffic, and differing byte definitions are not included in a single billing total.

Collection runs about once a minute, with a short CDN delay and background reconciliation across Bunny’s available three-day log window. Aggregates are retained for 400 days; event IDs for deduplication are removed after five days. Iron Fountain stores no visitor IPs, request paths, cookies, or authorization values in its usage database. Direct gateway logs rotate within the existing shared Bunny volume, with up to eight 10 MiB archives retained for up to four days; the current file adds up to 10 MiB. If collection is offline longer than available logs remain, missing traffic cannot be recovered. The dashboard shows collection status so missing or delayed data is visible.

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.

Each submission stores its fields as a JSON object. Multiple selections use arrays. Indexed site ID, form key, environment, status, and timestamp support the inbox without requiring a new database schema for each form. Attachments are private Bunny Storage objects and can only be downloaded by someone with access to the site.

All routes below are relative to /api/v1/accounts/:accountId/sites/:siteId.

MethodRoutePermissionBehavior
GET/formssites:readList forms, current revision availability, unread counts, protection configuration, and usage.
POST/forms/connectdeployments:writeConnect compatible HTML forms or synchronize connected fields in a staging revision. Optional body: expected_staging_revision_id for concurrency. Does not recrawl or publish.
PATCH/forms/:formKeysites:writeSet enabled to pause or resume a form.
PATCH/forms/protectionsites:writeSet site_key and secret for reCAPTCHA v2. Secrets are encrypted and never returned. Omit secret to keep the saved value.
GET/form-submissionssites:readList up to 50 submissions, their fields, and attachment metadata.
PATCH/form-submissions/:idsites:writeSet read (boolean), or status (inbox or spam).
DELETE/form-submissions/:idsites:writeDelete a submission and revoke attachment access immediately; private files are cleaned up in the background.
GET/form-attachments/:idsites:readDownload an attachment as a file. Authentication is required.
GET/form-submissions.csv?form=:formKeysites:readDownload 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, status=inbox|spam, 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 Test submissions inbox. Temporary anonymous previews simulate submissions and file selection without storing submitted data. Staging and temporary previews 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. Rate limits apply across all containers: 20 submission attempts per visitor per site per 10 minutes and 2,000 per site per day. Abandoned uploads expire after an hour.

Honeypot checks and request limits are automatic; submissions containing more than five web links go to Spam for review. Imported reCAPTCHA forms require a valid site key and secret before publication; tests use a clearly labeled test mode. HTML field patterns and browser-native validation remain in the page; the server also validates required fields, email, URL, numeric ranges, selected options, attachment ownership, and request size. Email notifications are not enabled. Historical submissions and private notification settings cannot be discovered by crawling a public site.

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.