Subdomain & Domain API
Kuploy provides free *.kuploy.app subdomains for your applications, custom domain registration, and domain purchasing. This API allows kuploy-cloud instances to provision and manage subdomains, custom domains, and purchased domains programmatically.
This documentation is for tenant operators integrating subdomain provisioning into their kuploy-cloud instances. End users typically interact with subdomains through the application UI, not directly via this API.
Overview
The subdomain API enables:
- Checking subdomain availability
- Reserving subdomains for instances
- Provisioning DNS records
- Releasing subdomains when no longer needed
The custom domain API enables:
- Registering custom domains against your license
- Removing custom domain registrations
- Listing custom domains for an instance
The purchased domain API enables:
- Registering domains purchased through a registrar
- Removing purchased domain registrations
- Listing purchased domains for an instance
All endpoints require authentication via your license key.
Configuration
Your kuploy-cloud instance should have these environment variables configured:
| Variable | Description |
|---|---|
LICENSE_HUB_URL | Base URL of the kuploy-app instance (e.g., https://kuploy.app) |
LICENSE_KEY | Your license key from kuploy-app |
Base URL
The subdomain API is available at:
{LICENSE_HUB_URL}/api/subdomain
For example: https://kuploy.app/api/subdomain
Authentication
All requests must include your LICENSE_KEY as the licenseKey in the request body:
{
"licenseKey": "{LICENSE_KEY}",
...
}
Endpoints
Check Availability
Check if a subdomain name is available.
POST /api/subdomain/check
Request:
{
"name": "my-app"
}
Response (available):
{
"available": true,
"name": "my-app",
"fqdn": "my-app.kuploy.app"
}
Response (unavailable):
{
"available": false,
"name": "my-app",
"fqdn": "my-app.kuploy.app",
"error": "This subdomain is already taken"
}
Reserve Subdomain
Reserve a subdomain for an instance. This does not create DNS records yet.
POST /api/subdomain/reserve
Request:
{
"name": "my-app",
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"subdomain": {
"id": "sub_xxxxxxxxxxxxxxxx",
"name": "my-app",
"fqdn": "my-app.kuploy.app",
"status": "pending"
}
}
Error (limit reached):
{
"success": false,
"error": "Subdomain limit reached (5). Upgrade your plan for more subdomains."
}
Provision DNS Record
Create the actual DNS record for a reserved subdomain.
POST /api/subdomain/provision
Request:
{
"subdomainId": "sub_xxxxxxxxxxxxxxxx",
"ipAddress": "203.0.113.50",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"subdomain": {
"id": "sub_xxxxxxxxxxxxxxxx",
"name": "my-app",
"fqdn": "my-app.kuploy.app",
"ipAddress": "203.0.113.50",
"status": "active"
}
}
DNS propagation is typically instant since Kuploy manages the authoritative DNS for kuploy.app.
Release Subdomain
Release a subdomain and delete its DNS record.
POST /api/subdomain/release
Request:
{
"subdomainId": "sub_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"message": "Subdomain my-app.kuploy.app has been released"
}
List Subdomains
List all subdomains for an instance.
POST /api/subdomain/list
Request:
{
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"subdomains": [
{
"id": "sub_xxxxxxxxxxxxxxxx",
"name": "my-app",
"fqdn": "my-app.kuploy.app",
"ipAddress": "203.0.113.50",
"status": "active",
"provisionedAt": "2025-01-10T12:00:00Z",
"createdAt": "2025-01-10T11:55:00Z"
}
]
}
Subdomain Status
| Status | Description |
|---|---|
pending | Reserved but DNS not yet provisioned |
active | DNS record created and live |
suspended | Temporarily disabled |
released | Previously used, now available |
Subdomain Naming Rules
Subdomains must follow these rules:
- Length: 3-63 characters
- Characters: Lowercase letters, numbers, and hyphens only
- Format: Cannot start or end with a hyphen
- Uniqueness: Must be unique across all Kuploy users
Reserved Names
The following subdomain names are reserved and cannot be used:
www, app, api, admin, mail, smtp, pop, imap, ftp, ssh,
ns1, ns2, dns, test, dev, staging, prod, production,
beta, alpha, status, help, support, docs, blog, cdn,
static, assets, media, images, img, dashboard, billing,
account, login, signup, register, auth, oauth, sso, kuploy
Error Codes
| HTTP Status | Error | Description |
|---|---|---|
| 400 | INVALID_SUBDOMAIN | Name doesn't meet naming rules |
| 400 | INVALID_IP | Invalid IPv4 address format |
| 403 | LIMIT_REACHED | Subdomain limit for plan exceeded |
| 404 | NOT_FOUND | Subdomain or instance not found |
| 409 | ALREADY_TAKEN | Subdomain already reserved by another user |
Integration Example
Here's a typical flow for provisioning a subdomain:
const LICENSE_HUB_URL = process.env.LICENSE_HUB_URL;
const LICENSE_KEY = process.env.LICENSE_KEY;
// 1. Check availability
const check = await fetch(`${LICENSE_HUB_URL}/api/subdomain/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'my-app' })
});
const { available } = await check.json();
if (!available) {
throw new Error('Subdomain not available');
}
// 2. Reserve the subdomain
const reserve = await fetch(`${LICENSE_HUB_URL}/api/subdomain/reserve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'my-app',
instanceId: 'inst_xxx',
licenseKey: LICENSE_KEY
})
});
const { subdomain } = await reserve.json();
// 3. Provision DNS (once you know the IP)
const provision = await fetch(`${LICENSE_HUB_URL}/api/subdomain/provision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subdomainId: subdomain.id,
ipAddress: '203.0.113.50',
licenseKey: LICENSE_KEY
})
});
// Subdomain is now live at my-app.kuploy.app
Rate Limits
API requests are rate-limited per license:
| Operation | Limit |
|---|---|
| Check availability | 60/minute |
| Reserve/Provision/Release | 10/minute |
| List subdomains | 30/minute |
Exceeding limits returns HTTP 429 with a Retry-After header.
Custom Domain API
Custom domains allow your users to attach their own domains (e.g., app.example.com) to applications. These endpoints register and manage custom domains with the license hub.
Custom domains require a Starter plan or higher. Requests from the Hobby plan will return a 403 error.
Base URL
{LICENSE_HUB_URL}/api/custom-domain
Register Custom Domain
Register a custom domain for an instance. Enforces plan feature flags and domain limits server-side.
POST /api/custom-domain/register
Request:
{
"host": "app.example.com",
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response (success):
{
"success": true,
"customDomain": {
"id": "cd_xxxxxxxxxxxxxxxx",
"host": "app.example.com",
"status": "active"
}
}
Error (plan doesn't support custom domains):
{
"error": "Custom domains are not available on your plan. Upgrade to Starter or higher."
}
Error (limit reached):
{
"error": "Custom domain limit reached (10). Upgrade your plan for more custom domains."
}
Remove Custom Domain
Remove a custom domain registration and free up a domain slot.
POST /api/custom-domain/remove
Request:
{
"customDomainId": "cd_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"message": "Custom domain app.example.com has been removed"
}
List Custom Domains
List all custom domains registered for an instance.
POST /api/custom-domain/list
Request:
{
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"customDomains": [
{
"id": "cd_xxxxxxxxxxxxxxxx",
"host": "app.example.com",
"status": "active",
"createdAt": "2025-01-10T12:00:00Z"
}
]
}
Custom Domain Error Codes
| HTTP Status | Error | Description |
|---|---|---|
| 400 | INVALID_HOST | Invalid domain format |
| 403 | FEATURE_DISABLED | Plan doesn't include custom domains |
| 403 | LIMIT_REACHED | Custom domain limit for plan exceeded |
| 403 | LICENSE_SUSPENDED | License is suspended |
Purchased Domain API
Purchased domains are domains bought through the platform's registrar integration (e.g., Namecheap). These endpoints track purchased domain registrations with the license hub for plan limit enforcement.
Domain purchasing requires the domainPurchase feature flag to be enabled — either via your platform license (e.g., Enterprise) or the user's subscription plan (e.g., Pro/Business). Requests without this feature return a 403 error. The kuploy-cloud instance uses a license-first, plan-fallback check before calling this API. See Domain Administration — Access Control for details.
Base URL
{LICENSE_HUB_URL}/api/purchased-domain
Register Purchased Domain
Register a domain purchased through the registrar. Enforces the domainPurchase feature flag and purchasableDomains limit server-side. Idempotent — re-registering the same host for the same instance returns the existing record.
POST /api/purchased-domain/register
Request:
{
"host": "example.com",
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx",
"registrarId": "12345",
"expiresAt": "2027-01-15T00:00:00Z",
"autoRenew": true
}
| Field | Required | Description |
|---|---|---|
host | Yes | The purchased domain name |
instanceId | Yes | Instance that owns this domain |
licenseKey | Yes | License key for authorization |
registrarId | No | Domain ID from the registrar |
expiresAt | No | Domain expiration date (ISO 8601) |
autoRenew | No | Whether auto-renewal is enabled (default: true) |
Response (success):
{
"success": true,
"purchasedDomain": {
"id": "pd_xxxxxxxxxxxxxxxx",
"host": "example.com",
"status": "active"
}
}
Error (feature not available):
{
"success": false,
"error": "Domain purchasing is not available on your plan. Upgrade to a plan with domain purchase enabled."
}
Error (limit reached):
{
"success": false,
"error": "Purchased domain limit reached (10). Upgrade your plan for more purchasable domains."
}
Remove Purchased Domain
Remove a purchased domain registration and free up a domain slot. This does not cancel the domain at the registrar — it only removes the license hub tracking.
POST /api/purchased-domain/remove
Request:
{
"purchasedDomainId": "pd_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"message": "Purchased domain has been removed"
}
List Purchased Domains
List all active purchased domains for an instance.
POST /api/purchased-domain/list
Request:
{
"instanceId": "inst_xxxxxxxxxxxxxxxx",
"licenseKey": "lic_xxxxxxxxxxxxxxxx"
}
Response:
{
"success": true,
"purchasedDomains": [
{
"id": "pd_xxxxxxxxxxxxxxxx",
"host": "example.com",
"status": "active",
"registrarId": "12345",
"expiresAt": "2027-01-15T00:00:00Z",
"autoRenew": true,
"createdAt": "2026-01-15T12:00:00Z"
}
]
}
Purchased Domain Error Codes
| HTTP Status | Error | Description |
|---|---|---|
| 400 | MISSING_FIELDS | Required fields missing from request |
| 403 | FEATURE_DISABLED | Plan doesn't include domain purchasing |
| 403 | LIMIT_REACHED | Purchased domain limit for plan exceeded |
| 403 | LICENSE_SUSPENDED | License is suspended |
| 404 | NOT_FOUND | Domain or instance not found |