Web Directory Software

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"}}}
HTTPcodeMeaning
400bad_jsonThe body is not valid JSON.
401no_key, bad_keyNo key, or not a valid key for this site.
403forbiddenThe key lacks the permission for this request.
403key_disabled, key_expired, ip_not_allowed, https_requiredThe key is switched off, past its end date, used from another address, or the request was not https.
404not_found, not_availableNo such item, or a feature this edition does not have.
409duplicate, wrong_stateAlready exists, or not possible in the item's current state.
422invalidFields need fixing (see fields).
429rate_limited, blockedToo many requests this hour (Retry-After says when), or too many wrong keys from this address.
503api_offThe 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-Key header with a POST, PATCH or DELETE: repeating the request with the same key within a day returns the first answer (with Idempotent-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

RequestWhat it does
GET /site
site:read
Name, address, version, edition, features, and this key's permissions.
GET /stats
stats:read
Listings waiting and live, premium, categories, articles, clicks, revenue in the last 30 days.
GET /health
health:read
Warnings, versions, free disk space, broken links, event notices waiting, update available.
POST /tick
health:read
Run the site's routine work now (notices, cleaning up, the daily update check).

Listings

RequestWhat it does
GET /listings
listings: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 /listings
listings: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}/approve
listings:moderate
Approve a waiting listing. email_owner: true, false, or leave out to follow the site's setting.
POST /listings/{id}/reject
listings:moderate
Reject a waiting listing, with an optional reason sent to the owner.
POST /listings/{id}/activate
listings:moderate
Also deactivate, premium, regular.
DELETE /listings/{id}
listings:delete
Delete. Kept 30 days in the trash; the site owner can restore it.

Categories

RequestWhat it does
GET /categories
categories:read
Every category with parent, depth, full name, listing count and address.
POST /categories
categories:write
Add: name, parent_id, description, meta_description.
PATCH /categories/{id}
categories:write
Rename, or change the description.

Business pages

RequestWhat it does
GET /bizpages
bizpages:read
status: pending (default), live, rejected, off, draft, any.
POST /bizpages/{listing_id}/approve
bizpages:moderate
Publish it. Also /reject with a note to the owner.

Articles

RequestWhat it does
GET /articles
articles: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 /articles
articles: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}/publish
articles:publish
Publish now, or at a later time with "at". Also /unpublish and /send-back (with notes).
POST /articles/{id}/image
articles:write
The main picture: multipart "file" or image_base64.
POST /media
articles: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

RequestWhat it does
GET /payments
payments:read
Payments received, newest first. Filter: listing_id.
GET /messages
messages:read
Contact form messages.
GET /newsdesk
newsdesk:read
Runs and costs of the built-in news desk (full edition only). POST /newsdesk/run starts one.
GET /log
log:read
The API activity log. Filter: key.
GET /updates
system:update
Installed and latest version, licence status. POST /updates/install installs the latest.
GET /templates
system:update
The template gallery for this licence. POST /templates/install with name (and activate).

Event notices

RequestWhat it does
GET /events/subscriptions
events:manage
This key's subscriptions and the events available.
POST /events/subscriptions
events: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/test
events: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.