# Iron Fountain API

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](https://www.ironfountain.net/docs/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.

```bash
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.

```bash
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

| 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 file or path redirects. |
| `deployments:write` | Import, upload, publish, restore, pin, and unpin revisions. |
| `domains:write` | Prepare 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`:

```json
{"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.

```json
{
  "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:

```bash
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

| 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` |
| Publish or restore a saved revision | `POST /accounts/ACCOUNT_ID/sites/SITE_ID/deployments/REVISION_ID/restore` with `{}` |
| 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` |

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`:

```json
{
  "url": "https://example.com/",
  "rights_confirmed": true,
  "terms_version": "2026-09-11"
}
```

Importing requires permission from the content owner. Read the [current Terms](https://www.ironfountain.net/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`:

```json
{"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`:

```http
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:

```json
{
  "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](https://www.ironfountain.net/docs/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:

```json
{"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](https://www.ironfountain.net/docs/forms/) 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`.

| Method | Route | Permission | Behavior |
| --- | --- | --- | --- |
| GET | `/forms` | `sites:read` | List forms, current revision availability, unread counts, protection configuration, and usage. |
| POST | `/forms/connect` | `deployments:write` | Connect 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/:formKey` | `sites:write` | Set `enabled` to pause or resume a form. |
| PATCH | `/forms/protection` | `sites:write` | Set `site_key` and `secret` for reCAPTCHA v2. Secrets are encrypted and never returned. Omit `secret` to keep the saved value. |
| GET | `/form-submissions` | `sites:read` | List up to 50 submissions, their fields, and attachment metadata. |
| PATCH | `/form-submissions/:id` | `sites:write` | Set `read` (boolean), or `status` (`inbox` or `spam`). |
| DELETE | `/form-submissions/:id` | `sites:write` | Delete a submission and revoke attachment access immediately; private files are cleaned up in the background. |
| 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`, `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.
