---
title: "GET /v1/installations/{id}/install"
method: GET
path: "/v1/installations/{id}/install"
---

# GET /v1/installations/{id}/install

`GET /v1/installations/{id}/install`

## Request

- The document declares no server URL.
- Auth: one of:
  - HTTP bearer
  - API key in cookie `__Secure-better-auth.session_token`

How far the install got, and what stopped it.

## Path parameters

- `id` string, required

## Response `200`

The request has succeeded.

- InstallProgress: How far the install got, and what stopped it. ⛔ **Carries no credential, by construction.** The backend builds this from a cell that never holds one — see the `Installer` Durable Object. A founder's Cloudflare token and the installation's own signing key are the two things this shape must never be able to express.
  - `step` 'awaiting_authorization' | 'authorized' | 'connecting_account' | 'serving_zone' | 'activating_routing' | 'installed', required: `awaiting_authorization`: waiting for the founder at Cloudflare's consent screen. `authorized`: they approved it, and a credential is held for minutes — call `startInstall` now. ⛔ Reported so an AGENT can see it: until 2026-10-06 the approval left this at `awaiting_authorization`, so an agent had to ask a person "did it work?" while the credential burned down, and on that day it expired exactly so. Still a label and never the credential. `connecting_account`: deploying the relay and giving it an address — touches no mail. `serving_zone`: pointing a domain's mail at the relay. ⚠️ The destructive half. `activating_routing`: the domain is served and Cloudflare is switching its mail on — usually 5–15 minutes; keep polling. Mail sent before it finishes may bounce `550 5.1.1`, which is why the install does not say `installed` until Cloudflare says ready (P58). `failed` here names what Cloudflare last reported; installing again carries on waiting and redoes nothing. `installed`: done — Cloudflare accepts the domain's mail.
  - `state` 'waiting' | 'running' | 'failed' | 'done', required
  - `completed` string[], required: The steps that completed, in order — so a resumed install can show what it skipped.
  - `error` string: Why it stopped, when it stopped.
  - `refusals` Refusal[]: Things we refuse to do to the founder's account without being told to: an apex MX already serving their mail elsewhere, an SPF record enabling Email Routing would collide with, a hostname the relay's address would overwrite, a delivery-events subscription of theirs that Cloudflare will not let ours sit beside. Each says what proceeding would do.
    - `kind` 'zone_missing' | 'zone_inactive' | 'account_mismatch' | 'foreign_mx' | 'apex_spf' | 'hostname_taken' | 'events_taken', required: Which check refused. Machine-readable, so a console can render the fix rather than read it. `zone_missing` / `zone_inactive` / `account_mismatch` are blockers: we cannot proceed at all. `foreign_mx` / `apex_spf` / `hostname_taken` / `events_taken` are dangers: we could, and it would break something the customer already relies on.
    - `message` string, required: The refusal in words, complete on its own — this is what a log line carries.
    - `remedy` Remedy: The value a founder has to go and publish, when the fix IS a value. ⛔ **Separate from the message on purpose.** It used to live inside the prose — the SPF refusal read `\n replace with: v=spf1 …` — and the only way for a console to offer it as something copyable was to parse English out of a sentence. That is the bug the dashboard's own rule forbids ("screens branch on the status, never on the message"), because it breaks on a copy edit with nothing to catch it. A record typed out by hand from a paragraph is a record typed out wrong, and getting an SPF record wrong stops the domain's mail either way.
      - `label` string, required: What this value IS, e.g. `SPF record for acme.com`.
      - `value` string, required: The value to publish, verbatim and complete. Nothing has to be extracted from it.
      - `replaces` string: What it replaces, when it replaces something — so the founder finds the right record.
    - `records` string[]: The MX hosts the domain receives at today — `foreign_mx` only — so a console can say who still gets its mail without parsing `message`.

## Other responses

- `default`: An unexpected error response.

---

[API](https://skmtc.dev/foundermail/apis/org.md) - [All operations](https://skmtc.dev/foundermail/apis/org/llms.txt) - [OpenAPI document](https://skmtc.dev/foundermail/apis/org/revisions/8bbb36e378ab?raw)
