Topolo Docs Topolo Platform
Open workspace

Verify and connect a custom domain

Check the domain's ownership TXT record on the public resolver, then claim the hostname in Topolo Web's hostname registry for this job board. Web routes the hostname to Recruit, creates its Cloudflare for SaaS custom hostname and attaches it to its own Turnstile widget; Recruit holds no Cloudflare credential. Status moves pending_dns → pending_ssl (claimed; certificate or Turnstile widget not ready) → active, which needs edgeStatus active and turnstileStatus attached. Call it again while pending_ssl: it repeats the claim to refresh both. An active domain is returned unchanged. Recruiters only.

Outcome

Check the domain's ownership TXT record on the public resolver, then claim the hostname in Topolo Web's hostname registry for this job board. Web routes the hostname to Recruit, creates its Cloudflare for SaaS custom hostname and attaches it to its own Turnstile widget; Recruit holds no Cloudflare credential. Status moves pending_dns → pending_ssl (claimed; certificate or Turnstile widget not ready) → active, which needs edgeStatus active and turnstileStatus attached. Call it again while pending_ssl: it repeats the claim to refresh both. An active domain is returned unchanged. Recruiters only.

Before you start

  • Verify identity and the pinned organization context, then discover the exact action and a granted Recruit workspace.
  • The required application permission is workspace:write. Hiring-team roles and record visibility are enforced separately; a workspace grant alone does not grant every Recruit action.
  • Organization and workspace identity come from verified context. Pass the workspace as resourceType/resourceId, separate from action input; request-body organizationId/workspaceId values cannot select an authorized scope.
  • The first authorized action can initialize an identity-only workspace mirror and invoke the configured workspace-created callback. It does not create an Auth workspace or grant new access.

Recruiters can perform the full hiring workflow. Hiring managers see their managed roles and candidates; interviewers see assigned interviews and their candidates. Workspace owners and organization administrators act as recruiters. Use team.me to read the caller's effective role and allowed actions.

Discover the target

topolo whoami --json
topolo actions get app_topolo_recruit.domains.verify --detail --json
topolo resources topolo-recruit --json
topolo workspaces list topolo-recruit --json
topolo actions get app_topolo_recruit.team.me --detail --json

Request

POST /api/domains/{id}/verify

Save input as payload.json. CLI and MCP action input includes path identifiers. The reviewed plan separates path/query fields from the HTTP body. Set RECRUIT_WORKSPACE_ID from granted resource discovery.

Inspect and test this action in the signed action console. Use its discovered schema and plan; a test response still requires the verification described below.

Input fields

FieldRequiredTypeConstraints
idRequiredstring{"minLength":1,"maxLength":64}

Exact input schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    }
  },
  "required": [
    "id"
  ],
  "additionalProperties": false
}

Effects

  • Claims the hostname in Web's registry once its ownership record is published (Web creates the edge hostname and attaches Turnstile) and records edgeStatus, turnstileStatus and the widget's site key; an active domain serves the job board at its url.

Interpret the response

Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

The response identifier path is $.domain.id for custom_domain. Lists may be empty; an empty list is not a discovered target.

Exact success schema:

{
  "type": "object",
  "required": [
    "domain"
  ],
  "properties": {
    "domain": {
      "type": "object",
      "required": [
        "id",
        "hostname",
        "status",
        "guide"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "hostname": {
          "type": "string"
        },
        "status": {
          "enum": [
            "pending_dns",
            "pending_ssl",
            "active"
          ]
        },
        "apex": {
          "type": "boolean"
        },
        "dnsTarget": {
          "type": "string"
        },
        "guide": {
          "type": "object",
          "required": [
            "title",
            "summary",
            "steps",
            "records",
            "copyAllValue",
            "safetyNote"
          ],
          "description": "The registrar-level DNS instructions in the request's language: one routing and one ownership record to publish, with the steps and notes shown to people.",
          "properties": {
            "title": {
              "type": "string"
            },
            "summary": {
              "type": "string"
            },
            "steps": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "records": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "purpose",
                  "type",
                  "host",
                  "name",
                  "value",
                  "instruction"
                ],
                "properties": {
                  "purpose": {
                    "enum": [
                      "routing",
                      "ownership"
                    ]
                  },
                  "type": {
                    "enum": [
                      "A",
                      "CNAME",
                      "TXT"
                    ]
                  },
                  "host": {
                    "type": "string",
                    "description": "The name relative to the customer's zone, as most DNS hosts expect it (@ for the apex)."
                  },
                  "name": {
                    "type": "string",
                    "description": "The fully qualified record name."
                  },
                  "value": {
                    "type": "string"
                  },
                  "required": {
                    "type": "boolean"
                  },
                  "instruction": {
                    "type": "string",
                    "description": "What to publish, in the request's language."
                  },
                  "title": {
                    "type": "string"
                  },
                  "purposeLabel": {
                    "type": "string"
                  },
                  "hostLabel": {
                    "type": "string"
                  },
                  "valueLabel": {
                    "type": "string"
                  },
                  "hostHelp": {
                    "type": "string"
                  },
                  "valueHelp": {
                    "type": "string"
                  }
                }
              }
            },
            "copyAllLabel": {
              "type": "string"
            },
            "copyAllValue": {
              "type": "string",
              "description": "Every record as tab-separated type, host and value lines."
            },
            "safetyNote": {
              "type": "string"
            },
            "apexNote": {
              "type": "string",
              "description": "Present for an apex domain: the DNS host must support CNAME flattening, ALIAS or ANAME."
            }
          }
        },
        "url": {
          "type": [
            "string",
            "null"
          ],
          "description": "The address once the domain is active; null before."
        },
        "attached": {
          "type": "boolean",
          "description": "True once the hostname is claimed in Topolo Web's hostname registry."
        },
        "edgeStatus": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "pending_ssl",
            "active",
            null
          ],
          "description": "The certificate, as the latest claim reported it; null before the first claim."
        },
        "turnstileStatus": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "pending",
            "attached",
            "failed",
            null
          ],
          "description": "The domain's own Turnstile widget, as the latest claim reported it; null before the first claim."
        },
        "createdAt": {
          "type": "string"
        },
        "updatedAt": {
          "type": "string"
        },
        "verifiedAt": {
          "type": [
            "string",
            "null"
          ]
        }
      }
    }
  }
}

Worked examples

Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. A synthetic fixture identifier grants no access. Validate and inspect the planned request before executing. Commands with --confirm or MCP confirm:true assume the intended effect or data export has already been authorized.

Successful request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.domains.verify",
    "input": {
      "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 200:

{
  "domain": {
    "id": "dom_60dbd36a98f442d38fb792590cb7e3a5",
    "hostname": "careers.example.com",
    "status": "pending_ssl",
    "apex": false,
    "dnsTarget": "jobs.topolo.app",
    "guide": {
      "title": "Connect careers.example.com",
      "summary": "Open the DNS settings at the company where this domain is managed, then add the records below exactly as shown.",
      "steps": [
        "Open DNS settings for your domain.",
        "Add each record below. If a matching record already exists, edit it instead of creating a duplicate.",
        "Save your changes, return here, and select Verify DNS."
      ],
      "records": [
        {
          "purpose": "routing",
          "type": "CNAME",
          "host": "careers",
          "name": "careers.example.com",
          "value": "jobs.topolo.app",
          "required": true,
          "instruction": "Publish a CNAME from careers to jobs.topolo.app. Leave MX, SPF, and DKIM records unchanged.",
          "title": "1. Send visitors to Topolo",
          "purposeLabel": "Website address",
          "hostLabel": "Name / Host",
          "valueLabel": "Points to / Value",
          "hostHelp": "Enter careers in the Name or Host field.",
          "valueHelp": "Paste jobs.topolo.app exactly, without quotes."
        },
        {
          "purpose": "ownership",
          "type": "TXT",
          "host": "_topolo-verification.careers",
          "name": "_topolo-verification.careers.example.com",
          "value": "topolo-verification=bc7127af0291421db77684b6ea29b045",
          "required": true,
          "instruction": "Publish a TXT record at _topolo-verification.careers.example.com with this exact value. Leave every other TXT record unchanged.",
          "title": "2. Confirm you own the domain",
          "purposeLabel": "Ownership check",
          "hostLabel": "Name / Host",
          "valueLabel": "TXT value",
          "hostHelp": "Enter _topolo-verification.careers in the Name or Host field.",
          "valueHelp": "Paste topolo-verification=bc7127af0291421db77684b6ea29b045 exactly, without quotes."
        }
      ],
      "copyAllLabel": "Copy all records",
      "copyAllValue": "CNAME\tcareers\tjobs.topolo.app\nTXT\t_topolo-verification.careers\ttopolo-verification=bc7127af0291421db77684b6ea29b045",
      "safetyNote": "Do not change MX, SPF, DKIM, or other email records. DNS changes can take a few minutes to appear."
    },
    "url": null,
    "attached": true,
    "edgeStatus": "pending_ssl",
    "turnstileStatus": "attached",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z",
    "verifiedAt": null
  }
}

HTTP 200. Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

Another supported request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.domains.verify",
    "input": {
      "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 200:

{
  "domain": {
    "id": "dom_60dbd36a98f442d38fb792590cb7e3a5",
    "hostname": "careers.example.com",
    "status": "active",
    "apex": false,
    "dnsTarget": "jobs.topolo.app",
    "guide": {
      "title": "Connect careers.example.com",
      "summary": "Open the DNS settings at the company where this domain is managed, then add the records below exactly as shown.",
      "steps": [
        "Open DNS settings for your domain.",
        "Add each record below. If a matching record already exists, edit it instead of creating a duplicate.",
        "Save your changes, return here, and select Verify DNS."
      ],
      "records": [
        {
          "purpose": "routing",
          "type": "CNAME",
          "host": "careers",
          "name": "careers.example.com",
          "value": "jobs.topolo.app",
          "required": true,
          "instruction": "Publish a CNAME from careers to jobs.topolo.app. Leave MX, SPF, and DKIM records unchanged.",
          "title": "1. Send visitors to Topolo",
          "purposeLabel": "Website address",
          "hostLabel": "Name / Host",
          "valueLabel": "Points to / Value",
          "hostHelp": "Enter careers in the Name or Host field.",
          "valueHelp": "Paste jobs.topolo.app exactly, without quotes."
        },
        {
          "purpose": "ownership",
          "type": "TXT",
          "host": "_topolo-verification.careers",
          "name": "_topolo-verification.careers.example.com",
          "value": "topolo-verification=bc7127af0291421db77684b6ea29b045",
          "required": true,
          "instruction": "Publish a TXT record at _topolo-verification.careers.example.com with this exact value. Leave every other TXT record unchanged.",
          "title": "2. Confirm you own the domain",
          "purposeLabel": "Ownership check",
          "hostLabel": "Name / Host",
          "valueLabel": "TXT value",
          "hostHelp": "Enter _topolo-verification.careers in the Name or Host field.",
          "valueHelp": "Paste topolo-verification=bc7127af0291421db77684b6ea29b045 exactly, without quotes."
        }
      ],
      "copyAllLabel": "Copy all records",
      "copyAllValue": "CNAME\tcareers\tjobs.topolo.app\nTXT\t_topolo-verification.careers\ttopolo-verification=bc7127af0291421db77684b6ea29b045",
      "safetyNote": "Do not change MX, SPF, DKIM, or other email records. DNS changes can take a few minutes to appear."
    },
    "url": "https://careers.example.com",
    "attached": true,
    "edgeStatus": "active",
    "turnstileStatus": "attached",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z",
    "verifiedAt": "2026-10-05T09:00:00.000Z"
  }
}

HTTP 200. Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

Another supported request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.domains.verify",
    "input": {
      "id": "dom_60dbd36a98f442d38fb792590cb7e3a5"
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 200:

{
  "domain": {
    "id": "dom_60dbd36a98f442d38fb792590cb7e3a5",
    "hostname": "careers.example.com",
    "status": "pending_ssl",
    "apex": false,
    "dnsTarget": "jobs.topolo.app",
    "guide": {
      "title": "Connect careers.example.com",
      "summary": "Open the DNS settings at the company where this domain is managed, then add the records below exactly as shown.",
      "steps": [
        "Open DNS settings for your domain.",
        "Add each record below. If a matching record already exists, edit it instead of creating a duplicate.",
        "Save your changes, return here, and select Verify DNS."
      ],
      "records": [
        {
          "purpose": "routing",
          "type": "CNAME",
          "host": "careers",
          "name": "careers.example.com",
          "value": "jobs.topolo.app",
          "required": true,
          "instruction": "Publish a CNAME from careers to jobs.topolo.app. Leave MX, SPF, and DKIM records unchanged.",
          "title": "1. Send visitors to Topolo",
          "purposeLabel": "Website address",
          "hostLabel": "Name / Host",
          "valueLabel": "Points to / Value",
          "hostHelp": "Enter careers in the Name or Host field.",
          "valueHelp": "Paste jobs.topolo.app exactly, without quotes."
        },
        {
          "purpose": "ownership",
          "type": "TXT",
          "host": "_topolo-verification.careers",
          "name": "_topolo-verification.careers.example.com",
          "value": "topolo-verification=bc7127af0291421db77684b6ea29b045",
          "required": true,
          "instruction": "Publish a TXT record at _topolo-verification.careers.example.com with this exact value. Leave every other TXT record unchanged.",
          "title": "2. Confirm you own the domain",
          "purposeLabel": "Ownership check",
          "hostLabel": "Name / Host",
          "valueLabel": "TXT value",
          "hostHelp": "Enter _topolo-verification.careers in the Name or Host field.",
          "valueHelp": "Paste topolo-verification=bc7127af0291421db77684b6ea29b045 exactly, without quotes."
        }
      ],
      "copyAllLabel": "Copy all records",
      "copyAllValue": "CNAME\tcareers\tjobs.topolo.app\nTXT\t_topolo-verification.careers\ttopolo-verification=bc7127af0291421db77684b6ea29b045",
      "safetyNote": "Do not change MX, SPF, DKIM, or other email records. DNS changes can take a few minutes to appear."
    },
    "url": null,
    "attached": true,
    "edgeStatus": "active",
    "turnstileStatus": "pending",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z",
    "verifiedAt": null
  }
}

HTTP 200. Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

Another supported request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "id": "dom_6e84baf8a8a944e5bd207c7ecf0e06bc"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.domains.verify",
    "input": {
      "id": "dom_6e84baf8a8a944e5bd207c7ecf0e06bc"
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 200:

{
  "domain": {
    "id": "dom_6e84baf8a8a944e5bd207c7ecf0e06bc",
    "hostname": "careers.example.com",
    "status": "pending_ssl",
    "apex": false,
    "dnsTarget": "jobs.topolo.app",
    "guide": {
      "title": "Connect careers.example.com",
      "summary": "Open the DNS settings at the company where this domain is managed, then add the records below exactly as shown.",
      "steps": [
        "Open DNS settings for your domain.",
        "Add each record below. If a matching record already exists, edit it instead of creating a duplicate.",
        "Save your changes, return here, and select Verify DNS."
      ],
      "records": [
        {
          "purpose": "routing",
          "type": "CNAME",
          "host": "careers",
          "name": "careers.example.com",
          "value": "jobs.topolo.app",
          "required": true,
          "instruction": "Publish a CNAME from careers to jobs.topolo.app. Leave MX, SPF, and DKIM records unchanged.",
          "title": "1. Send visitors to Topolo",
          "purposeLabel": "Website address",
          "hostLabel": "Name / Host",
          "valueLabel": "Points to / Value",
          "hostHelp": "Enter careers in the Name or Host field.",
          "valueHelp": "Paste jobs.topolo.app exactly, without quotes."
        },
        {
          "purpose": "ownership",
          "type": "TXT",
          "host": "_topolo-verification.careers",
          "name": "_topolo-verification.careers.example.com",
          "value": "topolo-verification=f2a37d42e1124c16a3a2238413157c04",
          "required": true,
          "instruction": "Publish a TXT record at _topolo-verification.careers.example.com with this exact value. Leave every other TXT record unchanged.",
          "title": "2. Confirm you own the domain",
          "purposeLabel": "Ownership check",
          "hostLabel": "Name / Host",
          "valueLabel": "TXT value",
          "hostHelp": "Enter _topolo-verification.careers in the Name or Host field.",
          "valueHelp": "Paste topolo-verification=f2a37d42e1124c16a3a2238413157c04 exactly, without quotes."
        }
      ],
      "copyAllLabel": "Copy all records",
      "copyAllValue": "CNAME\tcareers\tjobs.topolo.app\nTXT\t_topolo-verification.careers\ttopolo-verification=f2a37d42e1124c16a3a2238413157c04",
      "safetyNote": "Do not change MX, SPF, DKIM, or other email records. DNS changes can take a few minutes to appear."
    },
    "url": null,
    "attached": true,
    "edgeStatus": "active",
    "turnstileStatus": "attached",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z",
    "verifiedAt": null
  }
}

HTTP 200. Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

Refused request · not_found

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_other"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 404:

{
  "error": {
    "code": "not_found",
    "message": "Custom domain was not found in this workspace.",
    "messageKey": "recruit.api.notFound.customDomain",
    "locale": "en"
  }
}

HTTP 404. Preserve the exact response code and details. Do not claim success.

Refused request · dns_records_missing

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_78e9c7263c58480ea91f62aa91eef201"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 409:

{
  "error": {
    "code": "dns_records_missing",
    "message": "The ownership record is not published yet: TXT _topolo-verification.careers.example.com → topolo-verification=cadcbd9c08ee42219ea6d9d04ac68d6f. DNS changes can take a few minutes to appear.",
    "messageKey": "recruit.api.errors.ownershipRecordMissing",
    "messageValues": {
      "name": "_topolo-verification.careers.example.com",
      "value": "topolo-verification=cadcbd9c08ee42219ea6d9d04ac68d6f"
    },
    "locale": "en"
  }
}

HTTP 409. Preserve the exact response code and details. Do not claim success.

Refused request · custom_domains_unconfigured

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_ae0ce836eebd4b8b9022eb8e67cc2aee"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 409:

{
  "error": {
    "code": "custom_domains_unconfigured",
    "message": "Custom domains are not available in this environment yet.",
    "messageKey": "recruit.api.errors.customDomainsUnconfigured",
    "locale": "en"
  }
}

HTTP 409. Preserve the exact response code and details. Do not claim success.

Refused request · hiring_team_role_required

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_x"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 403:

{
  "error": {
    "code": "hiring_team_role_required",
    "message": "Your hiring-team role in this workspace does not allow this action.",
    "messageKey": "recruit.api.errors.teamRoleRequired",
    "locale": "en",
    "details": {
      "teamRole": "interviewer",
      "allowedRoles": [
        "recruiter"
      ]
    }
  }
}

HTTP 403. Preserve the exact response code and details. Do not claim success.

Refused request · hostname_taken

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 409:

{
  "error": {
    "code": "hostname_taken",
    "message": "careers.example.com is already connected elsewhere on Topolo. Choose another hostname.",
    "messageKey": "recruit.api.errors.hostnameTakenElsewhere",
    "messageValues": {
      "hostname": "careers.example.com"
    },
    "locale": "en"
  }
}

HTTP 409. Preserve the exact response code and details. Do not claim success.

Refused request · registry_unavailable

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 503:

{
  "error": {
    "code": "registry_unavailable",
    "message": "Custom domains cannot be connected right now. Nothing was lost; try again shortly.",
    "messageKey": "recruit.api.errors.registryUnavailable",
    "locale": "en"
  }
}

HTTP 503. Preserve the exact response code and details. Do not claim success.

Refused request · registry_rejected

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 502:

{
  "error": {
    "code": "registry_rejected",
    "message": "Topolo could not connect the domain. Try again shortly.",
    "messageKey": "recruit.api.errors.registryRejected",
    "locale": "en"
  }
}

HTTP 502. Preserve the exact response code and details. Do not claim success.

Refused request · custom_domain_capacity_reached

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 409:

{
  "error": {
    "code": "custom_domain_capacity_reached",
    "message": "Topolo cannot connect more custom domains right now. Your domain is kept; verify again later.",
    "messageKey": "recruit.api.errors.customDomainCapacity",
    "locale": "en"
  }
}

HTTP 409. Preserve the exact response code and details. Do not claim success.

Refused request · custom_domains_unconfigured

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 409:

{
  "error": {
    "code": "custom_domains_unconfigured",
    "message": "Custom domains are not available in this environment yet.",
    "messageKey": "recruit.api.errors.customDomainsUnconfigured",
    "locale": "en"
  }
}

HTTP 409. Preserve the exact response code and details. Do not claim success.

Refused request · hostname_reserved

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_69c38ee17d9d4a6195e8bf354566db80"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 422:

{
  "error": {
    "code": "hostname_reserved",
    "message": "This hostname is reserved for the hosting platform and cannot be connected. Use a hostname on a domain you own.",
    "messageKey": "recruit.api.errors.hostnameReserved",
    "locale": "en"
  }
}

HTTP 422. Preserve the exact response code and details. Do not claim success.

Refused request · dns_lookup_failed

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "id": "dom_cd7d994980c3437d9be33d3fa64be284"
}

CLI:

topolo actions validate app_topolo_recruit.domains.verify --data @payload.json --json
topolo actions plan app_topolo_recruit.domains.verify --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 502:

{
  "error": {
    "code": "dns_lookup_failed",
    "message": "The domain's DNS records could not be read. Try again shortly.",
    "messageKey": "recruit.api.errors.dnsLookupFailed",
    "locale": "en"
  }
}

HTTP 502. Preserve the exact response code and details. Do not claim success.

Verify the result

  • Call domains.list and confirm status, edgeStatus and turnstileStatus; when active, fetch domain.url and expect the job board.

Errors and recovery

  • organization_context_required (HTTP 403): Select the organization authorized by the credential; do not send an organization id in the action input.
  • permission_denied (HTTP 403): Grant the credential the required Recruit workspace permission.
  • hiring_team_role_required (HTTP 403): Ask a recruiter to give the caller a hiring-team role that allows this action (team.members.set); error.details lists the allowed roles.
  • workspace_required (HTTP 409): Create a Recruit workspace, or pass the workspace resource context.
  • invalid_input (HTTP 422): Correct the fields named in error.details using inputSchema, then retry.
  • not_found (HTTP 404): List custom domains with domains.list to find a valid id. Records outside the caller's hiring-team view also answer 404.
  • invalid_hostname (HTTP 422): Send a hostname such as www.example.com (letters, digits, hyphens and dots; no scheme or path).
  • hostname_reserved (HTTP 422): Use a hostname on the organization's own domain, not a reserved hosting-platform hostname (Topolo platform zones, workers.dev, pages.dev).
  • hostname_taken (HTTP 409): Another workspace or application holds this hostname. Choose another hostname; do not retry.
  • custom_domains_unconfigured (HTTP 409): Custom domains are not available in this environment yet. Keep the domain and retry later; nothing was changed.
  • dns_records_missing (HTTP 409): Publish the TXT ownership record named in the error (the guide.records of the domain), wait a few minutes for DNS, then verify again.
  • ownership_unverified (HTTP 409): Web could not confirm the ownership record yet. Wait for DNS to update, then verify again.
  • custom_domain_capacity_reached (HTTP 409): No custom-domain capacity is left; nothing was claimed and the domain is kept. Verify again later or escalate to the platform.
  • dns_lookup_failed (HTTP 502): The domain's DNS records could not be read. Retry shortly.
  • registry_rejected (HTTP 502): The hostname registry rejected the claim. Retry shortly; if it persists, report the request id.
  • registry_unavailable (HTTP 503): The hostname registry could not be reached; nothing was lost. Retry shortly.
  • domain_connection_failed (HTTP 422): The domain could not be connected. Check the hostname and try again.
  • invalid_json (HTTP 400): Send a valid JSON object. The request is rejected before workspace resolution or record creation.
  • api_key_workspace_binding_required (HTTP 403): Create the API key bound to a workspace (an organization-wide key names a workspace nobody verified), then call again.
  • workspace_required (HTTP 400): Send the workspace the key is bound to with --resource-type workspace --resource-id <workspace id> (or the X-Topolo-Resource-Type and X-Topolo-Resource-Id headers).
  • workspace_forbidden (HTTP 403): Use a workspace the key is bound to; read the key's bindings in API key management.
  • workspace_access_insufficient (HTTP 403): Give the key the workspace permission this action needs: workspace:read to read and workspace:write to change. A delete also needs workspace:write, because the write permission is what grants the read_write level.

Authentication or gateway failures may occur before the Recruit handler. Preserve the exact discovered service error, request id and credential context. The local fixtures do not establish live credential authorization.

Recovery boundary

Delete the domain with domains.delete; its claim is released at Web first.

  • Remove a custom domain: Release the hostname's claim in Topolo Web's hostname registry (Web deletes its edge hostname and Turnstile membership; a claim already gone counts as released), then delete the domain. The job board stays published at its Recruit address. The organization can then remove the DNS records. Recruiters with workspace:delete only.

Evidence

Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. Test capture provenance is retained with each fixture. API, database and integration outcomes require separate verification in the target environment.

Documentation history

  • 2026-10-18: Added this action guide with reviewed request, response, permission, verification and recovery guidance from source 63171ed7c08aa066adb44cc01e64011e465302e1. Examples are retained local captures, not production execution or delivery proof.