---
title: "Verify the customer's Route 53 access role and write the connection's records with it"
method: POST
path: "/connections/{id}/route53-role:complete"
tags: ["Connections"]
---

# Verify the customer's Route 53 access role and write the connection's records with it

`POST /connections/{id}/route53-role:complete`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/connections/{id}/route53-role:complete`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

**Route 53 access role, second step.** Send the `RoleArn` output of the stack the customer created from the `route53-role:start` link. CustomDomain™ assumes the role with the connection's external ID and checks that AWS refuses the role both without an external ID and with a wrong one: a role that lets CustomDomain™ in without the right external ID is refused and never stored. It then finds the hosted zone and writes the connection's records through Route 53 with the role's temporary credentials, and the connection moves to `propagating` exactly as after an API key apply. The verified role is stored, so calling this again with an empty body re-applies the records with it. Branch on `code`: `route53_role_not_started` (409), `route53_role_arn_invalid` (400), `route53_external_id_mismatch` (the role was made from another link), `route53_role_not_found` (AWS refused the role), `route53_external_id_not_required`, `route53_zone_not_found` (no public hosted zone for the domain or a parent of it), `route53_zone_ambiguous` (two public hosted zones share the name and the role can read both), `route53_access_denied` (the role cannot read that zone, or cannot write a record the connection needs) and `route53_record_conflict` (another record holds the name with a type the needed record cannot share; delete or change it in the Route 53 console), all 422, and `route53_unavailable` (503, nothing was changed, try again).

## Path parameters

- `id` string, required

## Request body

- object
  - `role_arn` string: The stack's `RoleArn` output, such as `arn:aws:iam::123456789012:role/CustomDomainDNSAccess-0123456789abcdef0123456789abcdef`. Optional once a role has been verified for the connection.

## Response `200`

The records were written and the connection is propagating.

- Route53RoleComplete: The answer of `POST /connections/{id}/route53-role:complete`.
  - `connection` Connection, required
    - `id` string, required
    - `application_id` string, required
    - `domain` string, required
    - `host` string: subdomain label; empty means the apex
    - `provider_id` string: detected / applied DNS provider
    - `setup_type` 'automatic' | 'manual' | 'async' | 'mcp', required: How a connection reaches live.
    - `status` 'pending' | 'propagating' | 'live' | 'failed', required
    - `managed` boolean: `true` when the connection opted into **managed mode**: the control plane holds a durable, encrypted async grant and can re-apply (`POST /v1/connections/{id}:reapply`) or revert (`DELETE /v1/connections/{id}`) the template server-side without re-consent. Omitted (treated as `false`) for the default use-once connection.
    - `created_at` string, date-time, required
    - `sandbox` boolean: `true` when the connection belongs to a test mode application. Its DNS is simulated, nothing real is read, written or served, and it counts for nothing (no free domain, quota, billing or metering). Copied from the application at create and never changed. Omitted (treated as `false`) on a real connection.
    - `last_checked_at` string, date-time: When the propagation poller last examined this connection. Omitted until the first check.
    - `www_redirect` boolean: Serve an apex↔www redirect. Echoed from create.
    - `override_spf` boolean: Replace an existing SPF record instead of merging into it, where the provider supports it. Echoed from create.
    - `validate_dmarc` boolean: Stored and echoed from create; not acted on yet.
    - `validate_caa` boolean: Whether the Let's Encrypt CAA records are part of the desired record set. Echoed from create.
    - `monitor` boolean: Whether the domain is enrolled in drift monitoring. Echoed from create, where it defaults to true when omitted, and honoured: the drift watch is registered when the connection goes live only if this is true. Connections created before the flag was honoured read true (they were monitored unconditionally, and were backfilled), so this is false only where a caller explicitly opted out.
    - `batch_id` string: The multi-domain session this connection was created in. Omitted for a standalone connect.
    - `end_user_ref` string: The integrator's own identifier for the end user who connected this domain. Omitted for a console/direct connect.
    - `retried_at` string, date-time: When a human (or the create-path resume, or real edge traffic) last asked for this connection to be re-verified. The retry clock is max(created_at, retried_at): a revived connection gets a fresh window from here, not from its original creation. Absent when it was never retried.
    - `routing` ConnectionRouting
      - `mode` 'edge' | 'custom', required: `edge`: records point at the CustomDomain™ edge. `custom`: at your own infrastructure.
      - `cname_target` string: For `custom`: the hostname subdomains and ALIAS-capable roots point at.
      - `apex_ipv4` string[]: For `custom`: the addresses roots get on DNS providers with no ALIAS-style record.
      - `set_at` string, date-time, required: When this routing was recorded for the domain.
    - `records_source` 'integrator': Present as `integrator` when the connection's desired record set was supplied through `PUT /v1/connections/{id}/records` rather than authored by us. Omitted for the usual server-synthesized or Domain Connect template-materialized set. Such a connection cannot use the Domain Connect rail, which applies our published templates at the provider.
    - `created_via` '' | 'widget' | 'console' | 'api' | 'agent' | 'mcp': Where the connection was created, recorded once at create and never changed. `widget`: an end user's connect-scoped widget token (the embedded widget in your product). `console`: the CustomDomain™ console. `api`: an API key, or an app-scoped token, from your server. `agent`: a delegated agent access token. `mcp`: the MCP server. Empty for connections created before this was recorded. A trusted server-side caller may name its surface with the `X-CustomDomain-Client` request header (`console` or `mcp`); the header is a label only and grants nothing.
    - `secure_root_domain` boolean: The edge also issues/secures the registrable apex alongside this subdomain connection. Omitted (treated as `false`) when unset.
    - `redirect_to` string, uri: URL the edge 301s this host to (Power redirect target). Omitted when the connection is not a redirect.
    - `power_root_path_access` string[]: URL paths the edge may static-serve at the root (Power root-path access). Omitted when none are configured.
    - `error_code` string: The last failure reason recorded for this connection, from whichever path failed, not only the background poller. It is persisted by: the propagation poller / `:recheck` verification (e.g. `propagation_timeout`, `setup_incomplete`, `pending_expired`, `domain_already_connected`); the foreground apply on `POST /connections/{id}/apply` (e.g. `dns_write_failed`, `ProviderAuthenticationError`, `spf_merge_failed`, `apex_not_supported`, `forwarding_not_supported`); the OAuth callback (which also records `forwarding_not_supported`); and the Domain Connect async callback (both e.g. `provider_unavailable`, `ProviderAuthenticationError`, `apply_failed`, `internal_error`, or the provider's own `error` parameter). Rejected request validation (`400`) is deliberately NOT recorded here: a malformed request is not a connection failure. Omitted when healthy or never failed; cleared when the connection next verifies.
    - `error_message` string: Human-readable detail for `error_code`. Populated on the same paths and cleared alongside it. Omitted when there is none.
    - `write_rail` 'api_key' | 'sign_in' | 'domain_connect' | 'domain_connect_async' | 'route53_role' | 'provider_account' | 'managed_dns': The rail that last wrote this connection's records. Omitted when no rail wrote them (a manual setup, or a connection from before this was recorded). It decides what removing the domain can do with the records (`GET /connections/{id}/removal`).
    - `provider_account_id` string: The connected provider account whose credential wrote this connection's records and, when the account's access is `continuous`, keeps them (repairs and removal). Omitted when none.
    - `removal_started_at` string, date-time: Set while the domain is being removed: the records are being taken out and the connection is deleted when that finishes. The domain no longer serves from this moment. Omitted otherwise.
  - `records` DnsRecord[], required
    - `type` string, required: DNS record type. `APEXCNAME` is the engine's provider-agnostic CNAME-at-the-apex type (a plain CNAME is illegal at a zone apex); it is realized to the host's native ALIAS / ANAME / flattened-CNAME on write, and shown to users as `ALIAS` in manual instructions.
    - `host` string, required: fully-qualified record name
    - `value` string, required
    - `ttl` integer
    - `priority` integer, nullable: MX only
    - `applied` boolean: true once written through a provider
    - `change` 'created' | 'added' | 'replaced' | 'merged' | 'existing': What writing this record did to its name, recorded when we wrote it through a provider API. `created`: nothing of this type was there. `added`: our value joined a shared TXT or CAA set and the values already there were kept. `replaced`: the values of this type at the name were replaced (`prior` holds them). `merged`: an SPF policy was merged with the one already there (`prior` holds the original). `existing`: our exact value was already there, so removing the domain leaves it. Omitted when not recorded (a manual or one-click setup, or a record written before this was recorded).
    - `prior` PriorValue[]: The values this record replaced, so removing the domain can put them back. Omitted when nothing was replaced or it is unknown.
      - `type` string, required
      - `value` string, required
      - `ttl` integer
      - `priority` integer
    - `prior_source` 'provider' | 'public_dns': Where `prior` was read: `provider` (through the provider's API right before the write) or `public_dns` (observed in public DNS before the change). Omitted when unknown.
  - `role_arn` string, required: The verified role, now stored so an empty call re-applies with it.
  - `zone` string, required: The Route 53 hosted zone the records were written into.

## Other responses

- `400`: Error envelope: {code, title, details}.
- `401`: Error envelope: {code, title, details}.
- `403`: Error envelope: {code, title, details}.
- `404`: Error envelope: {code, title, details}.
- `409`: Error envelope: {code, title, details}.
- `422`: Error envelope: {code, title, details}.
- `503`: Error envelope: {code, title, details}.

---

[API](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api.md) · [All operations](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/revisions/34d89cb7a7cd?raw)
