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, httpx and curl are not affected.
  • With urllib, send your own User-Agent header: any honest name like my-scraper/1.0 works.
  • 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

python
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, ever

Keep 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)

python
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

python
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

python
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:

python
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

python
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.