Guides and API documentation to get you up and running.
Whether you've got technical questions or just want to learn more, we're happy to dig into it with you. Get answers from a real, human engineer over email or on a call.
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.
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 four endpoints to create, read, update, and delete a virtual host.
Note:
Make sure that you include headers for
Content-Type and Accept set to 'application/json' for all requests to 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.
// Example response: { "data": { "id": 445922, "incoming_address": "acustomdomain.com", "target_address": "myapp.com", "target_ports": "443", "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." } }
// Example response { "errors": { "incoming_address": [ "This incoming address has already been created on the cluster you selected." ] } }
The custom domain that you'd like to route.
The address that you'd like requests for the custom domain to be routed to. Typically another domain or domain with a sub-page.
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.
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.
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.
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.
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.
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" }, ... ], after_cursor: "a39fdk32kf", // will be null if there is no next page before_cursor: "lf3jeuc3406" // will be null if there is no previous page }
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.
{"incoming_address": "acustomdomain.com"}
// 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" } }
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" } }
// Example response { "errors": { "incoming_address": [ "This incoming address has already been created on the cluster you selected." ] } }
The custom domain for an existing Virtual Host.
A new custom domain you would like to change the existing Virtual Host to.
The address that you'd like requests for the custom domain to be routed to. Typically another domain or domain with a sub-page.
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.
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.
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.
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.
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.
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.
{"incoming_address": "acustomdomain.com"}
Deleting customdomain.com
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:
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.
{ "records": [ { "actual_values": [ "93.184.216.34" ], "address": "example.com", "match": false, "match_against": "12.345.678.90", "type": "a" } ] }
A list of DNS records you'd like to check.
{ "records": [ { "address": "example.com", "match_against": "12.345.678.90", "type": "a" } ] }
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.
The DNS record type you'd like to check, formatted as a lowercase 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.
{ "records": [ { "actual_values": [ "93.184.216.34" ], "address": "example.com", "match": false, "match_against": "12.345.678.90", "type": "a" } ] }
A list of DNS records you'd like to check.
{ "records": [ { "address": "example.com", "match_against": "12.345.678.90", "type": "a" } ] }
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.
The DNS record type you'd like to check, formatted as a lowercase 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.
You can see a working example of the widget with default styling by clicking the button below:
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.
// 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);
});
// 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"
}
]
});
// 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:
.apxdnswidget {
--text-color: #333;
--light-text-color: #666;
--link-text-color: rgb(46, 90, 173);
--main-bg-color: #FFF;
--shaded-bg-color: #FAFAFA;
--shaded-border: 1px solid #EEE;
--button-bg-color: #333;
--button-text-color: #FFF;
--radius: 5px;
--widget-max-width: 700px;
--border-color: #EEE;
}
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.
{ "token": "a-valid-token" }
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.
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.
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.
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.
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:
someblog.com
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:
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.
incoming_address:
someblog.comtarget_address:
https://mybloghost.com/some-blog
someblog.com/some-blog/some-post
https://mybloghost.com/some-blog/some-post
someblog.com
https://mybloghost.com/some-blog
someblog.com/assets/app.css
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
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.
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.
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.
someblog.com
mybloghost.com
<link rel="stylesheet" href="https://mybloghost.com/assets/app.css" />
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.
Laravel is one of the easiest frameworks to integrate with Approximated. We've created an entire guide for you, and a Laravel custom domains example repo to make things even easier.
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.
Ruby on Rails makes integrating with Approximated easy. The best way to learn how to integrate Approximated with your Rails app is to look at our Ruby on Rails custom domains guide and it's complimentary github example repo.
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.