Custom domains
Serve Connect from your own domain, verified by DNS.
Each endpoint, main and test, can answer on up to 5 of your own domains.
Keep in mind
- Main and test
- Pick the endpoint with
endpoint:mainby default, ortest. - Pausing keeps the setup
- Disabling a domain with
edit-domainkeeps its certificate and virtual host.
Verifying a domain
- Point an A record for the domain at the panel address.
- Call
create-domain. The first answer isdns_txt_requiredwith the record indetails.nameanddetails.value. - Add that TXT record at
_modkey.<domain>and callcreate-domainagain. - ModKey issues a Let's Encrypt certificate and the domain starts answering Connect.
5 per hour
Create and delete share a quota of 5 operations an hour, failed TXT checks included. Past it they answer 429 with rate_limited and no retry details.
List domains
https://dev.mod.wtf/api/v2/action
methodget-domains
Custom domains for this endpoint (up to 5 on Main and the same number on Test).
Body
-
soft_idinteger requiredID of a software you own
-
endpointstring optionalMain or test, default main
Errors it can return
POST /api/v2/action HTTP/1.1
Host: dev.mod.wtf
Content-Type: application/json
Accept: application/json
{
"api_key": "YOUR_API_KEY",
"method": "get-domains",
"soft_id": 123
}
curl -sS -X POST 'https://dev.mod.wtf/api/v2/action' \
-H 'Content-Type: application/json' \
-d '{
"api_key": "YOUR_API_KEY",
"method": "get-domains",
"soft_id": 123
}'
const response = await fetch("https://dev.mod.wtf/api/v2/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"api_key": "YOUR_API_KEY",
"method": "get-domains",
"soft_id": 123
}),
});
const result = await response.json();
$ch = curl_init('https://dev.mod.wtf/api/v2/action');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'api_key' => 'YOUR_API_KEY',
'method' => 'get-domains',
'soft_id' => 123,
]),
]);
$result = json_decode(curl_exec($ch), true);
import requests
response = requests.post(
"https://dev.mod.wtf/api/v2/action",
json={
"api_key": "YOUR_API_KEY",
"method": "get-domains",
"soft_id": 123,
},
timeout=10,
)
result = response.json()
{
"ok": true,
"data": {
"soft_id": 123,
"endpoint": "main",
"limit": 5,
"used": 2,
"items": [
{
"id": 1,
"soft_id": 123,
"endpoint": "main",
"domain": "keys.example.com",
"active": true,
"provisioned": true,
"created_at": "2025-06-01 12:00:00",
"created_by": "JohnDoe",
"public_url": "https://keys.example.com/connect"
}
]
}
}
{
"ok": false,
"error": "not_found",
"message": "soft not found or not yours"
}
{
"ok": false,
"error": "unauthorized",
"message": "Invalid or missing api_key"
}
Create domain
https://dev.mod.wtf/api/v2/action
methodcreate-domain
Add a custom domain. Point it to the panel IP and add a TXT record at _modkey.<domain>: until it exists the call returns dns_txt_required with the record name and value in details.name and details.value. Then a Let's Encrypt certificate and Apache vhost are provisioned. Up to 5 create or delete operations per hour, failed TXT checks included.
Body
-
soft_idinteger requiredID of a software you own
-
domainstring requiredLowercase domain that already points to the panel IP
-
endpointstring optionalMain or test, default main
Errors it can return
DNS first
Until the TXT record at _modkey.<domain> exists this answers dns_txt_required, and each failed check uses one of the 5 hourly operations.
POST /api/v2/action HTTP/1.1
Host: dev.mod.wtf
Content-Type: application/json
Accept: application/json
{
"api_key": "YOUR_API_KEY",
"method": "create-domain",
"soft_id": 123,
"domain": "keys.example.com",
"endpoint": "main"
}
curl -sS -X POST 'https://dev.mod.wtf/api/v2/action' \
-H 'Content-Type: application/json' \
-d '{
"api_key": "YOUR_API_KEY",
"method": "create-domain",
"soft_id": 123,
"domain": "keys.example.com",
"endpoint": "main"
}'
const response = await fetch("https://dev.mod.wtf/api/v2/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"api_key": "YOUR_API_KEY",
"method": "create-domain",
"soft_id": 123,
"domain": "keys.example.com",
"endpoint": "main"
}),
});
const result = await response.json();
$ch = curl_init('https://dev.mod.wtf/api/v2/action');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'api_key' => 'YOUR_API_KEY',
'method' => 'create-domain',
'soft_id' => 123,
'domain' => 'keys.example.com',
'endpoint' => 'main',
]),
]);
$result = json_decode(curl_exec($ch), true);
import requests
response = requests.post(
"https://dev.mod.wtf/api/v2/action",
json={
"api_key": "YOUR_API_KEY",
"method": "create-domain",
"soft_id": 123,
"domain": "keys.example.com",
"endpoint": "main",
},
timeout=10,
)
result = response.json()
{
"ok": true,
"data": {
"id": 1,
"soft_id": 123,
"endpoint": "main",
"domain": "keys.example.com",
"active": true,
"provisioned": true,
"created_at": "2025-06-01 12:00:00",
"created_by": "JohnDoe",
"public_url": "https://keys.example.com/connect"
}
}
{
"ok": false,
"error": "dns_txt_required",
"message": "domain ownership verification failed",
"details": {
"name": "_modkey.keys.example.com",
"value": "mdk-verify-<token>"
}
}
{
"ok": false,
"error": "unauthorized",
"message": "Invalid or missing api_key"
}
Update domain
https://dev.mod.wtf/api/v2/action
methodedit-domain
Enable or disable a domain. The vhost and certificate are retained in both cases.
Body
-
domain_idinteger requiredDomain ID from get-domains
-
soft_idinteger requiredID of a software you own
-
activeboolean requiredTrue enables the domain, false pauses it
Errors it can return
POST /api/v2/action HTTP/1.1
Host: dev.mod.wtf
Content-Type: application/json
Accept: application/json
{
"api_key": "YOUR_API_KEY",
"method": "edit-domain",
"soft_id": 123,
"domain_id": 1,
"active": false
}
curl -sS -X POST 'https://dev.mod.wtf/api/v2/action' \
-H 'Content-Type: application/json' \
-d '{
"api_key": "YOUR_API_KEY",
"method": "edit-domain",
"soft_id": 123,
"domain_id": 1,
"active": false
}'
const response = await fetch("https://dev.mod.wtf/api/v2/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"api_key": "YOUR_API_KEY",
"method": "edit-domain",
"soft_id": 123,
"domain_id": 1,
"active": false
}),
});
const result = await response.json();
$ch = curl_init('https://dev.mod.wtf/api/v2/action');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'api_key' => 'YOUR_API_KEY',
'method' => 'edit-domain',
'soft_id' => 123,
'domain_id' => 1,
'active' => false,
]),
]);
$result = json_decode(curl_exec($ch), true);
import requests
response = requests.post(
"https://dev.mod.wtf/api/v2/action",
json={
"api_key": "YOUR_API_KEY",
"method": "edit-domain",
"soft_id": 123,
"domain_id": 1,
"active": False,
},
timeout=10,
)
result = response.json()
{
"ok": true,
"data": {
"id": 1,
"soft_id": 123,
"domain": "keys.example.com",
"active": false,
"provisioned": true,
"created_at": "2025-06-01 12:00:00",
"created_by": "JohnDoe",
"public_url": "https://keys.example.com/connect"
}
}
{
"ok": false,
"error": "not_found",
"message": "domain not found"
}
{
"ok": false,
"error": "unauthorized",
"message": "Invalid or missing api_key"
}
Delete domain
https://dev.mod.wtf/api/v2/action
methoddelete-domain
Delete a domain. The Apache vhost is removed; the certificate remains on disk. Shares the 5 per hour quota with domain creation.
Body
-
domain_idinteger requiredDomain ID from get-domains
-
soft_idinteger requiredID of a software you own
Errors it can return
POST /api/v2/action HTTP/1.1
Host: dev.mod.wtf
Content-Type: application/json
Accept: application/json
{
"api_key": "YOUR_API_KEY",
"method": "delete-domain",
"soft_id": 123,
"domain_id": 1
}
curl -sS -X POST 'https://dev.mod.wtf/api/v2/action' \
-H 'Content-Type: application/json' \
-d '{
"api_key": "YOUR_API_KEY",
"method": "delete-domain",
"soft_id": 123,
"domain_id": 1
}'
const response = await fetch("https://dev.mod.wtf/api/v2/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"api_key": "YOUR_API_KEY",
"method": "delete-domain",
"soft_id": 123,
"domain_id": 1
}),
});
const result = await response.json();
$ch = curl_init('https://dev.mod.wtf/api/v2/action');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'api_key' => 'YOUR_API_KEY',
'method' => 'delete-domain',
'soft_id' => 123,
'domain_id' => 1,
]),
]);
$result = json_decode(curl_exec($ch), true);
import requests
response = requests.post(
"https://dev.mod.wtf/api/v2/action",
json={
"api_key": "YOUR_API_KEY",
"method": "delete-domain",
"soft_id": 123,
"domain_id": 1,
},
timeout=10,
)
result = response.json()
{
"ok": true,
"data": {
"deleted": true,
"domain": "keys.example.com"
}
}
{
"ok": false,
"error": "not_found",
"message": "domain not found"
}
{
"ok": false,
"error": "unauthorized",
"message": "Invalid or missing api_key"
}