Run this playbook on-demand to move Open WebUI feedback into the Portkey feedback API with zero infrastructure or product changes.

Why this cookbook

Portkey is your control plane for AI observability, governance, and feedback. Open WebUI already captures thumbs-up/thumbs-down signals at the message level. When you need a fast, enterprise-friendly way to push those ratings into the Portkey feedback API, this guide hands you a no-infra approach. For additional automation options, see the Open WebUI integration overview.

What you’ll build (at a glance)

  • One-file script: Choose Python or Node to handle the entire fetch-map-post flow.
  • Manual cadence: Trigger the sync whenever leadership wants a fresh pulse.
  • Governed ingestion: Enforce consistent trace_id, value, weight, and metadata across every payload.
  • Zero changes to Open WebUI: Leverage existing export endpoints—no patches, no servers.

Architecture overview

Open WebUI  ──(GET feedback)──► Your local script ──(POST /feedback)──► Portkey
                 admin/user API         map +1/–1, add metadata            store & analyze

Data mapping for the Portkey feedback API

Portkey fieldSource in Open WebUINotes
trace_id"{chat_id}:{message_id}"Composite identifier that stays unique per message.
valuedata.ratingMap 👍 to +1, 👎 to -1. Other ratings are ignored.
weightConstant 1Keep scoring aligned for executive dashboards.
metadatadata ∪ meta (plus extras)Merge native metadata and append snapshot_chat_id, message_index, user_id, etc.

Prerequisites

  • Open WebUI access: Base URL plus a token with permission to read feedback.
  • Portkey workspace: API key for the feedback API and optional custom base URL if you self-host Portkey.

Environment variables

export OPENWEBUI_BASE_URL="https://openwebui.example.com"
export OPENWEBUI_TOKEN="OWUI_ADMIN_OR_USER_TOKEN"
export PORTKEY_API_KEY="pk_live_xxx"
# Optional for self-hosted control planes
# export PORTKEY_BASE_URL="https://cp.your-domain.com/v1"
On Windows PowerShell, set variables with:
$env:OPENWEBUI_BASE_URL="https://openwebui.example.com"
$env:OPENWEBUI_TOKEN="OWUI_ADMIN_OR_USER_TOKEN"
$env:PORTKEY_API_KEY="pk_live_xxx"
# Optional: $env:PORTKEY_BASE_URL="https://cp.your-domain.com/v1"

Quick start: manual sync without infra

pip install requests
# Save the script from the Python section as owui_to_portkey.py
python3 owui_to_portkey.py --since $(date -d '2 hours ago' +%s 2>/dev/null || python - <<'PY'
import time
print(int(time.time() - 7200))
PY
)
Dry-run first to review the payloads before sending them into the Portkey feedback API.

Single-file scripts (copy-paste ready)

#!/usr/bin/env python3
"""Manual sync from Open WebUI to Portkey feedback API."""

import argparse
import json
import os
import sys
from typing import Any, Dict, Iterable, List, Optional

import requests

OW_BASE = os.getenv("OPENWEBUI_BASE_URL", "").rstrip("/")
OW_TOKEN = os.getenv("OPENWEBUI_TOKEN", "")
PK_KEY = os.getenv("PORTKEY_API_KEY", "")
PK_BASE = os.getenv("PORTKEY_BASE_URL", "https://api.portkey.ai/v1").rstrip("/")
PK_URL = f"{PK_BASE}/feedback"


def die(message: str) -> None:
    print(message, file=sys.stderr)
    sys.exit(1)


def fetch_feedbacks(admin: bool = True) -> List[Dict[str, Any]]:
    if not OW_BASE or not OW_TOKEN:
        die("Missing OPENWEBUI_BASE_URL or OPENWEBUI_TOKEN")

    path = "/api/v1/evaluations/feedbacks/all/export" if admin else "/api/v1/evaluations/feedbacks/user"
    url = f"{OW_BASE}{path}"
    headers = {"Authorization": f"Bearer {OW_TOKEN}", "Content-Type": "application/json"}

    response = requests.get(url, headers=headers, timeout=30)
    if response.status_code == 401:
        cookie_headers = {"Cookie": f"token={OW_TOKEN}", "Content-Type": "application/json"}
        response = requests.get(url, headers=cookie_headers, timeout=30)

    response.raise_for_status()
    payload = response.json()
    if not isinstance(payload, list):
        die("Unexpected Open WebUI response format")
    return payload


def within_since(item: Dict[str, Any], since_epoch: Optional[int]) -> bool:
    if not since_epoch:
        return True
    timestamp = int(item.get("updated_at") or item.get("created_at") or 0)
    return timestamp >= since_epoch


def map_item(feedback: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    data = feedback.get("data") or {}
    meta = feedback.get("meta") or {}

    try:
        rating = int(data.get("rating"))
    except (TypeError, ValueError):
        return None

    if rating not in (1, -1):
        return None

    chat_id = meta.get("chat_id") or "unknown_chat"
    message_id = meta.get("message_id") or feedback.get("id") or "unknown_msg"
    trace_id = f"{chat_id}:{message_id}"

    metadata: Dict[str, Any] = {**data, **meta}

    snapshot_chat = (feedback.get("snapshot") or {}).get("chat")
    if isinstance(snapshot_chat, dict) and snapshot_chat.get("id"):
        metadata["snapshot_chat_id"] = snapshot_chat["id"]

    for key in ("message_index", "user_id", "version"):
        if key in feedback and feedback[key] is not None:
            metadata[key] = feedback[key]

    return {"trace_id": trace_id, "value": rating, "weight": 1, "metadata": metadata}


def post_to_portkey(item: Dict[str, Any]) -> None:
    if not PK_KEY:
        die("Missing PORTKEY_API_KEY")

    response = requests.post(
        PK_URL,
        headers={"x-portkey-api-key": PK_KEY, "Content-Type": "application/json"},
        json=item,
        timeout=30,
    )

    if not response.ok:
        print(f"[Portkey] {response.status_code} {response.text}", file=sys.stderr)
    else:
        print(f"[Portkey] OK {response.json()}")


def main() -> None:
    parser = argparse.ArgumentParser(description="Sync Open WebUI feedback to Portkey")
    parser.add_argument("--user-scope", action="store_true", help="Use per-user endpoint")
    parser.add_argument("--since", type=int, help="Only sync items updated_at >= EPOCH_SECONDS")
    parser.add_argument("--dry-run", action="store_true", help="Print mapped payloads only")
    args = parser.parse_args()

    feedbacks = fetch_feedbacks(admin=not args.user_scope)
    filtered = [item for item in feedbacks if within_since(item, args.since)]
    mapped = [mapped for item in filtered if (mapped := map_item(item))]

    print(f"Fetched {len(feedbacks)}; {len(filtered)} within --since; {len(mapped)} mapped 👍/👎.")

    if args.dry_run:
        print(json.dumps(mapped, indent=2))
        return

    for payload in mapped:
        post_to_portkey(payload)


if __name__ == "__main__":
    main()

How to run (dry-run to production)

1

Dry-run first

Preview mapped payloads before ingesting them into Portkey.
python3 owui_to_portkey.py --dry-run
node owui_to_portkey.mjs --dry-run
2

Filter by timeframe

Limit ingestion to recent ratings by passing a Unix epoch.
# Linux
python3 owui_to_portkey.py --since $(date -d '2 hours ago' +%s)
node owui_to_portkey.mjs --since=$(date -d '2 hours ago' +%s)

# macOS
python3 owui_to_portkey.py --since $(date -v-2H +%s)
node owui_to_portkey.mjs --since=$(date -v-2H +%s)
3

Ship to Portkey feedback API

Remove --dry-run to push ratings into Portkey whenever executives need updated insights.
python3 owui_to_portkey.py
node owui_to_portkey.mjs
4

Switch to user scope when needed

If your token is limited to personal feedback, add --user-scope. The mapping logic stays the same.
python3 owui_to_portkey.py --user-scope --since $(date -d '1 hour ago' +%s 2>/dev/null || python - <<'PY'
import time
print(int(time.time() - 3600))
PY
)
node owui_to_portkey.mjs --user-scope --since=$(date -d '1 hour ago' +%s 2>/dev/null || node -e "console.log(Math.floor(Date.now() / 1000) - 3600)")

Security & guardrails

  • Least privilege: Issue read-only Open WebUI tokens and rotate them regularly.
  • Secret hygiene: Set keys via environment variables—never commit them into source control.
  • Right-sized metadata: The scripts include light snapshot details (snapshot_chat_id). Trim fields if your policies require tighter scoping.
  • Idempotent reruns: The --since filter keeps repeated executions from flooding Portkey with duplicates.

Troubleshooting

The scripts automatically retry with Cookie: token=...—validate the token scope if issues persist.
Confirm that ratings exist and that your Open WebUI user has evaluation access.
Double-check PORTKEY_API_KEY and verify trace_id, value, weight, and metadata formats.
Add additional filters (for example by model_id or user_id) before posting to Portkey.

FAQ

Yes. Portkey accepts value ranges from -10 to 10. Adjust the script’s mapping once you adopt richer scales.
Absolutely. Append any key/value pairs to metadata before the payload posts to Portkey.
This cookbook is optimized for on-demand runs. Contact the Portkey team for the managed connector when you’re ready for fully automated synchronization.