WMD SED - Custom Tools Developer Guide
System reference for building tools with AI assistanceWhat is Watch My Domains SED?
Watch My Domains Server Edition (WMD SED) is a self-hosted domain portfolio management platform. It provides organizations with a centralized system for tracking, monitoring, and governing their domain name assets across their full lifecycle.
Core capabilities include:
- Domain inventory - a centralized, searchable database of all domains, with support for custom data columns, categories, and bulk operations
- Expiry and renewal tracking - automated monitoring of registration and SSL certificate expiry dates, with configurable alerts
- WHOIS and RDAP lookups - scheduled and on-demand retrieval of registry and registrar data, with historical change tracking
- DNS monitoring - continuous visibility into DNS configuration, nameserver delegation, and record changes
- SSL and HTTP checks - certificate validity, website availability, and HTTP response monitoring
- Registrar integration - direct API connections to registrars for portfolio import and synchronization
- Multi-user access control - role-based permissions with category-level and column-level access rights
- Authentication - native username/password login with optional two-factor authentication (2FA) and SAML/SSO integration for enterprise identity providers
- Web page monitoring - periodic HTTP checks that capture screenshots, detect changes in page content, and analyze cookies set by monitored domains
- Reporting and alerts - scheduled CSV reports and email notifications for expiry events and data changes
WMD SED is designed to support DNRAM practices - treating domain names as managed resources that require the same visibility and governance as other critical infrastructure. More information on DNRAM is available at softnik.com/dnram.
WMD SED runs as a PHP web application on your own server and exposes a comprehensive API that gives programmatic access to all of its functionality. This guide covers how to build custom tools against that API.
What the API Can Do
The WMD SED API gives programmatic access to virtually everything the application can do. This includes:
- Domain data - read, write, import and delete domain records; bulk edit columns across multiple domains
- Lookups - queue and trigger domain WHOIS, DNS, SSL, HTTP, IP WHOIS, ping and subdomain lookups; monitor queue status
- Categories and queries - create, edit and delete categories and auto-queries; manage domain membership
- Custom columns - add, configure and delete custom data columns; set column-level access rights
- Users - create, edit and delete users; configure access rights per user and per column
- Reports - create and schedule CSV reports; trigger and configure email reports
- DNS monitoring - enable and configure DNS change monitoring and alerts
- Subdomains - retrieve subdomain and DNS record data
- Web screenshots - take and retrieve web page screenshots (if the feature is enabled on the server)
- WHOIS and RDAP - configure WHOIS and RDAP settings per TLD
- Email - configure email settings and templates
- Authentication and security - configure SAML/SSO, two-factor authentication (2FA) and session timeout settings
- Branding - change the application logo and branding
- Logs - access audit logs and system logs
The full API reference is at learn.domainpunch.com/wmdsed/api/. This guide covers the subset needed to build tools - authentication, session handling, deployment models, and the core domain data endpoints.
Architecture
Watch My Domains SED is a PHP web application that exposes all its functionality through a single API endpoint - api.php. Custom tools are clients of this API. They can be built in any language or stack - PHP, Python, plain HTML and JavaScript, Node.js, or anything else that can make HTTP requests and parse JSON. They can be embedded pages living inside the WMD installation folder, separate applications on the same server, or entirely external tools running on a different domain.
The API is stateful and session-based. All responses are JSON. A successful response always contains "status": "ok". Every call must be authenticated - either via a shared browser session or via an explicit login step.
Full official API reference: learn.domainpunch.com/wmdsed/api/
Deployment Models
There are three ways to build and deploy a tool against the WMD API. The right choice depends on where the tool is hosted and how it handles authentication.
1. Embedded tool (simplest)
The tool is a .php or .html file placed inside the WMD installation folder. The recommended location is a user-tools/ folder in the installation root:
wmdsed/user-tools/my-tool.php wmdsed/user-tools/expiring-domains.php wmdsed/user-tools/ai-built/my-tool.php
Placing user tools in user-tools/ keeps them clearly separate from the app's own tools/ folder, which contains pre-built tools distributed with WMD along with their associated JS and CSS files. Putting user files directly inside tools/ risks filename clashes and files being overwritten during app updates.
Because the tool is served from the same origin as WMD, it automatically inherits the active session cookie. No login step is needed - if the user is already logged in to WMD, the tool is authenticated. All API calls use credentials: 'same-origin' and the API URL is resolved from window.location at runtime (see Startup Context).
This is the lowest-friction option and the recommended starting point for most tools.
../api.php. Instead, walk up window.location.pathname one segment at a time, probing each candidate with api.php?sw=0. The first path that returns validate === 0 is the WMD root. This probe also serves as session validation - no separate check is needed.
async function findApiUrl() { const parts = window.location.pathname.split('/').filter(Boolean); for (let i = parts.length; i >= 0; i--) { const candidate = window.location.origin + (i > 0 ? '/' + parts.slice(0, i).join('/') : '') + '/api.php'; try { const r = await fetch(candidate + '?sw=0', { credentials: 'same-origin' }); const d = await r.json(); if (d && d.validate === 0) return candidate; } catch (_) {} } return null; // not found or session expired }
2. Same-origin standalone tool
The tool is a separate application on the same server and domain as WMD, but outside the WMD installation folder - for example at https://example.com/my-app/ while WMD lives at https://example.com/wmdsed/.
Whether the WMD session cookie is accessible depends on how WMD sets its cookie path. If the cookie is scoped to /wmdsed/, it will not be visible to pages at /my-app/. If WMD sets it at the root path /, it will be shared. This is determined by WMD's server configuration, not by the tool.
If the cookie is available, the tool can call api.php at its known absolute URL using credentials: 'same-origin', with no login step. If the cookie is not available, the tool must implement the full login flow described in model 3. The API URL is known and fixed in this model - no path-walking probe is needed.
3. Cross-origin / external tool
The tool runs on a different domain or server entirely. It has no access to the WMD session cookie and must authenticate explicitly using c=auth, then manage the session ID for all subsequent requests.
The login flow is:
- Call
api.php?c=auth&user=username&pass=password - Capture the session ID from the response - see the official session ID guide
- Include the session ID in every subsequent API request
- Call
api.php?c=auth&logout=1to end the session when done
Because credentials and session IDs are handled in application code, this pattern is best suited to server-side tools (PHP, Python, Node, etc.) rather than browser JavaScript, where credentials would be exposed in client-side code.
CORS
WMD does not set any CORS headers itself - it is a same-origin application by design and has no built-in CORS configuration. Browser-based JavaScript running on a different origin will have its requests blocked by the browser before they reach the API.
If you need cross-origin browser access to the WMD API, CORS headers must be added at the web server level, outside of WMD. The two most common setups:
Nginx
Add the following inside the location block that serves your WMD installation, or scope it specifically to api.php:
location /wmdsed/api.php {
add_header Access-Control-Allow-Origin "https://your-tool-domain.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type";
add_header Access-Control-Allow-Credentials "true";
if ($request_method = OPTIONS) {
return 204;
}
}
Apache
Add the following to your .htaccess in the WMD installation folder, or to the relevant <Directory> block in your virtual host config:
Header set Access-Control-Allow-Origin "https://your-tool-domain.com"
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type"
Header set Access-Control-Allow-Credentials "true"
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^ - [R=204,L]
Access-Control-Allow-Origin: * - browsers block credentialed requests (cookies) when the origin is a wildcard. You must specify the exact origin of your tool. Also note that CORS headers only control browser behaviour - they do not add any server-side authentication or access control. The WMD session still needs to be valid for every request.
For most external tool scenarios, a server-side proxy is simpler and safer than configuring CORS - the proxy authenticates with WMD using the session ID pattern, and the browser only ever talks to the proxy on the same origin as the tool.
Startup Context
When a tool page loads, the following information must be available to the tool's JavaScript before any API calls are made. How this is obtained is an implementation detail - what matters is that the tool has these values:
| Value | Type | Description |
|---|---|---|
| Base URL | string | Absolute base URL of the WMD installation, e.g. https://example.com/wmdsed/. Resolved at runtime - see Tool Placement above. |
| API URL | string | Full URL to the API endpoint, always <base_url>api.php. Resolved at runtime - see Tool Placement above. |
| Is Admin | bool | Whether the currently logged-in user has administrator rights. |
| Current Username | string | The logged-in user's name. |
Session & Authentication
WMD uses PHP session-based authentication. How a tool authenticates depends on its deployment model - see Deployment Models above. For embedded and same-origin tools that have access to the session cookie, no explicit login step is needed. For external tools, authentication must be handled explicitly via c=auth.
fetch() call must include a credentials option. Use credentials: 'same-origin' for embedded and same-origin tools. Use credentials: 'include' only for genuine cross-origin requests where CORS headers are configured on the server. Without the correct setting the browser will not send the session cookie and the API will return a session error.
The validate field
Every API response includes a validate field. Your tool should check this on every response - not just at startup. The possible values are:
| Value | Constant | Meaning |
|---|---|---|
0 | VALID | Session is valid - continue normally |
1 | TIMEDOUT | Session timed out - redirect to login |
-1 | INVALID | Session is invalid - redirect to login |
-2 | UNKNOWN | Session state unknown - redirect to login |
-3 | LIMITED | Access limited - user has no access to this endpoint, or an endpoint-specific timeout has been reached. Handle gracefully rather than redirecting to login. |
Because validate is present on every response, session monitoring comes for free from your normal API calls. There is no need for a separate polling mechanism - just check validate whenever you process a response and redirect if it is non-zero.
function handleResponse(data) { if (!data) return; if (data.validate !== 0) { // session timed out or logged out - redirect to login window.location.href = '../index.php'; return; } // process data.rows, data.status etc. normally }
Startup probe with ?sw=0
Before rendering any UI, you need to confirm the session is active. If your tool has no other API call to make at startup, use api.php?sw=0 - it is a minimal no-op call whose only purpose is to return a response containing validate.
const res = await fetch(API_URL + '?sw=0', { credentials: 'same-origin' }); const data = await res.json(); if (!data || data.validate !== 0) { // session not valid - redirect to login return; }
If your tool makes a real API call at startup (e.g. loading initial data), you can skip ?sw=0 entirely and just check validate on that first response instead. For embedded tools, the API URL discovery probe described in Deployment Models already serves as the startup session check.
Users & Rights
WMD has two user types: admin and regular user. The distinction affects both what the tool should show and what the API will permit.
| Capability | Admin | Regular User |
|---|---|---|
| Read domain data | All columns | Only permitted columns - enforced automatically by the API |
| Write domain data | Yes | Yes, within permitted columns |
Write config values (c=admin) | Yes | No - API returns a permission error |
| Delete categories / auto-queries | Yes | No |
| Add or edit categories / auto-queries | Yes | No |
| Access all categories | Yes | Only categories explicitly assigned to their account |
The API enforces column-level and category-level access automatically. A regular user requesting a restricted column via c=grid will simply not receive that column in the response - no error is raised. The tool does not need to filter the response; it can trust the API output.
Checking admin status
The admin flag is a boolean available at startup (see Startup Context). Use it to conditionally show admin-only UI and to guard any calls to admin-only endpoints. The API will reject admin-only calls from regular users regardless, but checking client-side avoids unnecessary failed requests.
User rights bitmask
A user's rights are available as a bitmask from c=get&t=users&oper=info (see API: Get). One important bit controls whether the user can see all domains regardless of category assignment. If a user does not have this right and is not an admin, query results should be filtered to only the category IDs assigned to that user - obtainable from the same endpoint's cids array.
API Overview
All requests go to api.php. Requests should be sent over HTTPS. The standard URL structure is:
api.php?c=command&t=target&oper=operation
Every response is JSON. A successful call always returns "status": "ok". A failed call returns "status": "notok" with a description in the error field. Authenticated responses also include a validate field which is always 0 for a valid session.
| Command (c=) | Access | Purpose |
|---|---|---|
auth | Both | Authenticate or log out |
grid | Both | Paginated data tables - read, delete, edit rows |
get | Both | Fetch a single record, column metadata, config values, lookup queue |
set | Both | Update domain data, queue lookups, manage category membership |
list | Both | Column metadata lists, category lists, domain history |
add | Both | Import domains via CSV |
admin | Admin | Write config values and other admin operations |
API: Authentication
All API commands require prior authentication. For embedded and same-origin tools the browser session handles this automatically. For external scripts, authenticate explicitly using the auth command. Once authenticated the session remains valid until the user logs out or the session times out.
Login
Authenticates and creates a session.
| Parameter | Notes |
|---|---|
c | Always auth |
user | The user's login name |
pass | The user's password |
On failure:
{ "status": "notok", "user": "", "group": "", "error": "Invalid name or password (1/6)" }
On success:
{
"status": "ok",
"user": "myuser",
"group": "setupadmin",
"error": "",
"userid": 0,
"admin": 1,
"url": "https://example.com/wmdsed/"
}
The admin field is 1 if the user has administrator rights. The url field contains the absolute base URL of the WMD installation, which can be used to construct the full API URL for subsequent calls.
Logout
user and pass parameters are ignored when logout=1 is present.Session ID pattern for PHP scripts
The standard stateful API works well for browser-based tools, but is awkward for server-side PHP scripts that need to authenticate and make API calls without a browser. The recommended approach is to create a custom copy of api.php that accepts a session ID as a URL parameter, allowing the script to manage its own session explicitly.
- Copy
api.phpto a new file - for examplemyapi.php- in the same installation folder. Do not modify the originalapi.php. - At the top of
myapi.php, before therequire 'lib/php/loader.php'line, add:if (isset($_REQUEST['seid'])) { $seid = $_REQUEST['seid']; // validate $seid contains only safe characters before using it session_id($seid); } - In your PHP script, generate a session ID, pass it to
myapi.phpas theseidparameter, then authenticate and make API calls using that same ID:$seid = session_create_id('myprefix-'); $apiUrl = "https://example.com/wmdsed/myapi.php?seid=$seid"; // First call: authenticate // Subsequent calls: use the same $apiUrl - session is now active
myapi.php to trusted IP addresses (check $_SERVER['REMOTE_ADDR']) and use it only within a trusted network or intranet. Always use HTTPS.
Sample myapi.php
Place this file in the WMD installation root alongside the original api.php. It adds session ID injection and optional IP allowlisting, then hands off to the standard loader unchanged.
<?php
/**
* myapi.php - WMD SED custom API entry point for server-side scripts.
*
* Place in the WMD installation root (same folder as api.php).
* Do NOT modify the original api.php.
*
* Usage: call this file instead of api.php, passing a session ID
* as the 'seid' parameter, e.g.:
* https://example.com/wmdsed/myapi.php?seid=abc123&c=auth&user=user&pass=xxx
*/
// ── Optional: restrict access to specific IP addresses ──────────────────────
// $allowed_ips = ['127.0.0.1', '192.168.1.10'];
// if (!in_array($_SERVER['REMOTE_ADDR'], $allowed_ips)) {
// http_response_code(403);
// exit(json_encode(['status' => 'notok', 'error' => 'Access denied']));
// }
// ── Session ID injection ─────────────────────────────────────────────────────
// Must happen before session_start(), which loader.php triggers internally.
if (isset($_REQUEST['seid'])) {
$seid = $_REQUEST['seid'];
// Allow only characters that are valid in a PHP session ID.
if (preg_match('/^[a-zA-Z0-9,\-]+$/', $seid)) {
session_id($seid);
}
}
// ── Hand off to the standard WMD loader ─────────────────────────────────────
// Everything below this line is identical to api.php.
require 'lib/php/loader.php';
And a minimal PHP client script showing the full authenticate → query → logout cycle:
<?php
/**
* Example: fetch domains expiring in the next 30 days via myapi.php.
*/
$base = 'https://example.com/wmdsed/';
$api = $base . 'myapi.php';
$seid = session_create_id('ext-'); // generate a unique session ID
// Helper: GET request carrying the session ID
function wmd_get(string $api, string $seid, string $query): array {
$url = $api . '?seid=' . urlencode($seid) . '&' . $query;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true) ?? [];
}
// 1. Authenticate
$auth = wmd_get($api, $seid, 'c=auth&user=myuser&pass=mypassword');
if (($auth['status'] ?? '') !== 'ok') {
die('Login failed: ' . ($auth['error'] ?? 'unknown'));
}
// 2. Query domains expiring within 30 days
$today = date('Y-m-d');
$future = date('Y-m-d', strtotime('+30 days'));
$filter = urlencode("registry_expiry>='$today' AND registry_expiry<='$future'");
$result = wmd_get($api, $seid,
"c=grid&t=domain&columns=domain,registry_expiry&page=1&rows=500" .
"&sidx=registry_expiry&sord=asc&query=$filter"
);
foreach ($result['rows'] ?? [] as $row) {
echo $row['domain'] . ' expires ' . $row['registry_expiry'] . PHP_EOL;
}
// 3. Logout
wmd_get($api, $seid, 'c=auth&logout=1');
API: Grid
Paginated access to domain, category, DNS, SSL and auto-query data tables.
Read grid data
d. prefix for the domain table and s. for the subdomain table.
| Parameter | Notes |
|---|---|
t | domain, dns, ssl, category, query |
columns | Comma-separated column names. Use d. / s. prefixes where needed. |
page | Page number, starting from 1 |
rows | Rows per page |
sidx | Sort column name |
sord | asc or desc |
cid | Filter by category ID (domain grid only) |
aqid | Filter by auto-query ID (domain grid only) |
query | Optional additional SQL filter expression |
{ "page": 1, "total": 5, "records": "487", "rows": [ { "id": "23", "domain": "example.com", ... } ] }
total is the number of pages. records is the total row count as a string. Calculate total pages yourself: Math.ceil(parseInt(records) / rowsPerPage).
x.total >= 0 || x.status === 'ok'. Do not rely on status: "ok" alone.
c=grid is the only endpoint that does not include a validate field in its JSON response. All other endpoints (c=get, c=set, c=list, c=admin, etc.) do return validate. For grid requests, session errors are signalled via an HTTP-level failure (non-2xx status code). Check response.ok before parsing the JSON and treat an HTTP error as a session timeout.
Delete rows
id is a comma-separated list of row IDs. Deleting categories or auto-queries requires admin rights.
Edit a row
{ "status": "ok", "updated": 1 }
API: Get
Fetch a single domain record, specific columns, current user info, or lookup queue status.
Full domain record
data- all column values for the domaininfo- metadata for each column (label, fieldtype, editable, gridview, width)cids- array of category IDs the domain belongs toluq- current lookup queue entries for the domainsubdomains- subdomain hostnames
Specific columns only
{ "status": "ok", "data": [ { "domain": "softnik.org", "registry_expiry": "2024-11-29" } ] }
Current user info
data object.
{
"status": "ok", "validate": 0,
"data": {
"rights": 65535,
"uid": 0,
"cids": [ { "cid": 2, "name": "Keyword Domains" }, ... ],
"license": { ... }
}
}
Note: this endpoint does not return the username. Store the username from the c=auth login response if you need it later (e.g. in sessionStorage). The rights field is a bitmask - a value of 65535 indicates full administrator rights. The cids array lists the categories the user can access.
Lookup queue status
luqinfo- array of pending queue entries. Empty array means the lookup is complete.queue_size- total pending lookups across all domains on the serverlast_lookup_at- timestamp of the last completed lookup
API: Set
Update domain column values and manage the lookup queue.
Bulk edit one column across multiple domains
id=186,187,188&columns=customer_name&data=ABC+Inc. Sets the same value for one column across all specified domains.
{ "status": "ok", "updated": 3 }
Set multiple columns for one domain
sid=186&customer_name=ABC+Inc&invoice_no=A5690. Pass any number of column name/value pairs. Note: uses sid as the identifier, not id.
{ "status": "ok", "count": 2 }
Queue a domain lookup
what=1&id=173,174&ri=300. Adds domains to the background lookup queue.
what is a bitmask of lookup types: 1=domain WHOIS, 2=root DNS, 4=HTTP, 64=SSL, 128=IP WHOIS, 512=subdomains.
Combine types with bitwise OR. ri is the minimum refresh interval in seconds.
API: List
Retrieve column metadata and category information.
All domain column metadata
{
"columns": {
"registry_expiry": {
"label": "Domain Expiry",
"editable": "1",
"gridview": "1",
"custom": "0",
"fieldtype": "date",
"width": "123",
"acslevel": "0"
}
}
}
| Field | Meaning |
|---|---|
label | Human-readable column name |
editable | "1" if the column can be written via the API |
gridview | "1" if the column is intended for table/grid display |
custom | "1" if this is a user-defined custom column |
fieldtype | string, integer, float, boolean, date, datetime, text |
width | Last saved display width in pixels |
Add &group=type to receive columns grouped by fieldtype instead of a flat object.
Categories accessible to current user
ids array. Each entry includes id, name, caticon (icon filename), sortindex, and userids (array of user IDs that have access).
{
"status": "ok",
"validate": 0,
"ids": [
{ "id": "2", "name": "Keyword Domains", "caticon": "folder-gray.png", "sortindex": "1", "userids": [1] },
{ "id": "3", "name": "Parked Domains", "caticon": "folder-gray.png", "sortindex": "2", "userids": [1] }
]
}
Category ID 1 is the virtual "All Domains" category and is not included in this response. Alternatively, use c=grid&t=category&columns=name,sortindex,caticon&page=1&rows=50&sidx=sortindex&sord=asc to retrieve categories via the grid endpoint.
Category membership for specific domains
{ "categories": { "41": [ { "cid": 6, "name": "Bookmarked Domains" }, ... ] } }
API: Add (Domain Import)
csv_file- the CSV file. Must have a header row with column names.csv_sep- field separator, default,cid- optional category ID to assign imported domains to
editable=1 can be set via import.
{ "status": "ok", "added": 15 }
API: Config Store
A simple server-side key-value store for saving tool settings. Config is global - shared across all users. There is no per-user config store in WMD. Writing requires admin rights; reading is available to all users.
configname=config_my_key. Returns the stored value (a raw string) in the data field. Parse it as JSON if the value was stored as JSON.
configname=config_my_key&configdata={"any":"json"}. configdata is stored as a raw string - JSON-encode it before sending.
config_ prefix - always include it in the configname parameter. Keys must use only letters, digits and underscores. Hyphens cause a validation error.
Example: store as config_my_tool_settings.
localStorage is acceptable - but make this clear to the user, as those settings will not roam to other browsers or devices.
API: Category Operations
Add or remove domains from categories. These operations work on the category membership table, not the domain data itself.
cid=2,5&id=23,24,27. Both cid and id accept comma-separated lists.
{ "status": "ok", "added": 6 }
cid=5&id=23,24.
{ "status": "ok" }
Column width persistence
name=registry_expiry&width=220.
For virtual columns that have no server-side backing, save widths to localStorage instead.
Further Reading
This guide covers the bare minimum needed to build a working tool against the WMD API - authentication, session handling, deployment models, and the core data endpoints. The full API is considerably more extensive. Through the API you can also:
- Add, edit and delete custom domain columns and set column-level access rights
- Create, edit and delete users and configure their access rights
- Create and schedule CSV reports and trigger email reports
- Schedule and trigger domain, DNS, SSL and HTTP lookups
- Retrieve subdomain and DNS record data
- Configure WHOIS and RDAP settings per TLD
- Enable and configure DNS change monitoring
- Take and retrieve web page screenshots (if the feature is enabled)
- Configure email settings and templates
- Configure SAML/SSO and two-factor authentication (2FA)
- Set the default session timeout (native mode)
- Change branding and logo
- Access audit logs and system logs
The full API reference is available at learn.domainpunch.com/wmdsed/api/.