Quick setup guide

Connect your AI agent in minutes

This guide shows one practical way to connect a compatible AI tool securely. You are free to use your own preferred WordPress connection workflow.

Before you start

WP Element Bridge Plugin is the only additional WordPress plugin you need. It uses the REST API and Application Passwords already built into WordPress—no authentication plugin, middleware or separate API server required. Install it, activate your license or trial, and make sure you have WordPress 6.0+, PHP 7.4+, Elementor and HTTPS.

This setup is an example, not a requirement. The Python client and three-file workflow below are one convenient way to connect an AI tool. You may give your AI tools access to WordPress in any other secure way you prefer. The plugin simply makes the permitted Elementor and SEO fields available through the standard WordPress REST API; it does not depend on this Python client.

1. Purchase, install and activate the plugin

  1. Choose the subscription that covers the number of WordPress sites you manage and complete the checkout.
  2. After checkout, you will receive an email containing the plugin ZIP file and your license key. Keep this email and license key in a safe place.
  3. Sign in to WordPress Admin and open Plugins → Add New Plugin → Upload Plugin.
  4. Select the ZIP file from the email, click Install Now, and then click Activate Plugin.
  5. When prompted, enter the license key from the email and activate it. The REST bridge becomes available as soon as the license or trial is active.
Installation complete. The plugin is now ready. Continue below to connect your preferred AI agent, or use another secure WordPress REST API workflow of your choice.

2. Create a local working folder

Create a dedicated folder on your computer inside the workspace your AI agent can access. Add these three files to that same folder:

1

.env

Stores the secret Application Password outside your code.

.env
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
2

wp_client.py

Copy the verified Python client from this guide. It contains no hardcoded credentials and reads the password from .env.

Never add a real password, token or domain-specific secret to the Python source.
3

wp_sites.json

Replace the example URL and email with your own WordPress details.

wp_sites.json
{
  "site": {
    "url": "https://yourdomain.com",
    "username": "email@example.com",
    "app_password": "env:WP_APP_PASSWORD"
  }
}
WordPress

3. Create an Application Password

This is a separate, revocable password for the AI connection. Never use or share your normal WordPress login password.

  1. Sign in to WordPress Admin.
  2. Open Users → Profile.
  3. Scroll to Application Passwords.
  4. Enter a name such as AI agent – Elementor editing.
  5. Click Add New Application Password.
  6. Copy the generated password immediately—it is only shown once.

Copy wp_client.py

Expand the verified source code below, copy it and save it as wp_client.py in the same local folder as your other two files. The client contains no hardcoded domain, username, password, token or API key.

Show the complete Python client
wp_client.py · verified without embedded credentials
#!/usr/bin/env python3
"""
WordPress REST API client op basis van application passwords.
Werkt met meerdere zelf-gehoste WordPress-sites via wp_sites.json.
Ondersteunt posts, pagina's, media en (met de meegeleverde plugin)
Elementor-paginadata.

Vereisten:
    pip install requests

Gebruik:
    python3 wp_client.py test <site>
    python3 wp_client.py list <site> [posts|pages] [status]
    python3 wp_client.py get <site> [posts|pages] <id>
    python3 wp_client.py create <site> [posts|pages] "<titel>" "<inhoud>" [status]
    python3 wp_client.py update <site> [posts|pages] <id> field=value [field2=value2 ...]
    python3 wp_client.py delete <site> [posts|pages] <id>
    python3 wp_client.py upload-media <site> <bestandspad>
    python3 wp_client.py get-elementor <site> <id> [pages|posts]
    python3 wp_client.py backup-elementor <site> <id> <bestandspad> [pages|posts]
    python3 wp_client.py set-elementor <site> <id> <pad-naar-json-bestand> [pages|posts]

Voorbeelden:
    python3 wp_client.py test vynd
    python3 wp_client.py list vynd pages publish
    python3 wp_client.py create vynd pages "Nieuwe pagina" "<p>Tekst</p>" draft
    python3 wp_client.py get-elementor vynd 123 pages

BEWUST NIET ONDERSTEUND (en dat moet zo blijven, tenzij expliciet anders gevraagd):
    - Het wijzigen van de site-URL / domeininstellingen
    - Het installeren, activeren of verwijderen van thema's
    - Het installeren, activeren of verwijderen van plugins
Dit script heeft hier simpelweg geen functies voor. Voeg die ook niet toe zonder
expliciete, uitdrukkelijke toestemming van de klant/eigenaar van dit script.
"""

import json
import mimetypes
import os
import sys
from pathlib import Path

import requests

SITES_FILE = Path(__file__).parent / "wp_sites.json"
ENV_FILE = Path(__file__).parent / ".env"

# Metavelden die Elementor gebruikt om paginadata op te slaan.
ELEMENTOR_META_KEYS = [
    "_elementor_data",
    "_elementor_edit_mode",
    "_elementor_template_type",
    "_elementor_version",
    "_elementor_page_settings",
]


def _load_env_file():
    """Leest .env (KEY=waarde per regel) in en zet ze als omgevingsvariabele,
    zonder een extra package (python-dotenv) nodig te hebben."""
    if not ENV_FILE.exists():
        return
    with open(ENV_FILE, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, _, value = line.partition("=")
            os.environ.setdefault(key.strip(), value.strip())


_load_env_file()


def load_sites():
    with open(SITES_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def get_site(name):
    sites = load_sites()
    if name not in sites:
        raise SystemExit(f"Site '{name}' niet gevonden in {SITES_FILE.name}. "
                          f"Beschikbaar: {', '.join(sites.keys())}")
    site = dict(sites[name])  # kopie, zodat we 'm hieronder mogen aanpassen

    pw = site.get("app_password", "")
    if pw.startswith("env:"):
        env_key = pw[4:].strip()
        real_pw = os.environ.get(env_key)
        if not real_pw:
            raise SystemExit(
                f"Omgevingsvariabele '{env_key}' niet gevonden. "
                f"Zet 'm in {ENV_FILE.name} als: {env_key}=je-application-password"
            )
        site["app_password"] = real_pw

    return site


def _auth(site):
    return (site["username"], site["app_password"])


def _base(site):
    return site["url"].rstrip("/") + "/wp-json/wp/v2"


def _check(r):
    if not r.ok:
        try:
            detail = r.json()
        except ValueError:
            detail = r.text
        raise SystemExit(f"Fout {r.status_code}: {detail}")
    return r


# ---------- Algemeen (werkt voor elk contenttype: posts, pages, ...) ----------

def test_connection(name):
    site = get_site(name)
    r = _check(requests.get(f"{_base(site)}/users/me", auth=_auth(site), timeout=15))
    data = r.json()
    print(f"Verbonden als '{data.get('name')}' (id {data.get('id')}) op {site['url']}")
    return data


def list_items(name, content_type="posts", status="publish", per_page=20):
    site = get_site(name)
    r = _check(requests.get(
        f"{_base(site)}/{content_type}",
        auth=_auth(site),
        params={"per_page": per_page, "status": status},
        timeout=15,
    ))
    for item in r.json():
        title = item.get("title", {})
        if isinstance(title, dict):
            title = title.get("rendered") or title.get("raw") or "(zonder titel)"
        link = item.get("link") or item.get("url") or ""
        suffix = f" - {link}" if link else ""
        print(f"[{item['id']}] {title} ({item.get('status', 'onbekend')}){suffix}")
    return r.json()


def get_item(name, content_type, item_id):
    site = get_site(name)
    r = _check(requests.get(
        f"{_base(site)}/{content_type}/{item_id}",
        auth=_auth(site),
        params={"context": "edit"},
        timeout=15,
    ))
    return r.json()


def create_item(name, content_type, title, content, status="draft", **extra_fields):
    site = get_site(name)
    payload = {"title": title, "content": content, "status": status, **extra_fields}
    r = _check(requests.post(
        f"{_base(site)}/{content_type}",
        auth=_auth(site),
        json=payload,
        timeout=15,
    ))
    data = r.json()
    print(f"Aangemaakt: [{data['id']}] {data['title']['rendered']} - {data['link']}")
    return data


def update_item(name, content_type, item_id, **fields):
    site = get_site(name)
    r = _check(requests.post(
        f"{_base(site)}/{content_type}/{item_id}",
        auth=_auth(site),
        json=fields,
        timeout=15,
    ))
    data = r.json()
    print(f"Bijgewerkt: [{data['id']}] {data['title']['rendered']}")
    return data


def delete_item(name, content_type, item_id, force=False):
    site = get_site(name)
    r = _check(requests.delete(
        f"{_base(site)}/{content_type}/{item_id}",
        auth=_auth(site),
        params={"force": force},
        timeout=15,
    ))
    print(f"Verwijderd: {item_id} ({'permanent' if force else 'naar prullenbak'})")
    return r.json()


def upload_media(name, file_path):
    site = get_site(name)
    file_path = Path(file_path)
    content_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
    with open(file_path, "rb") as f:
        r = _check(requests.post(
            f"{_base(site)}/media",
            auth=_auth(site),
            headers={
                "Content-Disposition": f'attachment; filename="{file_path.name}"',
                "Content-Type": content_type,
            },
            data=f.read(),
            timeout=60,
        ))
    data = r.json()
    print(f"Geupload: [{data['id']}] {data['source_url']}")
    return data


# ---------- Elementor-specifiek ----------
# Vereist de meegeleverde plugin 'WP Element Bridge Plugin' actief op de site,
# anders geeft de API deze metavelden niet vrij.

def get_elementor_data(name, item_id, content_type="pages"):
    item = get_item(name, content_type, item_id)
    meta = item.get("meta", {})
    raw = meta.get("_elementor_data")
    parsed = json.loads(raw) if raw else None
    return {
        "elementor_data": parsed,
        "edit_mode": meta.get("_elementor_edit_mode"),
        "template_type": meta.get("_elementor_template_type"),
        "version": meta.get("_elementor_version"),
        "page_settings": meta.get("_elementor_page_settings"),
    }


def set_elementor_data(name, item_id, elementor_data, content_type="pages"):
    """elementor_data: python object (list/dict) zoals Elementor het verwacht,
    of al een JSON-string."""
    if not isinstance(elementor_data, str):
        elementor_data = json.dumps(elementor_data)
    return update_item(name, content_type, item_id, meta={"_elementor_data": elementor_data})


# ---------- CLI ----------

def _parse_kv(args):
    fields = {}
    for a in args:
        key, _, value = a.partition("=")
        fields[key] = value
    return fields


def _main():
    if len(sys.argv) < 3:
        print(__doc__)
        sys.exit(1)

    cmd, site_name, *rest = sys.argv[1:]

    if cmd == "test":
        test_connection(site_name)
    elif cmd == "list":
        content_type = rest[0] if len(rest) > 0 else "posts"
        status = rest[1] if len(rest) > 1 else "publish"
        list_items(site_name, content_type, status)
    elif cmd == "get":
        content_type, item_id = rest[0], rest[1]
        print(json.dumps(get_item(site_name, content_type, item_id), indent=2))
    elif cmd == "create":
        content_type, title, content = rest[0], rest[1], rest[2]
        status = rest[3] if len(rest) > 3 else "draft"
        create_item(site_name, content_type, title, content, status)
    elif cmd == "update":
        content_type, item_id, *kv = rest
        update_item(site_name, content_type, item_id, **_parse_kv(kv))
    elif cmd == "delete":
        content_type, item_id = rest[0], rest[1]
        delete_item(site_name, content_type, item_id)
    elif cmd == "upload-media":
        upload_media(site_name, rest[0])
    elif cmd == "get-elementor":
        item_id = rest[0]
        content_type = rest[1] if len(rest) > 1 else "pages"
        print(json.dumps(get_elementor_data(site_name, item_id, content_type), indent=2))
    elif cmd == "backup-elementor":
        item_id, json_path = rest[0], rest[1]
        content_type = rest[2] if len(rest) > 2 else "pages"
        data = get_elementor_data(site_name, item_id, content_type)
        path = Path(json_path)
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"Elementor-back-up geschreven: {path}")
    elif cmd == "set-elementor":
        item_id, json_path = rest[0], rest[1]
        content_type = rest[2] if len(rest) > 2 else "pages"
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        set_elementor_data(site_name, item_id, data, content_type)
    else:
        print(f"Onbekend commando: {cmd}")
        print(__doc__)
        sys.exit(1)


if __name__ == "__main__":
    _main()

4. Test the connection with your AI tool

Open the local folder in an AI tool that can work with files and a terminal—for example, ChatGPT Codex or Claude Code. Replace the example URL with your own WordPress domain, then give the AI this instruction:

Prompt for your AI agent
Test whether you have access to my WordPress site https://examplesite.com. Confirm that you can connect successfully and that you can read and edit Elementor page data. Start with a safe test and do not change live content without asking me first.

The AI agent can install the required Python requests package if it is not already available. Once it confirms WordPress access and verifies that Elementor data can be read and edited, you’re ready to go.

Optional: configure WordPress Multisite

On Multisite, raw Elementor and internal SEO fields are restricted to Super Admins by default. This is the safest setting and requires no additional configuration.

If trusted Editors also need access, open Network Admin → Settings → WP Element Bridge Plugin, enable the option that allows Editors to read and modify raw Elementor data through the REST bridge, and save the setting.

Only enable this when every affected Editor is trusted. The normal per-page WordPress edit permissions continue to apply, but the setting grants access to sensitive raw page data across the network.

Keep the credentials safe

  • Never commit .env to Git or upload it to a shared drive.
  • Never paste the Application Password into chat messages or documentation.
  • Use a dedicated WordPress user with only the permissions it needs.
  • Create a separate Application Password for each tool.
  • Revoke a password immediately when a tool no longer needs access.
  • Test bulk updates on staging or draft pages first.

Ready to stop building every Elementor page by hand?

Give your AI tool secure Elementor access and start saving hours. Try every feature free for 14 days—no credit card required.

Start saving time →