How-to
Use WREN from Python
Everything is plain HTTP and JSON. This page covers the few things that trip scripts up.
"HTTP Error 403: Forbidden", error code 1010
WREN servers behind Cloudflare may have a browser-integrity check switched on. It rejects the default User-Agent of Python's urllib (Python-urllib/3.x) with 403 and the body error code: 1010, before the request reaches WREN.
requests,httpxandcurlare not affected.- With
urllib, send your ownUser-Agentheader: any honest name likemy-scraper/1.0works. - If you run the server: turn off Browser Integrity Check for the WREN hostname (a Cloudflare Configuration Rule), and add a WAF skip rule if Bot Fight Mode is on.
Setup
import os, requests
from urllib.parse import quote
BASE = os.environ.get("WREN_URL", "https://wren.aemwip.com")
SLUG = "oak-spur-spur" # your org slug: GET /api/v1/me → org.slug
wren = requests.Session() # private API
wren.headers["Authorization"] = f"Bearer {os.environ['WREN_API_KEY']}"
pub = requests.Session() # public API: no key, everKeep the key in an environment variable or a secret store, never in the script. A key always acts in the org it was created in.
Public reads (no key)
P = f"{BASE}/api/v1/orgs/{SLUG}"
events = pub.get(f"{P}/events", params={"limit": 50, "where": "country:SUI", "select": "name,date"}).json()["items"]
one = pub.get(f"{P}/events/by-key/{quote('swiss-open-2026', safe='')}").json()
counts = pub.post(f"{P}/events/_query", json={
"aggregate": {"groupBy": ["country"], "metrics": {"n": {"count": "name"}}}}).json()["rows"]These only work for collections with a principal: "*" read rule. Otherwise they return 403.
Private reads and writes
A = f"{BASE}/api/v1"
def upsert(collection: str, key: str, doc: dict) -> dict:
"""Create the first time, new version afterwards (collection needs a naturalKey)."""
r = wren.put(f"{A}/{collection}/by-key/{quote(key, safe='')}", json=doc)
r.raise_for_status()
return r.json()
upsert("events", "swiss-open-2026", {"slug": "swiss-open-2026", "name": "Swiss Open 2026", "date": "2026-09-26"})
doc = wren.get(f"{A}/events/by-key/swiss-open-2026", params={"label": "preview"}).json()
hist = wren.get(f"{A}/events/{doc['id']}/versions").json()
wren.post(f"{A}/events/{doc['id']}/labels", json={"label": "published"}).raise_for_status()Files
with open("report.html", "rb") as f:
files = {"file": ("index.html", f, "text/html")}
created = wren.post(f"{A}/reports-assets", files=files).json() # new document
# later: new version of the SAME document (don't POST again)
with open("report.html", "rb") as f:
wren.put(f"{A}/reports-assets/{created['id']}", files={"file": ("index.html", f, "text/html")}).raise_for_status()
wren.put(f"{A}/tree/reports/trophy-2026/index.html", json={"documentId": created["id"]}).raise_for_status()More than 1,000 rows
A _query page returns at most 1,000 items. Asking for more is capped silently, so follow the cursor:
def query_all(collection: str, q: dict):
body = {**q, "limit": 1000}
while True:
page = wren.post(f"{A}/{collection}/_query", json=body).json()
yield from page["items"]
if not page.get("cursor"):
return
body["cursor"] = page["cursor"]
results = list(query_all("results", {"where": "event:swiss-open-2026", "select": ["ring", "winner"]}))For numbers you recompute after every write (standings, medal tables), a materialized query (PUT /api/v1/{collection}/_materialized/{name}, admin) keeps the result up to date on the server.
Standard library only
import json, os, urllib.request
def get(path: str, key: str | None = None):
headers = {"Accept": "application/json", "User-Agent": "my-script/1.0"} # avoids error 1010
if key:
headers["Authorization"] = f"Bearer {key}"
with urllib.request.urlopen(urllib.request.Request(BASE + path, headers=headers)) as r:
return json.load(r)
me = get("/api/v1/me", os.environ["WREN_API_KEY"])Windows: when you pass ids between scripts through a shell, strip line endings (value.strip()). An id with a trailing \r builds a URL that silently fails.