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.
DNS Widget Demo link
Try the widget with Classic, Dark, and Minimal styles in the full-page demo.
DNS Widget Basics link
- 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 to display that JSON in your own components.
DNS Widget Setup link
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.
-
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> -
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. -
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.10with your cluster IPv4 address. The example configures the apex A record and a CNAME atwww;{domain}is replaced with the domain entered by the customer.// 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); }); -
Optionally, set javascript listeners to call some of your own logic based on widget events.
// 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" } ] }); -
You can optionally stop and clear the widget element or restart the flow.
// 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()
DNS Widget Styling link
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 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:
.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;
}
Scoped DNS Widget Tokens link
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.
{
"token": "a-valid-token"
}
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.
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.
Headless Mode link
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.
Explore the JSON demo to see the setup instructions alongside their API response.
1. Request the setup instructions link
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 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.
<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.
Start with React or Vue link
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.
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:
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:
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.
2. Display the provider and record steps link
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.
- Show
provider.messageabove the records. Addprovider.login_urlas a link when it’s available. - For each record, use
record.titleas a heading and renderrecord.stepsin order. - Choose how to display each step using its
kind, as shown below.
text as a sentence, such as “Click the Save button to save changes”.text as a link to url. This usually opens the provider’s DNS settings.text, the provider’s field label, and a copyable value. The field key tells you whether it is the host, value, or TTL.{
"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.
{
"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."
}
]
}
]
}
]
}
3. Check the customer’s changes link
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.
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.
}
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 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:
{
"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 with your api-key header. That endpoint takes the full address instead of domain and host:
{
"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.
Manage the client’s lifecycle link
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.
domain and records as the API below.domain, host, type, and match_against (or value).complete, partial, or failed from the checked records. An empty array returns failed.renewToken() requests a renewal and returns a promise. getToken() returns the current token.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.Instructions API reference link
Both endpoints return the same JSON. Send Content-Type: application/json and Accept: application/json. Choose authentication based on where your code runs:
CORS-enabled. Include a short-lived widget token in the JSON body.
Send your cluster API key in the api-key header. Omit token from the body.
{
"token": "YOUR_WIDGET_TOKEN",
"domain": "shop.customer.com",
"records": [
{ "type": "CNAME", "host": "@", "value": "domains.example.com", "ttl": 3600 }
]
}
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.A, CNAME, or TXT. Letter case does not matter; the response uses uppercase.@ 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.{domain} placeholder is replaced with that record’s domain.3600 when omitted. Use the returned step value for provider-specific display.Response field reference link
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.
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 means the nameservers matched a known provider. supported means provider-specific instructions are available. Neither flag promises automatic setup.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.text and url to show alongside the message, such as information about a provider migration.null.Record fields
These fields are inside each item in a group’s records array, for example result.domains[0].records[0].steps.
bücher.de becomes xn--bcher-kva.de.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 is A, CNAME, or TXT. host is the host you supplied, with surrounding whitespace removed.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 contains the requested value with any {domain} placeholder replaced. match_against is identical and is ready to send to verification.3600 when omitted. The steps contain the provider’s display wording.{ "kind": "domain_connect", "url": "…" }. Open the URL to let the customer approve automatic setup with their provider. Otherwise, this field is null.Step fields
These fields are inside each record’s steps array. The kind determines which keys are present.
kind is text, link, or field. text is the sentence to show.host, value, or ttl.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.
Handle errors and expired sessions link
The instructions endpoints return the following error statuses. The client exposes API failures as ApxDnsError objects with code, message, status, and details.
api-key header.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.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.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 for the separate IP, cluster, and session budgets.{
"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.
DNS Widget Versioning link
Use v2 for token creation, the browser API, and JavaScript files:
| Integration | Files | API |
|---|---|---|
| Scoped customer tokens | Your 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.