Documentation
Remote API
Every directory running Web Directory Software 1.3 or later answers a JSON API at /api/v1/. The Web Directory Manager uses it, and so can your own programs.
Keys
The site owner makes keys in the directory's admin: Settings › API keys › Create key. A key is shown once and looks like this:
wds_live_k7f2m9qa_Qm4xT9vR2pLw8ZcN5jHyB3uE6sKd1aFg
wds_live_ (or wds_test_ on test copies), then the key ID, which is not secret, then the secret. Each key has an access level made of permissions (shown with every endpoint below), and may be limited to IP addresses or given an end date. The owner can switch any key off, rotate it or delete it at any time; everything a key changes is in the site's activity log.
Some things are never possible with a key: making or changing keys, the admin password, payment and API-key settings, templates' files and anything that runs code.
Requests
Send the key in a header, never in the address, and use https:
curl https://example-directory.com/api/v1/site \ -H "Authorization: Bearer wds_live_k7f2m9qa_Qm4x..."
Bodies are JSON (Content-Type: application/json), except picture uploads, which may be multipart. If a server drops the Authorization header, send X-Api-Key instead. Where address rewriting is not available, the same requests work as /api/index.php?r=/v1/site. Hosts that block PATCH and DELETE accept POST with X-HTTP-Method-Override.
Answers and errors
{"ok": true, "data": { ... }, "meta": {"total": 120, "page": 1, "per": 25, "pages": 5}}
{"ok": false, "error": {"code": "invalid", "message": "Some fields need fixing.", "fields": {"url": "must be a full address"}}}
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_json | The body is not valid JSON. |
| 401 | no_key, bad_key | No key, or not a valid key for this site. |
| 403 | forbidden | The key lacks the permission for this request. |
| 403 | key_disabled, key_expired, ip_not_allowed, https_required | The key is switched off, past its end date, used from another address, or the request was not https. |
| 404 | not_found, not_available | No such item, or a feature this edition does not have. |
| 409 | duplicate, wrong_state | Already exists, or not possible in the item's current state. |
| 422 | invalid | Fields need fixing (see fields). |
| 429 | rate_limited, blocked | Too many requests this hour (Retry-After says when), or too many wrong keys from this address. |
| 503 | api_off | The owner has switched off all keys. |
Limits and safe retries
- 600 requests an hour per key. Twenty wrong keys from one address block it for an hour.
- Send an
Idempotency-Keyheader with a POST, PATCH or DELETE: repeating the request with the same key within a day returns the first answer (withIdempotent-Replayed: true) instead of doing it twice. - Lists return 25 items a page by default, at most 100 (
per,page).
Endpoints
All paths start with /api/v1. The permission each needs is shown under it.
Site
| Request | What it does |
|---|---|
GET /sitesite:read | Name, address, version, edition, features, and this key's permissions. |
GET /statsstats:read | Listings waiting and live, premium, categories, articles, clicks, revenue in the last 30 days. |
GET /healthhealth:read | Warnings, versions, free disk space, broken links, event notices waiting, update available. |
POST /tickhealth:read | Run the site's routine work now (notices, cleaning up, the daily update check). |
Listings
| Request | What it does |
|---|---|
GET /listingslistings:read | List. Filters: status (any, pending, live, inactive, removed), view (all, premium, regular, reciprocal, paid, ended, bizpage), category_id, q, sort (new, old, title, clicks), page, per. |
GET /listings/{id}listings:read | One listing with its payments and business page. |
POST /listingslistings:write | Add: title, url, category_id, description, email, owner_name, premium, reciprocal_url, link (auto, follow, nofollow, sponsored), slug, status (live or pending). |
PATCH /listings/{id}listings:write | Change any of the fields above. |
POST /listings/{id}/approvelistings:moderate | Approve a waiting listing. email_owner: true, false, or leave out to follow the site's setting. |
POST /listings/{id}/rejectlistings:moderate | Reject a waiting listing, with an optional reason sent to the owner. |
POST /listings/{id}/activatelistings:moderate | Also deactivate, premium, regular. |
DELETE /listings/{id}listings:delete | Delete. Kept 30 days in the trash; the site owner can restore it. |
Categories
| Request | What it does |
|---|---|
GET /categoriescategories:read | Every category with parent, depth, full name, listing count and address. |
POST /categoriescategories:write | Add: name, parent_id, description, meta_description. |
PATCH /categories/{id}categories:write | Rename, or change the description. |
Business pages
| Request | What it does |
|---|---|
GET /bizpagesbizpages:read | status: pending (default), live, rejected, off, draft, any. |
POST /bizpages/{listing_id}/approvebizpages:moderate | Publish it. Also /reject with a note to the owner. |
Articles
| Request | What it does |
|---|---|
GET /articlesarticles:read | status (any, review, draft, scheduled, published, off), category_id, ref, source (api, this_key), q, updated_since. |
GET /articles/{id}articles:read | One article with its full text. |
POST /articlesarticles:write | Write: title, summary, body_html (cleaned like the editor), category_id, source_name, source_url, author, ai, noindex, ref, slug, status (draft, review, published, scheduled), publish_at, image_base64, image_alt, image_credit, image_credit_url. Publishing needs articles:publish. |
PATCH /articles/{id}articles:write | Change any of the fields above. |
POST /articles/{id}/publisharticles:publish | Publish now, or at a later time with "at". Also /unpublish and /send-back (with notes). |
POST /articles/{id}/imagearticles:write | The main picture: multipart "file" or image_base64. |
POST /mediaarticles:write | A picture for use inside an article's text (article_id plus the file); answers with its address. |
DELETE /articles/{id}articles:delete | Delete. Kept 30 days in the trash. |
Other
| Request | What it does |
|---|---|
GET /paymentspayments:read | Payments received, newest first. Filter: listing_id. |
GET /messagesmessages:read | Contact form messages. |
GET /newsdesknewsdesk:read | Runs and costs of the built-in news desk (full edition only). POST /newsdesk/run starts one. |
GET /loglog:read | The API activity log. Filter: key. |
GET /updatessystem:update | Installed and latest version, licence status. POST /updates/install installs the latest. |
GET /templatessystem:update | The template gallery for this licence. POST /templates/install with name (and activate). |
Event notices
| Request | What it does |
|---|---|
GET /events/subscriptionsevents:manage | This key's subscriptions and the events available. |
POST /events/subscriptionsevents:manage | Subscribe: url (https), events (a list, or ["*"]). Answers with the secret used to sign the notices, shown once. |
DELETE /events/subscriptions/{id}events:manage | Stop a subscription. |
POST /events/testevents:manage | Send a test notice to this key's subscriptions now. |
Event notices
Instead of asking every few minutes, a program can be told when something happens. The site sends a POST with a JSON body to the subscribed address:
{"event": "listing.submitted", "site": "https://example-directory.com/", "time": "2026-10-08T09:30:00+00:00",
"data": {"id": 1204, "title": "Joe the Plumber", "url": "https://joe.example.com/", "category_id": 17, "premium": false}}
Events: listing.submitted, listing.approved, payment.received, payment.refunded, bizpage.submitted, article.review, article.published, article.deleted, newsdesk.finished, key.rotated, site.updated. Answer with any 2xx status; otherwise the notice is sent again, waiting longer each time, for 24 hours.
Each notice is signed. Check it before trusting it:
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_WDS_TIMESTAMP'];
$want = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret); // secret from the subscription
if (! hash_equals($want, $_SERVER['HTTP_X_WDS_SIGNATURE']) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
Headers: X-WDS-Event, X-WDS-Delivery (a number, the same on every retry), X-WDS-Timestamp, X-WDS-Signature.
Versions
/api/v1/ stays as it is. Changes that would break programs get a new version; GET /site tells which version and features a site has.
Questions: contact us.