.env
Stores the secret Application Password outside your code.
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxxCreate one local folder that ChatGPT Codex, Claude Code or another AI tool can access, then connect it securely to WordPress.
Install and activate Elementor REST Bridge in WordPress, then activate your license or trial. You need WordPress 6.0+, PHP 7.4+, Elementor and HTTPS. The bridge registers no REST fields without an active license or trial.
Create a dedicated folder on your computer inside the workspace your AI agent can access. Add these three files to that same folder:
.envStores the secret Application Password outside your code.
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxxwp_client.pyPlace the supplied Python client file here. It contains no hardcoded credentials and reads the password from .env.
wp_sites.jsonReplace the example URL and email with your own WordPress details.
{
"site": {
"url": "https://yourdomain.com",
"username": "email@example.com",
"app_password": "env:WP_APP_PASSWORD"
}
}This is a separate, revocable password for the AI connection. Never use or share your normal WordPress login password.
Users → Profile
Copy the spaced password and paste it after WP_APP_PASSWORD= in your local .env file.
wp_client.pyExpand 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.
#!/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 'elementor-rest-bridge.php' 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()
Open the local folder as a new session in ChatGPT Codex, Claude Code or another AI tool that can work with files and a terminal. Replace the example URL with your own WordPress domain, then give the AI this instruction:
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.
.env to Git or upload it to a shared drive.Start your 14-day free trial. No credit card required.
Start your free trial →