# DNS Widget

<a id="dns-widget"></a>

We've created an optional DNS widget that's embeddable into your app, to help your users update their DNS records when connecting a custom domain.

The DNS widget detects the DNS provider and offers an automatic setup option where a supported provider integration is available. Provider-specific manual instructions are also available. Automation depends on the provider and requested record configuration.

<a id="dns-widget-demo"></a>

## DNS Widget Demo

Try the widget with Classic, Dark, and Minimal styles in the full-page demo.

<a id="apxdns-widget-demo"></a>

[Try the embeddable demo](https://cloud.approximated.app/dnswidget/demo)

<a id="dns-widget-basics"></a>

## DNS Widget Basics

- Embedded v2 uses two plain JavaScript files: the headless client and the widget renderer.
- Use the supplied CSS or override it to match your app.
- The widget renders directly in your page, with no iframe required.
- Start the widget by calling `window.apxDns.init(config)` after its container exists.
- The config object contains some settings and an array of the DNS records you'd like to apply to custom domains.
- You can optionally add a domain to the config to skip the domain entry UI step when the custom domain is already known.
- The widget calls Approximated API endpoints to detect domain DNS records, generate instructions/automation steps, and to verify changes.
- Embedded v2 renders instructions in the browser from the headless API’s JSON response.

   Prefer to render everything yourself? Use [headless mode](#dns-widget-headless) to display that JSON in your own components.

<a id="dns-widget-setup"></a>

## DNS Widget Setup

We've tried to make the DNS widget easily compatible with as many different stacks and approaches as possible. The setup below uses embedded v2 with a scoped customer token, powered by the headless API.

1. Load the headless client, then the embedded v2 renderer:

   `<script src="https://cloud.approximated.app/dnswidget/headless.v2.js"></script> <script src="https://cloud.approximated.app/dnswidget/dnswidget.v2.js"></script>`
2. Link the CSS file from Approximated:

   `<link rel="stylesheet" href="https://cloud.approximated.app/dnswidget/dnswidget.v2.css">`

   Note: you can take a look at this file and override styles and variables as needed with your own CSS as well.
3. Add a container where the widget should render:

   `<div id="apxdnswidget" class="apxdnswidget"></div>`

   Then call the widget init function after the container exists. Replace the documentation address `192.0.2.10` with your cluster IPv4 address. The example configures the apex A record and a CNAME at `www`; `{domain}` is replaced with the domain entered by the customer.

   ```js
   // Create the config data for the widget.
   const apx_dns_widget_config = {
       // Generate a scoped token on your server with GET /api/dns/v2/token.
       token: "use_token_api_endpoint_server_side_to_generate_this",
       api_url: "https://cloud.approximated.app/api/dns/v2",
       /*
       * The dnsRecords field is an array of record objects
       * that you'd like to have applied to custom domains.
       *
       * These will likely be A records pointing at your cluster IP address,
       * or CNAME records pointing at an intermediary domain you control.
       * The dnsRecords always consist of:
       *      - [string]  type (Uppercase DNS record type like A, CNAME, or TXT)
       *      - [string]  host (Use @ for the apex domain or a subdomain with no trailing dot)
       *      - [string]  value (the address, value, text, etc. for that record type)
       *      - [integer] ttl (how many seconds the record should be cached in DNS servers)
       * We currently allow setting A, CNAME, and TXT records with the DNS widget.
       */
       dnsRecords: [
           {
               type: "A",
               host: "@",
               value: "192.0.2.10",
               ttl: 3600,
           },
           {
               type: "CNAME",
               host: "www",
               value: "{domain}",
               ttl: 3600,
           }
       ],
       // domain: "example.com", // optional, skips the domain entry UI step
       // prefillDomain: "example.com", // optional, prefills the domain without skipping
       verifyAutoScroll: true // optional, auto-scroll to verification results. Default: true
   };

   // Call the DNS widget init function to launch the widget.
   // In this example we call it after the DOM content is loaded.
   document.addEventListener("DOMContentLoaded", function(event) {
       // The DNS widget functions/data are automatically namespaced
       // and available from window.apxDns.
       window.apxDns.init(apx_dns_widget_config);
   });
   ```
4. Optionally, set javascript listeners to call some of your own logic based on widget events.

   ```js
   // When the user submits a domain in the UI
   document.addEventListener('apx-dnswidget-user-submitted-domain', function(event) {
       // Your code to handle the event.
       // The event.detail string contains the custom domain/subdomain
       // submitted by the user into the UI.
   });

   // When the widget flow is restarted
   document.addEventListener('apx-dnswidget-restarted', function(event) {
       // Your code to handle the event.
   });

   // When all of the records are completely verified as updated successfully
   document.addEventListener('apx-dnswidget-records-completely-verified', function(event) {
       // Your code to handle the event.
       // The event.detail will contain the record results as a list of objects
   });

   // When none of the records match the desired updates, during verification
   document.addEventListener('apx-dnswidget-records-failed-verification', function(event) {
       // Your code to handle the event.
       // The event.detail will contain the record results as a list of objects
   });

   // When only some of the records are verified as updated successfully
   document.addEventListener('apx-dnswidget-records-partially-verified', function(event) {
       // Your code to handle the event.
       // The event.detail will contain the record results as a list of objects
       console.log(event.detail);

       // the output of the console.log above could look like this:
       [
           {
               actual_values: [
                   "192.0.2.10"
               ],
               apex: "example",
               combined_host: "@",
               domain: "example.com",
               full: "example.com",
               host: "@",
               match: true, // Approximated found an exact match DNS record
               match_against: "192.0.2.10", // The value to match against the DNS record
               non_tld: "example",
               subdomain: null,
               tld: "com",
               type: "A"
           },
           {
               actual_values: false,
               apex: "example",
               combined_host: "www",
               domain: "example.com",
               full: "example.com",
               host: "www",
               match: false, // Approximated could not find a matching DNS record
               match_against: "example.com", // The value to match against the DNS record
               non_tld: "example",
               subdomain: null,
               tld: "com",
               type: "CNAME"
           }
       ]
   });
   ```
5. You can optionally stop and clear the widget element or restart the flow.

   ```js
   // Stop and clear the element used for the widget.
   // The init function will need to be called again to restart.
   window.apxDns.stop()

   // Alternatively, you can restart without stopping by calling this function:
   window.apxDns.restart()
   ```

<a id="dns-widget-styling"></a>

## DNS Widget Styling

The widget comes with neutral styling that you can override with your own CSS. Its outermost element fills the available width up to 600px, with no fixed height, padding, or border, so you can fit it into your app. [Explore the Classic, Dark, and Minimal styles](https://cloud.approximated.app/dnswidget/demo) to see how CSS can change its appearance.

Every CSS class in the element is prefixed with apxdns-, and the outermost element always has a class of apxdnswidget. The intention for this is to avoid any class name conflicts with your existing CSS code.

The widget also makes use of CSS variables scoped to the class apxdnswidget to enable easier theming. The available variables and their default values are as follows:

```css
.apxdnswidget {
    --text-color: #333;
    --light-text-color: #666;
    --link-text-color: rgb(46, 90, 173);
    --main-bg-color: transparent;
    --shaded-bg-color: #FAFAFA;
    --shaded-border: 1px solid #EEE;
    --button-bg-color: #fb5168;
    --button-hover-bg-color: #da3551;
    --button-text-color: #FFF;
    --radius: 5px;
    --widget-max-width: 600px;
    --border-color: #EEE;
}
```

<a id="dns-widget-token"></a>

## Scoped DNS Widget Tokens

A scoped token is a temporary pass that lets the browser use the DNS widget without seeing your private API key. Create it on your server, then pass the returned `token` to the embedded widget or headless client. Each token is tied to the API key and proxy cluster that created it.

Call the URL below from an authenticated endpoint in your application. Check that the signed-in user is allowed to configure the domain, and send your cluster API key in the `api-key` header. Keep that key on your server.

GET

https://cloud.approximated.app/api/dns/v2/token

**Returns**

200 - Success

```json
{
    "token": "a-valid-token"
}
```

401 - The API key used does not exist

429 - Too many requests; wait before trying again

**No fields/data required.**

Each token lasts up to **10 minutes and 30 seconds**. While the page is open, the client attempts renewal every five minutes. Renewals keep the same session going for at most **24 hours from its first token**; they cannot extend that deadline. After expiry, restart the setup flow with a fresh token from your authenticated backend.

Deleting or replacing the API key that created a token stops that token and its renewals from working. Use a current key to start a new session. Token creation and renewal return `Cache-Control: no-store`; return that header from your own token endpoint too.

Use DNS Widget v2 throughout: `/api/dns/v2/token` creates the token, and instructions, DNS checks, and renewal use `/api/dns/v2/`. Load `headless.v2.js` for browser requests. The v2 browser API requires scoped tokens.

Widget tokens allow DNS instructions and checks. They do not prove domain ownership or give the browser permission to change your application’s accounts or domain settings.

<a id="dns-widget-limits"></a>

**Request limits**

The following limits apply per minute. A request must fit every applicable limit:

- **Create tokens:** 60 per IP address and 60 per proxy cluster.
- **Renew tokens:** 120 per IP address, 120 per cluster, and 12 per session.
- **Get instructions:** 120 per IP address, 120 per cluster, and 60 per session.
- **Verify records:** 240 per IP address, 240 per cluster, and 120 per session. Exact-match and existence checks share these limits.

Keys for the same cluster share its limit. Renewing a token does not reset its session’s count. Limits are enforced separately on each Approximated application server. Token creation also counts toward the general API limit of 240 requests per minute per key.

Widget limits return HTTP `429` with JSON error `rate_limited` and `Retry-After: 60`. Wait 60 seconds before retrying. The general API-key limit can instead return a plain-text 429 response.

Public demos use separate demo tokens: sessions last up to 30 minutes, requests allow one or two records, and instructions use manual setup without saving provider lookups. Use your own scoped customer token for your application, including supported automatic setup.

<a id="dns-widget-headless"></a>

## Headless Mode

Build DNS setup into your own interface. Send us the customer’s domain and the records you need; we return their DNS provider, the values to enter, and step-by-step instructions as JSON. You decide how to display them.

Use headless mode when you want to use your own components, layout, or wording. If you’d prefer a complete interface you can drop into a page, start with the [embeddable widget](#dns-widget-setup).

[Get started](#dns-widget-headless-start) [React and Vue](#dns-widget-headless-starters) [Display instructions](#dns-widget-headless-render) [Verify changes](#dns-widget-headless-verify) [API reference](#dns-widget-headless-api) [Handle errors](#dns-widget-headless-errors)

[Explore the JSON demo](https://cloud.approximated.app/dnswidget/demo/headless) to see the setup instructions alongside their API response.

<a id="dns-widget-headless-start"></a>

### 1. Request the setup instructions

For a browser integration, use our small JavaScript client. It has no dependencies, leaves the page’s HTML and styles to you, and renews the widget token while the customer works.

First, create an authenticated endpoint in **your app** that calls [GET /api/dns/v2/token](#dns-widget-token) with your cluster API key. Have it return `{ "token": "…" }` with `Cache-Control: no-store`. The examples call your endpoint `/api/dns-widget-token`; you need to implement it. Keep the API key on your server.

```html
<script src="https://cloud.approximated.app/dnswidget/headless.v2.js"></script>
<script type="module">
  const tokenResponse = await fetch("/api/dns-widget-token", {
    credentials: "same-origin",
    cache: "no-store"
  });
  if (!tokenResponse.ok) throw new Error("Could not start DNS setup");
  const { token } = await tokenResponse.json();
  if (typeof token !== "string" || !token) {
    throw new Error("Your token endpoint returned an invalid token");
  }

  const client = window.apxDnsHeadless.createClient({ token });
  const result = await client.instructions({
    domain: "shop.customer.com",
    records: [
      { type: "CNAME", host: "@", value: "domains.example.com", ttl: 3600 }
    ]
  });
  console.log(result.domains);
</script>
```

This asks the customer to point `shop.customer.com` at `domains.example.com`. Replace both with the domain being connected and your application’s target hostname. Here, `@` means the supplied domain itself, including its `shop` subdomain.

The snippet above logs the instructions. In your application, render them as described below and show a retry message if a request fails. If you prefer to call the API from your backend, skip to the [API reference](#dns-widget-headless-api).

<a id="dns-widget-headless-starters"></a>

### Start with React or Vue

Preview the Simple, Dashboard, and Guided designs in your browser, then adapt the v2 starter for your app. You can switch designs without losing your progress. Each public starter includes provider instructions with copy buttons, verification results, retries, and a server endpoint that creates scoped tokens.

- [React headless DNS widget starter](https://github.com/Approximated-Inc/headless-dns-widget-react-starter) · [Try React demo](https://cloud.approximated.app/dnswidget/demo/headless/react)
- [Vue headless DNS widget starter](https://github.com/Approximated-Inc/headless-dns-widget-vue-starter) · [Try Vue demo](https://cloud.approximated.app/dnswidget/demo/headless/vue)

Use GitHub’s “Use this template” button or clone the starter for your framework. Open the starter folder and, with Node.js 22.12 or newer installed, run:

```bash
npm ci
cp .env.example .env
```

In `.env`, set `APX_API_KEY` to your server API key and `VITE_CNAME_TARGET` to the hostname customers should point to.

Both starters use `headless.v2.js` and the `/api/dns/v2` API. Their server creates scoped customer tokens automatically. Start the interface:

```bash
npm run dev
```

Open `http://127.0.0.1:5173`. The API key stays on the Node server; the browser receives a short-lived token. The session code stops renewal when setup ends and ignores responses for a previous domain.

The included server runs locally only. When integrating with your application, move the token endpoint into your authenticated backend and check the customer's permission to configure the domain. Each repository’s README explains the files, configuration, and production integration.

<a id="dns-widget-headless-render"></a>

### 2. Display the provider and record steps

The response groups records by their root domain. For example, records for `shop.customer.com` and `www.customer.com` appear together under `apex_domain: "customer.com"`. Each group has a `provider` object and a `records` array.

1. Show `provider.message` above the records. Add `provider.login_url` as a link when it’s available.
2. For each record, use `record.title` as a heading and render `record.steps` in order.
3. Choose how to display each step using its `kind`, as shown below.

**Step kinds**

**`text`**

_Plain instruction_

Display `text` as a sentence, such as “Click the Save button to save changes”.

**`link`**

_Instruction with a link_

Display `text` as a link to `url`. This usually opens the provider’s DNS settings.

**`field`**

_Value the customer enters_

Show `text`, the provider’s field `label`, and a copyable `value`. The `field` key tells you whether it is the host, value, or TTL.

```json
{
  "kind": "field",
  "field": "host",
  "label": "Name",
  "value": "shop",
  "text": "Set Name to:"
}
```

For this step, your UI could show **Name: shop**. Use the step’s `value` for display: it already accounts for the provider’s conventions. An empty string means “leave this field blank”, and a TTL may be written as “1 Hour”. Render returned text as text, using your framework’s escaping or `textContent`.

If `record.automation` is present, you can also offer its `url` as an automatic setup link. Keep the manual steps available. A detected provider does not necessarily support automation; show the link only when it is returned.

If the provider can’t be identified, the API still returns general instructions. Use `provider.lookup_status` to distinguish a successful lookup from missing nameservers or a failed lookup, and give the customer a way to correct the domain or retry.

**View the full example response**

A 200 response for the CNAME request above, with Cloudflare detected and no automatic setup link available.

```json
{
  "domains": [
    {
      "apex_domain": "customer.com",
      "provider": {
        "name": "Cloudflare",
        "provider_domain": "cloudflare.com",
        "detected": true,
        "supported": true,
        "lookup_status": "ok",
        "nameservers": [
          "ada.ns.cloudflare.com",
          "rob.ns.cloudflare.com"
        ],
        "message": "We've detected Cloudflare as your DNS provider.",
        "message_link": null,
        "login_url": "https://dash.cloudflare.com/",
        "logo_url": null
      },
      "records": [
        {
          "domain": "shop.customer.com",
          "apex_domain": "customer.com",
          "subdomain": "shop",
          "type": "CNAME",
          "host": "@",
          "combined_host": "shop",
          "value": "domains.example.com",
          "match_against": "domains.example.com",
          "ttl": 3600,
          "title": "Add a DNS CNAME record for shop.customer.com",
          "automation": null,
          "steps": [
            {
              "kind": "link",
              "text": "Sign in to Cloudflare.",
              "url": "https://dash.cloudflare.com/"
            },
            {
              "kind": "text",
              "text": "Select the account for customer.com, then open that domain."
            },
            {
              "kind": "text",
              "text": "In \"DNS management for customer.com\", click Add Record."
            },
            {
              "kind": "text",
              "text": "Select CNAME as the record type."
            },
            {
              "kind": "field",
              "field": "host",
              "label": "Name",
              "value": "shop",
              "text": "Set Name to:"
            },
            {
              "kind": "field",
              "field": "value",
              "label": "Target",
              "value": "domains.example.com",
              "text": "Set Target to:"
            },
            {
              "kind": "field",
              "field": "ttl",
              "label": "TTL",
              "value": "3600",
              "text": "Set TTL to:"
            },
            {
              "kind": "text",
              "text": "Click Save to add the record."
            }
          ]
        }
      ]
    }
  ]
}
```

<a id="dns-widget-headless-verify"></a>

### 3. Check the customer’s changes

Once the customer has saved their DNS changes, let them run a check. Pass the original instructions result to `client.verify()`; the client sends the correct record fields for you. Run this when the customer clicks your check button, using the same `client` and `result` from step 1.

```javascript
try {
  const check = await client.verify(result);
  const state = client.summarize(check.records);
  console.log(state);         // "complete", "partial", or "failed"
  console.log(check.records); // Each record includes match and actual_values
} catch (error) {
  console.error(error.code, error.message);
  // Show a retry message in your UI.
}
```

**Verification results**

**`complete`**

_Every record matches_

The DNS changes are visible to the lookup. Continue with your application’s routing and HTTPS checks.

**`partial`**

_Some records match_

Show which records still need attention so the customer can check those values.

**`failed`**

_No records match_

Show the values found in DNS and let the customer retry. A “failed” summary means no records matched; a failed API request throws an error instead.

**The client checks for an exact match:** each queried name must have exactly one value for the requested record type, and it must equal `match_against`. To allow other values alongside the expected one, such as multiple TXT records, use the [check-records-exist API](./dns-checks-api.md#dns-check-records-exist) instead.

Each checked record includes `match` and `actual_values`. The latter is an array of values when DNS returns records, or `false` when no values are found. DNS updates can take time to become visible because of caching. A successful check confirms the DNS result; your backend should check the domain’s association with the customer, routing, and HTTPS before marking the domain connected.

**Verify without the JavaScript client**

From the browser, POST to `https://cloud.approximated.app/api/dns/v2/token/check-records-match-exactly` with `Content-Type: application/json`. Copy `domain`, `host`, `type`, and `match_against` from each returned record:

```json
{
  "token": "YOUR_WIDGET_TOKEN",
  "records": [
    {
      "domain": "shop.customer.com",
      "host": "@",
      "type": "CNAME",
      "match_against": "domains.example.com"
    }
  ]
}
```

From your backend, use [POST /api/dns/v2/check-records-match-exactly](./dns-checks-api.md#dns-check-records-match-exactly) with your `api-key` header. That endpoint takes the full `address` instead of `domain` and `host`:

```json
{
  "records": [
    {
      "address": "shop.customer.com",
      "type": "cname",
      "match_against": "domains.example.com"
    }
  ]
}
```

The address is `domain` when `host` is `@`, or `host + "." + domain` otherwise. Both `check-records-exist` endpoints also use `address`; the token variant additionally needs `token` in the body.

<a id="dns-widget-headless-client"></a>

### Manage the client’s lifecycle

Keep the client alive while the customer follows the instructions. It attempts renewal every five minutes; each token lasts up to 10 minutes and 30 seconds, within the session’s 24-hour deadline. Call `client.stop()` when the setup component is removed or the flow is finished. Handle renewal errors with `onError`. After expiry or key replacement, stop the old client and create a new one with a fresh token from your authenticated backend.

**Client methods and options**

**instructions(request)**

_Returns a promise_

Fetches the provider details and setup steps. Takes the same `domain` and `records` as the API below.

**verify(result)**

_Returns a promise_

Checks all records from an instructions result. Also accepts an array of records with `domain`, `host`, `type`, and `match_against` (or `value`).

**summarize(records)**

_Returns a string_

Returns `complete`, `partial`, or `failed` from the checked records. An empty array returns `failed`.

**stop()**

_Cleanup_

Stops token renewal. Call it when your setup component unmounts.

**renewToken() / getToken()**

_Token access_

`renewToken()` requests a renewal and returns a promise. `getToken()` returns the current token.

**createClient(options)**

_Configuration_

Pass `token`. Optional `api_url` defaults to `https://cloud.approximated.app/api/dns/v2`. Use `onTokenRenewed(token)` and `onError(error)` to handle background renewal results. Request errors still need a `try/catch`.

<a id="dns-widget-headless-api"></a>

### Instructions API reference

Both endpoints return the same JSON. Send `Content-Type: application/json` and `Accept: application/json`. Choose authentication based on where your code runs:

**POST · From the browser**

https://cloud.approximated.app/api/dns/v2/token/instructions

CORS-enabled. Include a short-lived widget `token` in the JSON body.

**POST · From your server**

https://cloud.approximated.app/api/dns/v2/instructions

Send your cluster API key in the `api-key` header. Omit `token` from the body.

```json
{
  "token": "YOUR_WIDGET_TOKEN",
  "domain": "shop.customer.com",
  "records": [
    { "type": "CNAME", "host": "@", "value": "domains.example.com", "ttl": 3600 }
  ]
}
```

**Request fields**

**`token`**

_String · Required in the browser_

The short-lived token returned by [the token endpoint](#dns-widget-token). Only include it on the browser endpoint.

**`domain`**

_String · Default for all records_

The domain to connect, such as `customer.com` or `shop.customer.com`. Required unless every record supplies its own `domain`. Use a domain with a registrable name and a TLD, such as `example.com`.

**`records`**

_Array · Required_

Send 1 to 25 records. You can include records for more than one domain; the response groups them by root domain.

**records\[type\]**

_String · Required_

`A`, `CNAME`, or `TXT`. Letter case does not matter; the response uses uppercase.

**records\[host\]**

_String · Required_

Use `@` for the record’s domain itself, or a relative name with no trailing dot. With `domain: "shop.customer.com"`, `@` targets `shop.customer.com` and `www` targets `www.shop.customer.com`.

**records\[value\]**

_String · Required_

The IPv4 address, target hostname, or text you want in DNS. Any `{domain}` placeholder is replaced with that record’s domain.

**records\[ttl\]**

_Integer · Optional_

Cache duration in seconds. Accepts a positive integer or numeric string; defaults to `3600` when omitted. Use the returned step value for provider-specific display.

**records\[domain\]**

_String · Optional override_

Set this when a record belongs to a different domain than the top-level default. An omitted or blank value uses the default.

<a id="dns-widget-headless-response"></a>

### Response field reference

A successful request returns `200` with a `domains` array. Groups appear in the order their root domains first appeared in your request; records within each group keep their original order. Each group contains `apex_domain`, `provider`, and `records`.

**Provider fields**

These fields are inside each group’s `provider` object, for example `result.domains[0].provider.name`. All ten keys are present; optional links and images can be `null`.

**Provider**

**name / provider_domain**

_Strings_

`name` is the display name, such as “Cloudflare”. `provider_domain` identifies the provider in your code, such as `cloudflare.com`. The fallback is `Generic` / `generic`.

**detected / supported**

_Booleans_

`detected` means the nameservers matched a known provider. `supported` means provider-specific instructions are available. Neither flag promises automatic setup.

**`lookup_status`**

_String_

`ok`: the lookup succeeded. `nameservers_not_found`: no nameservers were found; check the domain. `lookup_failed`: the lookup could not be completed; offer a retry.

**`nameservers`**

_Array of strings_

The nameservers used to detect the provider. Empty when none were found.

**`message`**

_String_

A sentence you can show above the instructions. General instructions ask the customer to go to their DNS provider.

**`message_link`**

_Object or null_

An optional link with `text` and `url` to show alongside the message, such as information about a provider migration.

**login_url / logo_url**

_Strings or null_

A link to the provider’s DNS settings and a hosted logo, when available. Each can be `null`.

**Record fields**

These fields are inside each item in a group’s `records` array, for example `result.domains[0].records[0].steps`.

**Record**

**`domain`**

_String_

The supplied domain, trimmed and lowercased, with any scheme, path, port, or trailing dot removed. International domains use punycode: `bücher.de` becomes `xn--bcher-kva.de`.

**apex_domain / subdomain**

_String / String or null_

`apex_domain` is the root domain, such as `customer.com`. `subdomain` is the part before it, such as `shop`, or `null` for the root itself.

**type / host**

_Strings_

`type` is `A`, `CNAME`, or `TXT`. `host` is the host you supplied, with surrounding whitespace removed.

**`combined_host`**

_String_

The host relative to the root domain. For `shop.customer.com`, `@` becomes `shop` and `www` becomes `www.shop`. For the value to display in the provider’s form, use the host step’s `value`.

**value / match_against**

_Strings_

`value` contains the requested value with any `{domain}` placeholder replaced. `match_against` is identical and is ready to send to verification.

**`ttl`**

_Integer_

The TTL in seconds, or `3600` when omitted. The steps contain the provider’s display wording.

**`title`**

_String_

A heading you can use above the record’s instructions.

**`automation`**

_Object or null_

When available: `{ "kind": "domain_connect", "url": "…" }`. Open the URL to let the customer approve automatic setup with their provider. Otherwise, this field is `null`.

**`steps`**

_Array of objects_

The ordered instructions for this record. Render them using the step kinds above, or use the record data to write your own instructions.

**Step fields**

These fields are inside each record’s `steps` array. The `kind` determines which keys are present.

**Step**

**kind / text**

_Strings · Every step_

`kind` is `text`, `link`, or `field`. `text` is the sentence to show.

**`url`**

_String · Link steps_

The destination for the instruction’s link.

**`field`**

_String · Field steps_

Which record value the step describes: `host`, `value`, or `ttl`.

**`label`**

_String · Field steps_

The input’s name in the provider’s interface, such as “Name” or “Target”.

**`value`**

_String · Field steps_

The value the customer should enter or select. Keep it as text, including values such as “1 Hour”. An empty string means leave the field blank.

DNS Widget v2 uses the `/api/dns/v2/` API and `headless.v2.js` client. New response fields may be added within v2, so ignore fields you don’t need. Breaking changes will use a new API path and client version; existing versions will keep their contract.

<a id="dns-widget-headless-errors"></a>

### Handle errors and expired sessions

The instructions endpoints return the following error statuses. The client exposes API failures as `ApxDnsError` objects with `code`, `message`, `status`, and `details`.

**API errors**

**401**

_API key endpoint_

The API key is invalid. Check the key your backend sends in the `api-key` header.

**403**

_Token endpoint_

`missing_token` means the token property is absent. A present but empty, malformed, expired, or revoked token returns `token_expired`; this also covers a token of the wrong type. Obtain a fresh token from your authenticated backend using a current API key and create a new client. Renewal cannot restore an expired or revoked token.

**422**

_Either endpoint_

`invalid_request` or `invalid_domain`. Check `details` for the record’s zero-based `index`, `field`, and `message`. Request-wide errors, such as too many records, can have an empty `details` array.

**429**

_Widget or API key endpoint_

Widget requests return JSON error `rate_limited` and `Retry-After: 60`; wait 60 seconds before retrying. The general API-key limit of 240 requests per minute can instead return plain text. See [request limits](#dns-widget-limits) for the separate IP, cluster, and session budgets.

```json
{
  "error": "invalid_domain",
  "message": "One or more records are invalid; see details",
  "details": [
    { "index": 0, "field": "domain", "message": "must be a valid hostname such as example.com or app.example.com" }
  ]
}
```

The client also uses `network_error` when a fetch fails, and `request_failed` for an unsuccessful response without a JSON error code. Calling `createClient()` without a token throws `missing_token` immediately. Handle request failures with `try/catch` and use the `onError` callback for token renewal failures.

<a id="dns-widget-versioning"></a>

## DNS Widget Versioning

Use v2 for token creation, the browser API, and JavaScript files:

IntegrationFilesAPI Scoped customer tokensYour authenticated server endpoint`GET /api/dns/v2/token` Embedded v2`headless.v2.js`, `dnswidget.v2.js`, and `dnswidget.v2.css``/api/dns/v2/` Headless v2, including React and Vue`headless.v2.js``/api/dns/v2/`

Customer sessions accept at most 25 A, CNAME, or TXT records per instructions request, with a valid domain, a non-empty host and value, and a positive numeric TTL when provided. Use `@` for the entered domain; omit TTL to use the default of 3600 seconds instead of passing `Auto`.

Embedded v2 verifies the normalized records returned by the API. Verification events contain those check results; use `match_against` for the expected value. Call `stop()` before removing the widget during client-side navigation.

Security fixes and compatible patches may update a pinned version. Breaking changes get new asset names or API paths; existing paths will not silently switch to a newer version.
