Event Webhooks and Custom Commands¶
This guide helps you build an endpoint that:
- verifies PRC Event Webhook signatures safely
- receives in-game custom commands starting with
; - routes commands to your own app handlers
Reference: ER:LC API docs
For command-specific alias, middleware, predicate, and unknown-handler routing, prefer Custom Commands Reference. This page shows the lower-level mixed event router.
1. Install webhook support¶
2. Build a secure FastAPI endpoint¶
from fastapi import FastAPI, HTTPException, Request
from erlc_api.webhooks import (
EventWebhookRouter,
WebhookEventType,
assert_valid_event_webhook_signature,
)
app = FastAPI()
router = EventWebhookRouter(command_prefix=";")
@router.on_command("ping")
def handle_ping(command, event):
return {"reply": "pong", "args": list(command.args)}
@router.on_command("warn")
async def handle_warn(command, event):
if not command.args:
return {"ok": False, "error": "usage: ;warn <player> <reason>"}
return {"ok": True, "target": command.args[0], "reason": " ".join(command.args[1:])}
@router.on_emergency_call()
def handle_emergency(event):
data = event.emergency_call or {}
return {"team": data.get("Team"), "caller": data.get("Caller")}
@router.on_unknown()
def handle_unknown(event):
return {"ok": False, "event_type": event.event_type}
@app.post("/erlc/events")
async def erlc_event_webhook(request: Request):
raw_body = await request.body()
try:
assert_valid_event_webhook_signature(
raw_body=raw_body,
headers=request.headers,
max_skew_s=300, # set None to disable skew check
)
except Exception as exc:
raise HTTPException(status_code=401, detail=str(exc)) from exc
payload = await request.json()
results = await router.dispatch(payload)
return {"handled_results": results}
3. Verification rules you must keep¶
- Always verify using raw bytes from
await request.body(). - Signature input is
timestamp + raw_bodywith no separator. - Read both required headers:
X-Signature-TimestampandX-Signature-Ed25519. - Return non-2xx for invalid signatures.
4. Useful helper functions¶
verify_event_webhook_signature(...)returnsTrue/False.assert_valid_event_webhook_signature(...)raises typed errors.decode_event_webhook_payload(...)normalizes event shape using explicit type fields plus fallback heuristics.parse_custom_command_text(";ban \"Player One\" rdm")parses command name and args.
5. Common beginner mistakes¶
- Parsing JSON first and then verifying a re-serialized body.
- Forgetting to install
.[webhooks]for Ed25519 verification. - Assuming a fixed webhook payload schema; keep unknown fields and code defensively.
- Returning
2xxon failed signature checks.
6. Next improvements you can add¶
- Add role/permission checks before executing command handlers.
- Add idempotency keys for repeated webhook deliveries.
- Log command handler outcomes to your moderation audit tables.
- Use
erlc_api.cache.AsyncCachedClientfor read-only lookups inside handlers. - Use
erlc_api.diagnosticsanderlc_api.discord_toolsfor readable handler failures.
Example read-only cache inside a handler:
from erlc_api.cache import AsyncCachedClient
cached_api = AsyncCachedClient(api, ttl_s=5)
@router.on_command("players")
async def handle_players(command, event):
players = await cached_api.players()
return {"players": [player.name for player in players]}
Keep command execution explicit through the original client and protect it with
your own auth/predicate checks plus CommandPolicy:
from erlc_api import CommandPolicy, cmd
announce_policy = CommandPolicy(allowed={"h"}, max_length=120)
safe_command = announce_policy.validate(cmd.h("hello"))
result = await api.command(safe_command)
Related Pages¶
- Earlier in the guide: Webhooks Reference
- Workflow Utilities Reference
- Next in the guide: Custom Commands Reference
Previous Page: Webhooks Reference | Next Page: Custom Commands Reference