Approximated helps you connect web domains to your application(s). These are often called custom domains when provided by your users.
This is accomplished by relaying internet traffic to your app(s) and back again for each domain that you configure, using a gloablly distributed set of machines called a Proxy Cluster.
This Proxy Cluster also provides and manages SSL certificates for each domain it's responsible for.
Approximated's Proxy Clusters are groups of machines distributed globally that serve traffic for you.
When you create one in Approximated, these machines are set aside and dedicated to you, along with your own dedicated IPv4 address.
You can create Virtual Hosts on your Proxy Cluster to tell it which domains to accept traffic from, and where to send it.
You can have as many Proxy Clusters as you need, and each one can scale to as many custom domains as you want.
Virtual hosts are entities that you create in Approximated.
They tell Approximated how to route requests
for custom domains. Each virtual host has an incoming address
field that matches the custom domain you want to route. It also
has a target address field that tells Approximated where to send
requests for that custom domain.
Every custom domain you want to connect needs to be pointed with
DNS at your Approximated proxy cluster, so that it can route them for
you. This can be done with an A record pointed at your cluster's
IPv4 address, or through an intermediary domain/subdomain with a
CNAME record.
If a DNS record is pointed at your cluster without a matching
virtual host, it will be ignored so that you control what custom
domains are routed by your cluster.
Choosing A records or CNAME records
Apex domains
An apex domain is a plain domain without www or any subdomains prepended.
Example apex domains: mydomain.com, google.com
These are not apex domains: www.mydomain.com, docs.google.com
DNS A records
The simplest way to point a domain at your cluster is a DNS A record,
but you should be aware of a few things:
If you ever choose to change to a different IP address, you
will need to have all custom domains change their A records
individually.
A records are the only broadly supported record type that allow pointing an apex domain (no
subdomain or www prepending it). CNAME records do not.
Example: customdomain.com (A record)
your cluster IP address
Using an intermediary with CNAMEs
Another option is to point an intermediary domain or subdomain
that you control at your cluster with an A record, and then have
custom domains point at the intermediary with a CNAME.
There are
a few things to know about this method:
Requests to custom domains will follow the CNAME and point
wherever your intermediary is pointed.
You can re-point all custom domains pointed this way at once
by changing the intermediary domain/subdomain.
Public DNS spec only allows CNAME records to point a subdomain
at another domain or subdomain. You can't point an apex
record using a CNAME, so it will need to have
a subdomain or www prepending it.
This will work:
www.customdomain.com (CNAME)
domains.myapp.com (A record)
your cluster IP address
This will not work because it points an apex domain with a CNAME:
customdomain.com (CNAME)
domains.myapp.com (A record)
your cluster IP address
TL;DR
If you're okay with your custom domains all having www or having another subdomain, use an intermediary domain with CNAMEs. Otherwise, use A records.
Approximated requires an private API key to accept API requests
from your application. This API key should never be made public or
added to client-side javascript code.
You can create or get an existing API key from the dashboard, and
add it to all API requests under the request header "api-key".
The virtual hosts API is how your application will interface
directly with Approximated. You can use it to automate custom
domains as needed. There are currently 6 endpoints:
create, list, read, update, bulk update, and delete.
Note:
Make sure that you include headers for
Content-Type and Accept set to 'application/json' for all requests to the API,
and be sure to include your API key under the header api-key.
In Approximated, a virtual host represents a custom domain mapping. It defines how a custom domain
(like "yourcustomdomain.com") should be routed to your application's actual domain (like "yourapp.com").
Virtual hosts are the core configuration objects in Approximated,
and are automatically tied to a proxy cluster by the API key used to create them.
Each virtual host contains the following fields:
Core Fields
id
The unique identifier for the virtual host.
incoming_address
The custom domain that points to Approximated (e.g., "yourcustomdomain.com").
target_address
The destination where requests should be routed (e.g., "yourapp.com").
When load balancing is enabled, this becomes the first upstream.
target_ports
The port(s) to use for the target. Default is "443" for HTTPS traffic.
keep_host
Controls whether the original Host header is preserved (true) or changed to the target address (false).
When null, uses the cluster default setting.
Load Balancing Fields
lb_enabled
Boolean that indicates whether load balancing is enabled for this virtual host.
lb_policy
The policy for distributing requests: "first", "random", "round_robin", or "least_conn".
lb_sticky
Boolean that indicates whether to maintain user connections to the same upstream server using cookies.
upstreams
Array of additional servers for load balancing, each with id, host, port, and enabled properties.
The target_address is automatically included as the first upstream.
Monitoring Fields
created_at
Timestamp when the virtual host was created.
has_ssl
Boolean indicating whether SSL is active for the domain.
is_resolving
Boolean indicating whether the domain is resolving correctly.
ssl_active_from
The start of the validity period for the SSL certificate.
ssl_active_until
The ending of validity period for the SSL certificate.
status
Current combined status code, such as ACTIVE_SSL.
Generally it's better to use apx_hit and has_ssl
to determine if a custom domain is successfully connected.
status_message
Current human-readable status description.
dns_pointed_at
The IP address the custom domain currently points to.
apx_hit
Boolean indicating whether requests are successfully reaching the Approximated cluster.
last_monitored_unix
A unix timestamp of the last monitoring check.
last_monitored_humanized
The human-readable time of the last monitoring check.
Not all fields are returned in every API response, and some fields (like monitoring data) are only available for existing virtual hosts.
The sections below detail the endpoints for creating, listing, reading, updating, and deleting virtual hosts through the API.
Creating a virtual host is done with a JSON POST request to the
Approximated API. It can be created at any time before or after
the custom domain is pointed at the cluster with a DNS record.
POST
https://cloud.approximated.app/api/vhosts
Returns
201 - Successfully created
// Example response:
{
"data": {
"id": 445922,
"incoming_address": "acustomdomain.com",
"target_address": "myapp.com",
"target_ports": "443",
"keep_host": null,
"lb_policy": "round_robin",
"lb_enabled": true,
"lb_sticky": false,
"upstreams": [
{
"id": 123,
"host": "myapp.com",
"port": 443,
"enabled": true
},
{
"id": 124,
"host": "server2.example.com",
"port": 443,
"enabled": true
}
],
"user_message": "In order to connect your domain, you'll need to have a DNS A record that points acustomdomain.com at 213.188.210.168. If you already have an A record for that address, please change it to point at 213.188.210.168 and remove any other A records for that exact address. It may take a few minutes for your SSL certificate to take effect once you've pointed your DNS A record."
}
}
422 - Validation errors
// Example response
{
"errors": {
"incoming_address": [
"This incoming address has already been created on the cluster you selected."
],
"upstreams": [
{
"params": {
"host": "",
"port": 0,
"enabled": true
},
"index": 0,
"errors": {
"host": ["can't be blank"],
"port": ["must be greater than 0"]
}
}
]
}
}
401 - The API key used does not exist
Fields
incoming_address
Required
String
The custom domain that you'd like to route.
Example: acustomdomain.com
target_address
Required
String
The address that you'd like requests for the custom domain
to be routed to. Typically another domain or domain with a
sub-page.
If load balancing is enabled, this will be used as the first upstream
in the load balancing pool.
Example: myapp.com, myapp.com/some/page
target_ports
Optional
String
Default: "443"
This sets the port that you'd like requests to arrive at
on the target address. By default it is port 443 as that
is the port that web traffic secured by SSL is typically
served from.
Example: 443, 80, 8080 (string)
redirect
Optional
Boolean
Default: false
Set this to true if you'd like to have requests be 301
redirected to the target address instead of proxied.
Note: redirects need to have the protocol (http:// or
https://) included in the target_address, or they will be
appended to the incoming address.
Example: true or false (boolean)
exact_match
Optional
Boolean
Default: false
Set this to true if you'd like to have requests that
exactly match the incoming address, including paths, be
overridden and routed somewhere specific. Typically this
is used in combination with another virtual host
configured for the base custom domain.
Note: this will ignore any extra user-added paths or
queries if it matches, and will override any other virtual
hosts for the same domain that don't exactly match.
Example: true or false (boolean)
redirect_www
Optional
Boolean
Default: false
For convenience, when set to true, Approximated will create a second virtual host as well that will 301 redirect the www version of the incoming address to this address.
Note: www redirects to an existing virtual host will not count towards your billable hosts.
Example: true or false (boolean)
keep_host
Optional
Boolean
Default: null (uses cluster default)
Set this to true if you'd like the Host header to be left as the incoming address (the custom domain) for this virtual host.
Set to false if you'd like to override the cluster default setting for this, or null if you'd like to use the default setting.
Note: when set to false, either by default or when explicitly set here,
your cluster will change the Host header for each request to the target address by default.
This can often avoid issues with servers/reverse proxies out of the box.
Example: true, false, null (boolean)
lb_enabled
Optional
Boolean
Default: false
Set this to true to enable load balancing for this virtual host. When enabled,
requests will be distributed across multiple upstreams according to the selected load balancing policy.
Note: When load balancing is enabled, the target_address will automatically be used
as the first upstream in the load balancing pool.
Example: true or false (boolean)
lb_policy
Optional
String
Default: "first"
The load balancing policy to use when distributing requests across upstreams.
Only applicable when lb_enabled is true.
Available policies:
first - Select first healthy upstream in the list
random - Randomly select a healthy upstream
round_robin - Distribute evenly in a round robin sequence
least_conn - Select healthy upstream with fewest connections
Example: "round_robin"
lb_sticky
Optional
Boolean
Default: false
When set to true, attempts to keep users connected to the same upstream
once they've been assigned to one. This uses a cookie to track which upstream
the user should connect to.
The first connection uses the lb_policy to select an upstream. If that upstream
becomes unhealthy, a new one will be selected using the lb_policy again.
Example: true or false (boolean)
upstreams
Optional
Array of Objects
An array of additional upstream servers to include in the load balancing pool.
Only used when lb_enabled is true.
Note: The target_address will always be used as the first upstream.
If the target_address contains a path, that path will be automatically
prepended to the path for all upstreams as well.
Each upstream object requires:
host - The hostname of the upstream server
port - The port of the upstream server (usually 443 for HTTPS)
We use a cursor based pagination system to page through your virtual hosts, up to 20 at a time.
You can get the first page of the list by calling the first endpoint below, without a cursor.
You'll receive back a JSON object with a data field that contains the list of virtual hosts, as well as an after_cursor and before_cursor.
You can then get the next or previous page by calling the endpoints below with the after or before cursor from the current results.
// Example response
{
"data": [
{
"apx_hit": true, // requests are reaching the cluster
"created_at": "2023-04-03T17:59:28", // UTC timezone
"dns_pointed_at": "213.188.210.168", // DNS for the incoming_address
"has_ssl": true,
"id": 405455,
"incoming_address": "acustomdomain.com",
"is_resolving": true, // is this returning a response
"last_monitored_humanized": "1 hour ago",
"last_monitored_unix": 1687194590,
"ssl_active_from": "2023-06-02T20:19:15", // UTC timezone
"ssl_active_until": "2023-08-31T20:19:14", // UTC timezone, auto-renews
"status": "ACTIVE_SSL",
"status_message": "Active with SSL",
"target_address": "myapp.com",
"target_ports": "443",
"keep_host": null,
"lb_policy": "round_robin",
"lb_enabled": true,
"lb_sticky": false,
"upstreams": [
{
"id": 123,
"host": "myapp.com",
"port": 443,
"enabled": true
},
{
"id": 124,
"host": "server2.example.com",
"port": 443,
"enabled": true
}
]
},
// More virtual hosts...
],
"after_cursor": "a39fdk32kf", // will be null if there is no next page
"before_cursor": "lf3jeuc3406" // will be null if there is no previous page
}
401 - The API key used does not exist
The monitoring fields and statuses are from the latest monitoring results. A fresh check is not performed for the list before responding, and it cannot be force checked like an individual virtual host.
Use a GET request with the incoming_address at the end of the URL to retrieve the details of a single Virtual Host.
If that Virtual Host was created with the API Key included in the header, it's details will be returned.
Note: to get by incoming incoming address, it must not include paths, query strings, or a protocol like https:// in the incoming address value.
If you've included those in the virtual host you'd like to get, please use the alternative POST endpoint below that takes a JSON object with incoming_address instead.
// Example response
{
"data": {
"apx_hit": true, // requests are reaching the cluster
"created_at": "2023-04-03T17:59:28", // UTC timezone
"dns_pointed_at": "213.188.210.168", // DNS for the incoming_address
"has_ssl": true,
"id": 405455,
"incoming_address": "acustomdomain.com",
"is_resolving": true, // is this returning a response
"last_monitored_humanized": "1 hour ago",
"last_monitored_unix": 1687194590,
"ssl_active_from": "2023-06-02T20:19:15", // UTC timezone
"ssl_active_until": "2023-08-31T20:19:14", // UTC timezone, auto-renews
"status": "ACTIVE_SSL",
"status_message": "Active with SSL",
"target_address": "myapp.com",
"target_ports": "443",
"keep_host": null,
"lb_policy": "round_robin",
"lb_enabled": true,
"lb_sticky": false,
"upstreams": [
{
"id": 123,
"host": "myapp.com",
"port": 443,
"enabled": true
},
{
"id": 124,
"host": "server2.example.com",
"port": 443,
"enabled": true
}
]
}
}
404- Could not find Virtual Host with that API key
401 - The API key used does not exist
In order to avoid spamming your custom domains with monitoring checks every time you call this endpoint,
Approximated returns the results of the most recent recorded status check, which may be out of date.
If you'd prefer to have it check again before responding,
you can add /force-check to the end of the endpoint URL. This may take up to 30 seconds if the domain DNS is not pointed yet,
and is rate limited to minimize accidentally DDOSing your application.
Updating a virtual host is done with a JSON POST request to the
Approximated API. It can be updated at any time before or after
the custom domain is pointed at the cluster with a DNS record.
Any optional fields not submitted will remain the same as they were previously.
// Example response
{
"data": {
"apx_hit": true, // requests are reaching the cluster
"created_at": "2023-04-03T17:59:28", // UTC timezone
"dns_pointed_at": "213.188.210.168", // DNS for the incoming_address
"has_ssl": true,
"id": 405455,
"incoming_address": "adifferentcustomdomain.com",
"is_resolving": true, // is this returning a response
"last_monitored_humanized": "1 hour ago",
"last_monitored_unix": 1687194590,
"ssl_active_from": "2023-06-02T20:19:15", // UTC timezone
"ssl_active_until": "2023-08-31T20:19:14", // UTC timezone, auto-renews
"status": "ACTIVE_SSL",
"status_message": "Active with SSL",
"target_address": "myapp.com",
"target_ports": "443",
"keep_host": true,
"lb_policy": "least_conn",
"lb_enabled": true,
"lb_sticky": false,
"upstreams": [
{
"id": 123,
"host": "myapp.com",
"port": 443,
"enabled": true
},
{
"id": 125,
"host": "backup-server.example.com",
"port": 443,
"enabled": true
}
]
}
}
422 - Validation errors
// Example response
{
"errors": {
"incoming_address": [
"This incoming address has already been created on the cluster you selected."
],
"upstreams": [
{
"params": {
"host": "",
"port": 0,
"enabled": true
},
"index": 0,
"errors": {
"host": ["can't be blank"],
"port": ["must be greater than 0"]
}
}
]
}
}
404 - Could not find an existing Virtual Host with that incoming address
401 - The API key used does not exist
Fields
current_incoming_address
Required
String
The custom domain for an existing Virtual Host.
Example: acustomdomain.com
incoming_address
Optional
String
A new custom domain you would like to change the existing Virtual Host to.
Example: adifferentcustomdomain.com
target_address
Optional
String
The address that you'd like requests for the custom domain
to be routed to. Typically another domain or domain with a
sub-page.
If load balancing is enabled, this will be used as the first upstream
in the load balancing pool.
Example: myapp.com, myapp.com/some/page
target_ports
Optional
String
Default: "443"
This sets the port that you'd like requests to arrive at
on the target address. By default it is port 443 as that
is the port that web traffic secured by SSL is typically
served from.
Example: 443, 80, 8080 (string)
redirect
Optional
Boolean
Default: false
Set this to true if you'd like to have requests be 301
redirected to the target address instead of proxied.
Note: redirects need to have the protocol (http:// or
https://) included in the target_address, or they will be
appended to the incoming address.
Example: true or false (boolean)
exact_match
Optional
Boolean
Default: false
Set this to true if you'd like to have requests that
exactly match the incoming address, including paths, be
overridden and routed somewhere specific. Typically this
is used in combination with another virtual host
configured for the base custom domain.
Note: this will ignore any extra user-added paths or
queries if it matches, and will override any other virtual
hosts for the same domain that don't exactly match.
Example: true or false (boolean)
redirect_www
Optional
Boolean
Default: false
For convenience, when set to true, Approximated will create a second virtual host as well that will 301 redirect the www version of the incoming address to this address.
Note: www redirects to an existing virtual host will not count towards your billable hosts.
Example: true or false (boolean)
keep_host
Optional
Boolean
Default: null (uses cluster default)
Set this to true if you'd like the Host header to be left as the incoming address (the custom domain) for this virtual host.
Set to false if you'd like to override the cluster default setting for this, or null if you'd like to use the default setting.
Note: when set to false, either by default or when explicitly set here,
your cluster will change the Host header for each request to the target address by default.
Setting this to false can often avoid issues with servers/reverse proxies out of the box,
but you'll need to use the apx-incoming-host header in your app to determine the custom domain.
Example: true, false, null (boolean)
lb_enabled
Optional
Boolean
Default: false
Set this to true to enable load balancing for this virtual host. When enabled,
requests will be distributed across multiple upstreams according to the selected load balancing policy.
Note: When load balancing is enabled, the target_address will automatically be used
as the first upstream in the load balancing pool.
Example: true or false (boolean)
lb_policy
Optional
String
Default: "first"
The load balancing policy to use when distributing requests across upstreams.
Only applicable when lb_enabled is true.
Available policies:
first - Select first healthy upstream in the list
random - Randomly select a healthy upstream
round_robin - Distribute evenly in a round robin sequence
least_conn - Select healthy upstream with fewest connections
Example: "round_robin"
lb_sticky
Optional
Boolean
Default: false
When set to true, attempts to keep users connected to the same upstream
once they've been assigned to one. This uses a cookie to track which upstream
the user should connect to.
The first connection uses the lb_policy to select an upstream. If that upstream
becomes unhealthy, a new one will be selected using the lb_policy again.
Example: true or false (boolean)
upstreams
Optional
Array of Objects
An array of additional upstream servers to include in the load balancing pool.
Only used when lb_enabled is true.
Note: The target_address will always be used as the first upstream.
If the target_address contains a path, that path will be automatically
prepended to the path for all upstreams as well.
Important: Providing this field will replace ALL existing upstreams.
If you want to add new upstreams while keeping existing ones, you should
first retrieve the current upstreams with the Read endpoint.
Each upstream object requires:
host - The hostname of the upstream server
port - The port of the upstream server (usually 443 for HTTPS)
This endpoint allows you to update multiple virtual hosts at once with the same settings.
It's useful when you need to change settings across many domains simultaneously, such as updating
the target address for all domains in a batch.
You can update up to 100 virtual hosts in a single request. If more than 100 domains are provided,
only the first 100 unique domains will be processed.
// Example response for validation errors
{
"errors": {
"target_address": ["can't be blank"],
"upstreams": [
{
"params": {
"host": "",
"port": 0,
"enabled": true
},
"index": 0,
"errors": {
"host": ["can't be blank"],
"port": ["must be greater than 0"]
}
}
]
}
}
400 - Bad request format
// Example response for bad request
{
"error": "Invalid incoming_addresses: must be a list"
}
401 - The API key used does not exist
Fields
incoming_addresses
Required
Array of Strings
A list of domain names (incoming addresses) that you want to update.
Duplicates will be automatically removed, and a maximum of 100 domains will be processed.
An object containing the fields you want to update for all the specified domains.
This can include any of the standard virtual host properties (except for incoming_address,
which cannot be updated in bulk).
If you want to update load balancing upstreams for all the specified domains,
include this array of upstream objects. Each object should contain host, port,
and enabled fields.
Note: Including this field will replace ALL existing upstreams for the virtual hosts.
If you set this to an empty array, it will delete all existing upstreams.
Set this to true to enable load balancing for all the specified domains.
When enabled, requests will be distributed across the target_address and any
additional upstreams provided.
Example: true
updates.lb_policy
Optional
String
The load balancing policy to use for distributing requests.
Available options: "first", "random", "round_robin", "least_conn"
Example: "round_robin"
updates.lb_sticky
Optional
Boolean
When enabled, attempts to keep users connected to the same upstream
once they've been assigned one. Uses a cookie for tracking.
Use a DELETE request with the incoming_address or with the virtual host ID, to remove a single Virtual Host.
Note: to delete by incoming incoming address, it must not include paths, query strings, or a protocol like https:// in the incoming address value.
If you've included those in the virtual host you'd like to delete, please use the alternative POST version of this endpoint that takes a JSON request object with a field for incoming_address instead.
Virtual hosts can have different status codes that indicate their current state based on a variety of montoring checks.
These are provided as a convenience, but may not be as granular as combining the various fields available
within the virtual host response object yourself.
Note: this list may be added to in the future as we improve monitoring further,
but existing statuses will not be deprecated without a new major API version release.
ACTIVE_SSL
Requests to his virtual host are reaching the cluster, it's DNS is pointed correctly, it has an active SSL certificate, and has no known issues.
ACTIVE_SSL_PROXIED
Requests to this virtual host are reaching the cluster despite being pointed elsewhere with DNS, which means it's likely being proxied through somewhere else first. It has an active SSL certificate and no known issues.
ACTIVE_NO_SSL
Requests to this virtual host are reaching the cluster, but an SSL certificate has not yet been issued for it. This status is very rare and likely to be very short lived.
TARGET_NOT_LOADING
Requests to this virtual host are reaching the cluster but the upstream target is not responding.
DNS_INCORRECT
Requests to this virtual host are not reaching the cluster, and it's DNS appears to be pointed elsewhere.
DNS_NOT_RESOLVING
Requests to this virtual host are not reaching the cluster and it appears to have no DNS records set at all.
UNKNOWN
We are not able to determine a reliable status for this virtual host at this time, but will keep trying. This status is very rare and likely to be short lived.
The DNS checks API allows you to check if DNS records exist for a domain or subdomain.
It's provided as a convenience for your application, for scenarios like:
Ensuring a user has pointed an A or CNAME record.
Using TXT records to validate domain ownership.
Helping you to support/debug DNS issues for your users.
Note:
Make sure that you include headers for
Content-Type and Accept set to 'application/json' for all requests to the API,
as well as including an Api-Key header with a current API key.
Send a JSON POST request to the Approximated API with a list of
data to compare against DNS records. Determines if an address
has exactly one matching record for each data object in the list.
This check returns the list back to you with the results for 'match' and 'actual_values' injected into each object.
The 'match' field will only be true if there is exactly one DNS record/value for each address,
and it must exactly match the 'match_against' value you've set.
The address of the record you want to check. Typically
a domain/subdomain. Do not use placeholders like @ that you
may see in DNS dashboards, but rather the complete address.
Example: myapp.com, subdomain.myapp.com
record[type]
Required
String
The DNS record type you'd like to check, formatted
as a lowercase string.
Example: "a", "cname", "ns", "txt"
record[match_against]
Required
String
This is the value that you'd like the record value to be compared against.
The value here can be any string, as all record values will be converted to strings.
Note: there can be multiple records/values for the same address.
This endpoint will only return "match": true if there is exactly one record,
and that record exactly matches this field.
Send a JSON POST request to the Approximated API with a list of
data you'd like to check against the DNS records of an address.
DNS allows multiple records for the same address,
and you may wish to check that at least one of them matches while disregarding the rest.
This check is for that purpose.
The data list is returned back to you with injected fields for 'match' and 'actual_values'.
The 'match' field will be true if there is any record/value that matches the 'match_against' field,
regardless of how many other records/values there may be for that address.
The address of the record you want to check. Typically
a domain/subdomain. Do not use placeholders like @ that you
may see in DNS dashboards, but rather the complete address.
Example: myapp.com, subdomain.myapp.com
record[type]
Required
String
The DNS record type you'd like to check, formatted
as a lowercase string.
Example: "a", "cname", "ns", "txt"
record[match_against]
Required
String
This is the value that you'd like the record value to be compared against.
The value here can be any string, as all record values will be converted to strings.
Note: there can be multiple records/values for the same address.
This endpoint will return "match": true if any one of them matches.
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 will detect the DNS provider for custom domains and offer one-click automatic DNS updates to the user when possible.
In cases where the DNS provider does not allow automation, the widget will generate instructions specific to the DNS provider and the custom domain so that you don't have to.
We've tried to make the DNS widget easily compatible with as many different stacks and approaches as possible.
You can follow the instructions below to get started quickly.
Note: you can take a look at this file and override styles and variables as needed with your own CSS as well.
Call the widget init function using javascript:
// Create the config data for the widget.
const apx_dns_widget_config = {
// A 10 minute token is generated server-side to allow access to the API.
token: "use_token_api_endpoint_server_side_to_generate_this",
api_url: "https://cloud.approximated.app/api/dns",
/*
* 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: "123.456.789.01",
ttl: 3600,
},
{
type: "CNAME",
host: "@",
value: "example.com",
ttl: 3600,
}
],
domain: "customdomain.com", // optional, skips the domain entry UI step
prefillDomain: "customdomain.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.domain will contain 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: [
"123.456.789.01"
],
address: "example.com",
apex: "example",
domain: "example.com",
full: "example.com",
host: "@",
match: true, // Approximated found an exact match DNS record
match_against: "123.456.789.01", // The value to match against the DNS record
non_tld: "example",
subdomain: "example",
tld: "com",
type: "a",
value: "123.456.789.01"
},
{
actual_values: false,
address: "example.com",
apex: "example",
domain: "example.com",
full: "example.com",
host: "@",
match: false, // Approximated could not find a matching DNS record
match_against: "anotherexample.com", // The value to match against the DNS record
non_tld: "example",
subdomain: "example",
tld: "com",
type: "cname",
value: "anotherexample.com"
}
]
});
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()
The DNS widget has some fairly neutral styling applied already, but you can override it with your own CSS as needed.
It's outermost element has a default width of 700px applied, but has no height, padding, or borders applied by default so that you can more easily place it within your app.
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:
To prevent abuse, we require a client-side public token to be generated with a server-side call to our API DNS Token endpoint.
This will return a token that can be added to the widget config data.
Note: make sure you include the API key for your proxy cluster whenever using the API.
GET
https://cloud.approximated.app/api/dns/token
Returns
200 - Success
{
"token": "a-valid-token"
}
401 - The API key used does not exist
No fields/data required.
The token expires in 10 minutes, but if the page is still open, the widget will automatically use the existing token to
get a new, valid token before 10 minutes are up.
To provide improvements and new features without unexpected surprises, we version the DNS widget with simple integer versioning in the form of dnswidget.v1.js and dnswidget.v1.css.
Patches, fixes, and other non-breaking updates will update the existing version, while any breaking changes will be placed on a newer version.
You can remain on older versions as long as needed, and should assume any new version will need to be tested with your application before deploying.
Approximated allows you to receive notifications about events happening with your virtual hosts by sending HTTP GET or POST requests to a URL you control.
Webhooks can be configured from your dashboard by logging in and navigating to the "Webhooks" section. A basic log history for each configured webhook is also available there.
Each webhook event includes an idempotent_key in its payload (e.g., "apx-whl-<integer id>"). This key is unique for each distinct event instance.
If Approximated needs to retry sending an event, the idempotent_key for that specific event instance will remain the same across all retries. You can use this key on your server to detect and deduplicate events, preventing accidental reprocessing of the same event.
For an added layer of security, you can configure each webhook with a secret string key in the dashboard.
If a key is configured, Approximated will include it in every webhook request sent to your endpoint via the webhook-key HTTP header. You can then verify this key on your server to ensure that the request genuinely originated from Approximated.
Approximated offers flexibility in how you route webhook events:
Single Webhook, Multiple Events: You can create a single webhook configuration (pointing to one URL) and have it trigger for multiple different event types (e.g., Virtual Host Created, Virtual Host Deleted). Your endpoint will then need to inspect the type and event fields in the payload to differentiate and process them accordingly.
Multiple Webhooks, Different Endpoints/Events: Alternatively, you can create several distinct webhook configurations. Each can point to a different URL or the same URL, and each can be configured to trigger for specific, individual events. This allows you to route different event types to different processing logic or microservices if needed.
Webhooks and their data are intended to be sent only to SSL/TLS encrypted endpoints. Each payload is not separately encrypted, so please ensure that any configured endpoints are setup with SSL/TLS certificates of their own.
Triggered when a new virtual host is successfully created through the API or dashboard.
Note: this event is triggered when a Virtual Host is added to your cluster but does not necessarily mean that the custom domain is successfully connected to the cluster yet.
Triggered when the configuration of an existing virtual host is updated through the dashboard or API.
Note: this event is NOT triggered by monitoring changes like DNS or SSL statuses changing.
To be notified of those changes, please use the
Virtual Host Monitor Updated event
.
Triggered when the active monitoring for each virtual host incoming address detects a change in the DNS, SSL, APX Hit, or resolving statuses.
Included for convenience is a 'changes' field that will provide the old and new values for any fields that have changed.
Note: this event is NOT automatically triggered by configuration changes made to a virtual host using the dashboard or API, though they may indirectly result in this event being triggered if statuses change.
To be notified of configuration changes to the virtual host, please use the
Virtual Host Updated event
instead.
Multiple IP addresses in DNS values: In some cases, monitors will find multiple IP addresses in the DNS records for a domain.
This occurs when there are either multiple records with different IP addresses, for the same host, or in some cases when a CNAME record is used to indirectly point the custom domain through another domain/subdomain.
Multiple IP addresses are not always an issue, particularly when combining Approximated Cloud with Self Hosted instances, but it can be an indication of improperly configured DNS records for a domain.
Edge Verify is invisible bot protection for your forms, built into your Approximated proxy.
There are no CAPTCHA puzzles for your visitors to solve, no third-party scripts, no keys to manage, and no cookies.
Unlike CAPTCHA services, there's also no server-side integration: no verification API to call, no SDK to install, and no backend changes.
Because Approximated is already your proxy, the edge validates submissions itself and blocks bad ones before they ever reach your app.
A small script, served first-party from your own domain by the edge, transparently proves that a visitor is a real browser using a background proof-of-work and environment signals.
It then mints a short-lived signed token (valid for about 10 minutes and refreshed automatically) that is bound to the visitor and domain, and is single-use.
The script injects the token as a hidden _apx_verify_token field into every form on the page, including forms added later by single-page apps.
When the form is submitted, the edge validates the token and either passes the request through to your app or blocks it.
Edge Verify blocks non-browser spam outright and makes automated abuse significantly more expensive, while staying invisible to your real users.
The scoring intentionally errs toward letting a human through, because a false block would break a real form submission.
Note: the script is served from your own domain by the edge, so there's no third-party URL and nothing extra to host.
Create an Edge Verify rule for your virtual host by logging into your dashboard and navigating to your virtual host, then More, then the Edge Verify tab.
You can also create rules for every virtual host at once from your cluster's panel.
A rule consists of:
Method and path: the form submission to protect, e.g. POST /contact.
Mode:monitor or enforce, described below.
That's it - there is no step 3. There's no verification endpoint to call from your server, no secret to store, and no code change in your app.
The edge blocks bad submissions before they reach your origin.
Every matching submission is checked and its outcome recorded, but nothing is ever blocked.
We recommend starting every rule in monitor mode.
Enforce
Submissions with a missing or invalid token receive a 403 response at the edge and never reach your app.
Running in monitor mode first lets you watch the stats and confirm that real submissions are passing before anything is blocked.
Anything that would trip up the widget, like broken javascript on a page or an unusual browser, shows up as data instead of lost submissions.
Once the passed rate looks right, flip the rule to enforce.
Everything you can do with rules in the dashboard can also be automated through the REST API, using the same headers and api-key as the
Virtual Hosts API.
Rule changes propagate to the edge automatically within about a minute.
Rules can be managed per virtual host, identified by its incoming address in the JSON body -
the same by-incoming convention as the
Virtual Hosts API endpoints.
All four endpoints are POST requests:
Delete a rule. The body takes the incoming_address and the rule id, and returns a 204 with no content.
Or cluster-wide, where a rule applies to every virtual host on the cluster.
The cluster is implicit from your API key, so there's no cluster id in the path:
Regular HTML form posts need nothing extra - the hidden _apx_verify_token field is injected for you.
If you submit with fetch or another javascript request instead, ask the widget for a token and send it in the X-Apx-Verify-Token header:
The Edge Verify tab shows an outcomes panel for each rule, with a chart and a time range picker, available per virtual host and for the whole cluster.
Submissions are counted by outcome:
Passed
The submission carried a valid token and went through to your app.
Missing
No token was present at all - typical of bots posting directly to your form endpoint without running a browser.
Invalid
A token was present but failed validation, such as a forged token or one bound to a different visitor or domain.
Expired
The token was valid once but too old by the time the form was submitted.
Replayed
The token had already been used for a previous submission.
The block rate shown is the share of submissions that didn't pass.
In monitor mode, that's the share that would have been blocked, which is exactly what you want to review before switching a rule to enforce.
The form must POST to the proxied domain. Edge Verify can't protect forms that submit to a third-party or un-proxied endpoint, because those submissions never pass through the edge.
Visitors with broken javascript or unusual browsers can't run the widget. In monitor mode these surface as missing-token data rather than lost submissions, which is another reason to start there.
If a legitimate form is being flagged, check with your browser's dev tools that the hidden _apx_verify_token field is present in the submitted form, confirm the rule's method and path match the submission exactly, and use monitor mode while debugging.
Edge Verify is currently rolling out across clusters, so it may not be available on yours quite yet. If you don't see the Edge Verify tab, feel free to reach out and we can prioritize your cluster.
Because a rule is just a method and a path, and the token can ride a header, Edge Verify isn't limited to classic form submits.
A valid token proves the request came from a real browser running your page, so you can attach it to anything your frontend calls:
login and signup endpoints (against credential stuffing and signup abuse), comment and review submissions, votes, add-to-cart, or any JSON API behind your pages.
The mechanics are the same as any other javascript submission - send the token in the X-Apx-Verify-Token header as shown in
Fetch and JSON Submissions, and create a rule for the endpoint's method and path.
Two boundaries to keep in mind: this only applies to browser traffic, since mobile apps and server-to-server clients can't run the widget.
And plain page navigations aren't the target - the widget can't attach a token to a regular GET page load.
Edge Verify runs entirely first-party on your own domain. It sets no cookies, stores nothing in the browser, does no cross-site tracking, and sends no data to third parties.
That makes it a much simpler story for privacy policies and consent banners than third-party CAPTCHA services.
When a request for a custom domain reaches your application, you likely want to return content specific to that custom domain.
For example, if your app hosts blogs, the custom domain should return content for that particular blog.
The sections below should help guide you in making any changes necessary to your application in order to handle custom domains.
When a request goes through your Approximated cluster, it's relayed as shown below:
Approximated provides SSL encryption between the user and the cluster, but only your app or server can provide SSL encryption between the cluster and your app.
If your target address is a naked IP address, you won't be able to SSL encrypt a connection to it or use port 443 (typically reserved for SSL connections).
Luckily, most applications already have a domain or subdomain pointed at their application with an SSL certificate.
It depends on your app, but it's very likely you can simply re-use this as your target address for custom domains.
Approximated has a few ways that your app can determine the custom domain for a request.
(Default) Apx-Incoming-Host header
By default, Approximated will change the host header in each request to match the target address instead of the incoming address (the custom domain).
We've made this choice because often apps, servers, or reverse proxies are not ready to handle any domain but the primary app domain without modifications.
We always inject an extra header "Apx-Incoming-Host" to every request, which the app can use to determine the custom domain when it receives the request.
Keep the Host header as-is
Alternatively, you can set your proxy cluster to keep the Host header for each request as the incoming address instead of modifying it.
You can find this by opening your proxy cluster in the dashboard and changing the Keep Host Headers setting to True. This can be overridden for each virtual host, as well.
To do so, in the dashboard under advanced settings you can set Keep Host to True/False/Default. In the API, you can set keep_host to true/false/null to achieve the same.
Send the X-Forwarded-Host header
Finally, you can set your proxy cluster to add an X-Forwarded-Host header containing the incoming address for each request. This is independent of the other settings.
You can find this by opening your proxy cluster in the dashboard and changing the Send X-Forwarded-Host setting to True.
With statically generated content, you probably have folders and files sitting on a server for each custom domain.
For example, a request to mybloghost.com/some-blog will load the content directly from the /some-blog folder on your server.
In this situation, there's probably no code being run before that content is loaded directly from the files.
You might have caching, but the end result is the same.
For statically generated sites, the easiest way to integrate custom domains is likely to target that user's folder URL directly.
With Approximated, you can accomplish this by adding a path to your virtual host target address field.
For example:
Incoming Address:
someblog.com
Target Address:
https://mybloghost.com/some-blog
* Note the https:// in the target address is required when using a path
There are some obstacles to this approach, however:
Additional appended paths may not exist
All paths that are appended to to the custom domain will also be appended to the end of the target, which may cause issues in some cases.
Going to this URL: someblog.com/some-blog/some-post
Will reach this URL on your server: https://mybloghost.com/some-blog/some-post
This is probably what you want, so there's likely no issue here.
Example #2
Incoming Address: someblog.com
Target Address: https://mybloghost.com/some-blog
Going to this URL: someblog.com/assets/app.css
Will reach this URL on your server: https://mybloghost.com/some-blog/assets/app.css
If your application is using shared assets like app.css or app.js for all custom domains,
then your code is probably expecting to find them at:
mybloghost.com/assets/app.css
Instead of at:
mybloghost.com/some-blog/assets.app.css
In that case, this URL will 404 on the custom domain because app.css is not located there.
Solutions:
Symlink folders like assets in each statically generated folder. Typically your server will return requests to anything within as if they were actually there.
Set URLs for things like assets to be absolute in the generated HTML code.
For example:
href="https://mybloghost.com/assets/app.css"
Instead of:
href="/assets/app.css"
Note:
you may run into CORS policy issues with this approach.
See below for more information.
Assets on your primary domain may be restricted by CORS for custom domains
If you make a request to your main app, for instance to get assets, you may get a CORS error if you have a CORS policy restricting other domains.
Example:
Custom domain: someblog.com
Your app domain: mybloghost.com
Linking an asset like this: <link rel="stylesheet" href="https://mybloghost.com/assets/app.css" />
From a custom domain may result in a CORS error if you have a CORS policy restricting other domains from loading your app's content.
Solutions:
Allow all origins in your CORS policy for those URLs by setting it to the wildcard "*".
Note: this could have security implications for your application, please consider how loading this content on other domains might impact you first.
Provide those assets relative to your custom domain as well, either by generating them in each user folder or symlinking.
Use a CDN for assets that will allow your custom domains with CORS.
You won't need to install any additional packages (security team high five!) and the guide covers every aspect you'll need to know about custom domains in a Laravel app - from request to response.
That guide and repo show a working example that demonstrates creating pages that can be tied to a custom domain, the routing required, and a class that interfaces with the Approximated API for you.
Apps using Elixir's Phoenix framework can integrate with Approximated easily, including websockets for liveview.
We've created a comprehensive developers guide for supporting custom domains in Phoenix
to help you get up and running as fast as possible.
We've also created a companion example repo here
for you to explore and run easily.
The example repo is a simple blog hosting platform, where you can create blogs and tie them to a custom domain.
It should serve as a reference for how your Phoenix app can handle routing, liveviews/websockets, security features, and more for custom domains.
Next.js can be used with Approximated, whether you host it with Vercel or anywhere else.
We've created a comprehensive developers guide for supporting custom domains in Next.js
to help you get up and running as fast as possible, which includes examples using both the App and Page routers.
We've also created a companion example repo here
for you to explore and run easily, to get a sense of how Approximated could be used to integrate custom domains into your Next.js app.