Custom Domains
Put your app on your own domain.
A published project answers at <projectId>.totalum-project.com. On the Professional and Enterprise plans it also answers at <projectId>.webapp-project.com — both addresses serve the site — and Update Project { "liveHostSuffix": "webapp-project.com" } makes that one the main address (the one shown as productionProjectUrl and used for links). Add one custom domain per project: a subdomain (app.example.com) or a whole domain (example.com or www.example.com, served as www.example.com; the bare domain redirects to it once it points here, which needs an ALIAS/ANAME record or CNAME flattening at your DNS provider — otherwise add a URL redirect from example.com to https://www.example.com there). You create the DNS records at your DNS provider; SSL is issued automatically. A domain bought on Totalum is attached with its own endpoint and needs no DNS work.
An address without "totalum" in it. For a white-label app, use either option:
webapp-project.com(Professional and Enterprise): setliveHostSuffixto"webapp-project.com", and the main address, links, sitemap and preview use<projectId>.webapp-project.com. Thetotalum-project.comaddress keeps working too, so old links never break; it just isn't shown anywhere.- Your own domain (any paid plan): add a custom domain or buy one on Totalum. It becomes the published address.
Domains are set per project: each project can have its own custom domain and/or use
webapp-project.com. One domain that replaces the address of every project of an account at once is not supported yet.
A custom domain costs 4 credits when added and 3 credits a month while active. Custom domains are included on paid plans.
#Add Custom Domain
/api/v1/vcaas/projects/:projectId/domainapi-key4 credits (first domain)Connect a domain you own. Publish the project first. Then create the records in dnsRecordsToAdd at your DNS provider and poll Get Project until customDomain.status and customDomain.sslStatus are "active" (usually minutes, up to a few hours for DNS). Calling it again with another hostname replaces the domain.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
hostname | string | Yes | app.example.com or example.com |
Response fields
| Field | Type | Description |
|---|---|---|
data.success | boolean | true |
data.hostname | string | The hostname that will serve the site |
data.status | string | "pending" until the DNS records are found, then "active" |
data.dnsRecordsToAdd | object[] | Records to create: { type: "CNAME" | "TXT" | "ALIAS", name, value }; name is the full host name. For a whole domain the bare domain's ALIAS and TXT come last: optional, they make it redirect to www. |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"hostname":"book.barbershop.com"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domainconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domain", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"hostname": "book.barbershop.com"
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"success": true,
"hostname": "book.barbershop.com",
"status": "pending",
"dnsRecordsToAdd": [
{ "type": "CNAME", "name": "book.barbershop.com", "value": "barber-bookings.totalum-project.com" },
{ "type": "TXT", "name": "_cf-custom-hostname.book.barbershop.com", "value": "4f6a2c9e-…" }
]
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_HOSTNAME | 400 | hostname is missing |
VALIDATION_ERROR | 400 | Not a valid domain name, or a Totalum host |
NO_DEPLOYMENT | 404 | Publish the project first |
DOMAIN_ALREADY_ADDED | 409 | This domain is already on the project |
DOMAIN_IN_USE | 409 | The domain is connected to another project |
PROJECT_HAS_CUSTOM_DOMAIN | 409 | The project uses a domain bought on Totalum; detach it first |
PLATFORM_CAPABILITY_QUOTA_REACHED | 403 | Custom domains are not included on your plan |
INSUFFICIENT_CREDITS | 402 | Not enough credits |
#Get Custom Domain
/api/v1/vcaas/projects/:projectId/domainapi-keyFreeThe project's custom domain and the state of each of its addresses — the site and, for a whole domain, the bare domain that redirects to it. Statuses are checked again with Cloudflare while an address is still pending (at most once a minute), so poll this (or Get Project, whose customDomain carries the same fields) until every status and sslStatus is "active". New engine projects only. data is null when the project has no custom domain.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.hostname | string | Where the site answers |
data.status | string | That address: "pending" until its DNS records are found, then "active" |
data.sslStatus | string | Its HTTPS certificate: "active" once issued |
data.domain | string | The domain (barbershop.io) |
data.kind | string | "domain" or "subdomain" |
data.canonicalHost | string | null | "www", "app", any label, or "apex"; null for a subdomain |
data.source | string | "purchase" (bought on Totalum: no DNS work) or "manual" |
data.accountDomainId | string | undefined | The bought domain, when source is "purchase" |
data.hostnames[] | object[] | Each address: { hostname, role, status, sslStatus, dnsRecordsToAdd, verificationErrors }; role is "canonical" (the site), "alias" (redirects to it with a 301, path and query kept) or "sub" |
data.dnsRecordsToAdd | object[] | Every record still to create, as in Add Custom Domain (empty for a bought domain: Totalum writes them) |
data.redirects | object | { canonical, applyToDefaultHost, rules } |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domainconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domain", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "hostname": "app.barbershop.io", "status": "active", "sslStatus": "active", "domain": "barbershop.io", "kind": "domain", "canonicalHost": "app", "source": "purchase", "accountDomainId": "dom_01k6y3", "hostnames": [{ "hostname": "app.barbershop.io", "role": "canonical", "status": "active", "sslStatus": "active", "dnsRecordsToAdd": [], "verificationErrors": [] }, { "hostname": "barbershop.io", "role": "alias", "status": "active", "sslStatus": "active", "dnsRecordsToAdd": [], "verificationErrors": [] }], "dnsRecordsToAdd": [], "redirects": { "canonical": "app.barbershop.io", "applyToDefaultHost": false, "rules": [] } } }#Remove Custom Domain
/api/v1/vcaas/projects/:projectId/domainapi-keyFreeDisconnect the custom domain. The site keeps answering at <projectId>.totalum-project.com. Your DNS records are not touched. A domain bought on Totalum is detached the same way: it stays yours and is not refunded, and it keeps renewing until you switch autoRenew off (Detach Domain).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.success | boolean | true |
data.hostname | string | null | The domain removed |
data.message | string | "Custom domain removed" |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domainconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/domain", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "success": true, "hostname": "book.barbershop.com", "message": "Custom domain removed" } }