Skip to content

Domains ​

Methods for domains: attach a domain or subdomain to a PWA or split, turn its DNS off and on, recreate DNS records and delete a domain. For how domains work in the panel, see the "PWA and Split Domains" section of the guide.

MethodAddressWhat it does
GET/domainsRoot domains available to the team
POST/domainsAttach a domain to a PWA or split
POST/domains/{id}/toggle-dnsTurn the domain's DNS off or on
POST/domains/{id}/recreate-dnsRecreate DNS records
DELETE/domains/{id}Delete a domain

Replace YOUR_KEY and panel.example.com with your own.

This is the real Cloudflare

Every method except GET changes the Cloudflare account right away: creates a zone, adds and deletes DNS records. This can't be undone through the API - check the domain and the PWA before the request, especially in loops.

Root domains ​

GET/domains

Second-level domains (example.com) on which you can create subdomains: domains of the PWA's or split's team and shared domains without a team. Subdomains are not in this list. Domains already attached to a specific PWA are in the domains field of GET/pwas/{id}.

ParameterRequiredWhat it is
pwa_id or pwa_split_idyes, one of the twoPWA or split - domains are picked by its team
bash
curl "https://panel.example.com/api/domains?pwa_id=WZAX" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
    'timeout' => 30,
]);

$response = $api->get('domains', ['query' => ['pwa_id' => 'WZAX']]);
$domains = json_decode($response->getBody(), true)['data']['domains'];

foreach ($domains as $domain) {
    echo "{$domain['id']} {$domain['domain']}" . PHP_EOL;
}
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.get(f"{API}/domains", headers=HEADERS, params={"pwa_id": "WZAX"}, timeout=30)
domains = response.json()["data"]["domains"]

for domain in domains:
    print(domain["id"], domain["domain"])
js
// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };

const response = await fetch(`${API}/domains?pwa_id=WZAX`, { headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();

for (const domain of data.domains) {
  console.log(domain.id, domain.domain);
}
Response
json
{
  "data": {
    "domains": [
      {
        "id": "Xk9P",
        "domain": "fit-tracker.example",
        "domain_level": 2,
        "dns_on": true,
        "team": { "id": "WQby", "name": "Team A" },
        "created_at": "05 Oct 2026 14:23",
        "updated_at": "05 Oct 2026 14:23"
      }
    ]
  }
}

The list is not paginated - it comes in full. dns_on is whether the domain's DNS is on in Cloudflare.

Errors
CodemessageWhen
422The pwa id field is required when pwa split id is not present.Neither pwa_id nor pwa_split_id passed, or the id doesn't exist
403This action is unauthorized.A PWA or split of another team

Attaching a domain ​

POST/domains

Adds the domain to Cloudflare and attaches it to a PWA or split.

FieldRequiredWhat it is
domainyesDomain or subdomain, e.g. fit-tracker.example or go.fit-tracker.example
pwa_id or pwa_split_idyes, one of the twoWhere to attach it
cloudflare_idnoCloudflare account for the zone. Without it the main (Primary) account is used

The domain's team is taken from the PWA or split; team_id is not needed in the request and is ignored. You can attach a domain only to a PWA or split you have access to.

bash
curl -X POST https://panel.example.com/api/domains \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"domain": "fit-tracker.example", "pwa_id": "WZAX", "cloudflare_id": "Lm3T"}'
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
    'timeout' => 30,
]);

$response = $api->post('domains', ['json' => [
    'domain' => 'fit-tracker.example',
    'pwa_id' => 'WZAX',
    'cloudflare_id' => 'Lm3T',
]]);
$result = json_decode($response->getBody(), true)['data'];

// NS servers to set at the registrar
echo implode(PHP_EOL, $result['zone']['name_servers']) . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.post(f"{API}/domains", headers=HEADERS, json={
    "domain": "fit-tracker.example",
    "pwa_id": "WZAX",
    "cloudflare_id": "Lm3T",
}, timeout=30)
result = response.json()["data"]

# NS servers to set at the registrar
print("\n".join(result["zone"]["name_servers"]))
js
// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };

const response = await fetch(`${API}/domains`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    domain: 'fit-tracker.example',
    pwa_id: 'WZAX',
    cloudflare_id: 'Lm3T',
  }),
  signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();

// NS servers to set at the registrar
console.log(data.zone.name_servers.join('\n'));

The response is the domain and the Cloudflare zone it went into:

Response
json
{
  "data": {
    "domain": {
      "id": "Xk9P",
      "domain": "fit-tracker.example",
      "domain_level": 2,
      "dns_on": true,
      "team": { "id": "WQby", "name": "Team A" }
    },
    "zone": {
      "id": "023e105f4ecef8ad9ca31a8372d0c353",
      "name": "fit-tracker.example",
      "status": "pending",
      "name_servers": ["ada.ns.cloudflare.com", "bob.ns.cloudflare.com"]
    }
  }
}

What happens in Cloudflare:

  • A new second-level domain - a zone is created with records that point the domain to the panel's server. Until the NS from zone.name_servers are set at the registrar, the zone is pending and the domain doesn't open. NS are set at the registrar - see the "Domains and Cloudflare" section.
  • A subdomain (go.fit-tracker.example) - a record for the subdomain is added to the root domain's zone. The root domain must already be in the panel, otherwise you get 404.

The same account as the PWA

Pass the cloudflare_id of the same account as the PWA's (the cloudflare_id field in GET/pwas/{id}). Without it the zone is created in the main account, and the domain may end up in a different account than the app.

A shared root domain (without a team) becomes bound to the PWA's or split's team once a subdomain is added on it.

A repeated POST with an already added domain and a different pwa_id re-attaches the domain to the new PWA. A domain of another team can't be taken this way - you get 403.

Domain limit

The number of domains is limited by the license - all domains and subdomains of the panel count. When the limit is used up you get 403 with the text Limit reached: max_domains (50) - on any POST/domains, even on re-attaching an already added domain.

Errors
CodemessageWhen
422The domain field is required.domain not passed
422The domain field format is invalid.domain is not a domain name
422The pwa id field is required when pwa split id is not present.Neither pwa_id nor pwa_split_id passed, or the id doesn't exist
422You do not have access to this resourceA PWA or split you have no access to; team_id then also gets You do not have access to this team
422The cloudflare id field must be an integer.cloudflare_id doesn't exist
403This action is unauthorized.The domain was already added by another team
403Limit reached: max_domains (50)The license domain limit is used up
404Not foundA subdomain whose root domain is not in the panel

Turning DNS off and on ​

POST/domains/{id}/toggle-dns

If the domain's DNS is on, deletes its records in Cloudflare (the domain stops opening but stays attached); if it is off - creates them again.

bash
curl -X POST https://panel.example.com/api/domains/Xk9P/toggle-dns \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
    'timeout' => 30,
]);

$response = $api->post('domains/Xk9P/toggle-dns');
$domain = json_decode($response->getBody(), true)['data']['domain'];

echo ($domain['dns_on'] ? 'DNS on' : 'DNS off') . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.post(f"{API}/domains/Xk9P/toggle-dns", headers=HEADERS, timeout=30)
domain = response.json()["data"]["domain"]

print("DNS on" if domain["dns_on"] else "DNS off")
js
// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };

const response = await fetch(`${API}/domains/Xk9P/toggle-dns`, { method: 'POST', headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();

console.log(data.domain.dns_on ? 'DNS on' : 'DNS off');

The response is the domain in data.domain, the new state in dns_on.

Errors
CodemessageWhen
404Not foundNo domain with this id
403This action is unauthorized.A domain of another team

Recreating DNS ​

POST/domains/{id}/recreate-dns

Brings the domain's records in Cloudflare to the right state: creates missing ones, fixes records with a wrong server address, creates the zone if needed. Calling it again breaks nothing. Useful if the records were deleted or changed manually in Cloudflare.

bash
curl -X POST https://panel.example.com/api/domains/Xk9P/recreate-dns \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
    'timeout' => 30,
]);

$response = $api->post('domains/Xk9P/recreate-dns');
$domain = json_decode($response->getBody(), true)['data']['domain'];

echo "DNS of {$domain['domain']} recreated" . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.post(f"{API}/domains/Xk9P/recreate-dns", headers=HEADERS, timeout=30)
domain = response.json()["data"]["domain"]

print("DNS of", domain["domain"], "recreated")
js
// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };

const response = await fetch(`${API}/domains/Xk9P/recreate-dns`, { method: 'POST', headers, signal: AbortSignal.timeout(30000) });
const { data } = await response.json();

console.log('DNS of', data.domain.domain, 'recreated');

The response is the domain in data.domain with dns_on: true.

Errors
CodemessageWhen
404Not foundNo domain with this id
403This action is unauthorized.A domain of another team

Deleting a domain ​

DELETE/domains/{id}

Deletes the domain's DNS records in Cloudflare and the domain itself from the panel. The zone in Cloudflare stays. Subdomains of a deleted root domain stay attached - delete them separately.

bash
curl -X DELETE https://panel.example.com/api/domains/Xk9P \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Accept: application/json"
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
    'timeout' => 30,
]);

$response = $api->delete('domains/Xk9P');

echo $response->getStatusCode() === 200 ? 'Domain deleted' . PHP_EOL : 'Error' . PHP_EOL;
python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}

response = requests.delete(f"{API}/domains/Xk9P", headers=HEADERS, timeout=30)

print("Domain deleted" if response.ok else "Error")
js
// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };

const response = await fetch(`${API}/domains/Xk9P`, { method: 'DELETE', headers, signal: AbortSignal.timeout(30000) });

console.log(response.ok ? 'Domain deleted' : 'Error');

Response: {"data": true}.

Errors
CodemessageWhen
404Not foundNo domain with this id
403This action is unauthorized.A domain of another team