Lesson 281: Construct February Fest Live Moderation Webhook Receipt on BUILD_RECEIPT (2026)
Direct answer: Before February 2027 live-week P0 Discord escalations, complete M1–M6—latch the listicle cousin receipt, lock the webhook URL outside git, pin p0_keyword_map_v1.json, send one synthetic test ping to #fest-ops, crosswalk facilitator_roster_v1.json, then file fest_moderation_webhook_receipt_v1.json and promote BUILD_RECEIPT fest_moderation_webhook_ok: true. Pair Construct webhook preflight (Guide #39), February moderation listicle (Blog), P0 webhook fix (Help #39), and cousin Lesson 267 (CSV ingest receipt).

Why this matters now (February 2027 live week)
February 2027 live week hour one: AutoMod flags refund/crash/chargeback and facilitators paste templates into #playtest, but the pager lane is silent because the #fest-ops webhook still points at last month’s deleted channel or stale AutoMod rule IDs.
This lesson is the missing half of the discipline: Lesson 267 proves your CSV ingest scope is correct; 281 proves your live escalation webhook is correct and CI-safe through BUILD_RECEIPT promotion.
Beginner path (first safe webhook session)
| Step | Action | Success check |
|---|---|---|
| 1 | Confirm listicle gates are GREEN (cousin receipt) | fest_live_moderation_ok: true on BUILD_RECEIPT |
| 2 | Lock webhook URL in env/secret (not git) | receipt stores secret ref only |
| 3 | Pin P0 keyword map | p0_keyword_map_v1.json has >= 5 P0 terms |
| 4 | Send synthetic test ping | message appears in #fest-ops < 60s |
| 5 | Crosswalk facilitator roster for today | discord_oncall + mention role valid |
| 6 | File webhook receipt + promote BUILD_RECEIPT | fest_moderation_webhook_ok: true |
Time: ~45 minutes first setup; ~90 seconds nightly when gates are pinned.
Developer path (gates M1–M6)
| Gate | Check | Fail when |
|---|---|---|
| M1 | Cousin fest_live_moderation GREEN |
fest_live_moderation_ok: false on BUILD_RECEIPT |
| M2 | Webhook URL lock outside git | URL appears in plaintext in evidence |
| M3 | p0_keyword_map_v1.json shape + rule IDs |
< 5 P0 terms or invalid/empty rule IDs |
| M4 | Test ping lands in #fest-ops |
HTTP 401/404 or message missing |
| M5 | Roster crosswalk for target fest day | discord_oncall empty or role doesn’t resolve |
| M6 | fest_moderation_webhook_receipt_v1.json + BUILD_RECEIPT latch |
receipt missing or BUILD_RECEIPT not promoted |
Cousin receipt crosswalk (what you latch vs what you promote)
| Field | Cousin / Guide | This lesson (281) |
|---|---|---|
| Schema | fest_live_moderation_receipt_v1 |
fest_moderation_webhook_receipt_v1 |
| Promotion | fest_live_moderation_ok |
fest_moderation_webhook_ok |
| Scope | templates + roster + slowmode | live escalation webhook + P0 map + test ping |
| Cousins in receipt | listicle + CSV ingest | CSV ingest: playtest_csv_ingest_receipt_v1 |
Promotion order on BUILD_RECEIPT:
fest_live_moderation_ok (M1) -> fest_moderation_webhook_ok (this lesson) -> pager lane.
M1 — latch listicle cousin receipt
jq -e '.fest_live_moderation_ok == true' release-evidence/BUILD_RECEIPT.json
jq -e '.schema == "fest_live_moderation_receipt_v1"' \
release-evidence/ops/FEST_LIVE_MODERATION_RECEIPT.json
jq -e '(.surfaces | length) >= 1' release-evidence/ops/FEST_LIVE_MODERATION_RECEIPT.json
Fail-closed: complete the February moderation listicle four-tool minimum before webhook wiring—templates without roster create false confidence.
M2 — webhook URL lock (never commit tokens)
Do not paste Discord webhook URLs into Construct project files or public exports.
- Create webhook in Discord → Integrations → Webhooks → new webhook targeting
#fest-ops - Store URL in env/secret as
DISCORD_FEST_OPS_WEBHOOK_URL - Record only the secret ref in receipt (no raw URL)
Receipt fields shape:
"webhook_secret_ref": "env:DISCORD_FEST_OPS_WEBHOOK_URL",
"webhook_channel": "#fest-ops",
"webhook_last_rotated_utc": "2027-02-20T18:00:00Z"
PowerShell sanity (local):
if (-not $env:DISCORD_FEST_OPS_WEBHOOK_URL) { throw "M2: webhook env missing" }
$env:DISCORD_FEST_OPS_WEBHOOK_URL -match '^https://discord\.com/api/webhooks/\d+/'
Red flag: webhook URL appears in release-evidence/ plaintext—rotate and rerun M4.
M3 — pin p0_keyword_map_v1.json
Pin under release-evidence/ops/p0_keyword_map_v1.json.
{
"schema": "p0_keyword_map_v1",
"fest_window": "2027-02-next-fest-live",
"terms": [
{ "keyword": "refund", "automod_rule_id": "123456789012345678", "severity": "P0" },
{ "keyword": "chargeback", "automod_rule_id": "123456789012345679", "severity": "P0" },
{ "keyword": "crash", "automod_rule_id": "123456789012345680", "severity": "P0" },
{ "keyword": "soft-lock", "automod_rule_id": "123456789012345681", "severity": "P0" },
{ "keyword": "data loss", "automod_rule_id": "123456789012345682", "severity": "P0" }
],
"case_insensitive": true,
"escalation_username": "Fest-P0-Escalation"
}
Fail-closed: AutoMod rule IDs missing → M3 fail.
Verify:
jq -e '.terms | length >= 5' release-evidence/ops/p0_keyword_map_v1.json
jq -e '.terms[] | select(.severity == "P0") | .keyword' \
release-evidence/ops/p0_keyword_map_v1.json | sort -u | wc -l
M4 — synthetic test ping (curl)
Send a synthetic payload (not production player text):
curl -sS -o /dev/null -w "%{http_code}" \
-H "Content-Type: application/json" \
-d '{
"username": "Fest-P0-Escalation",
"content": "[PREFLIGHT] P0 keyword map test — build_label=fest-demo-2027-02-rc1",
"allowed_mentions": { "parse": [] }
}' \
"$DISCORD_FEST_OPS_WEBHOOK_URL"
Pass: HTTP 204 (Discord default) or 200; message visible in #fest-ops within 60 seconds.
Capture in receipt under test_ping.
M5 — facilitator roster crosswalk (facilitator_roster_v1.json)
ROSTER="release-evidence/ops/facilitator_roster_v1.json"
jq -e --arg day "D1" \
'.facilitators[] | select(.fest_day == $day) | .discord_oncall' "$ROSTER"
Receipt crosswalk fields:
"roster_crosswalk": {
"fest_day": "D1",
"discord_oncall": "alex",
"roster_sha256": "f4e2a1…",
"mention_role_id": "987654321098765432",
"shift_utc": "2027-02-22T14:00:00Z/2027-02-22T18:00:00Z"
}
Pass: discord_oncall exists for today’s fest day and the mention role id is valid for the Discord server.
M6 — file fest_moderation_webhook_receipt_v1.json + promote BUILD_RECEIPT
Minimal receipt skeleton:
{
"schema": "fest_moderation_webhook_receipt_v1",
"fest_window": "2027-02-next-fest-live",
"build_label": "fest-demo-2027-02-rc1",
"webhook_secret_ref": "env:DISCORD_FEST_OPS_WEBHOOK_URL",
"webhook_channel": "#fest-ops",
"p0_keyword_map_path": "release-evidence/ops/p0_keyword_map_v1.json",
"roster_path": "release-evidence/ops/facilitator_roster_v1.json",
"cousin_receipts": {
"fest_live_moderation": "release-evidence/ops/FEST_LIVE_MODERATION_RECEIPT.json",
"playtest_csv_ingest": "release-evidence/playtest/PLAYTEST_CSV_INGEST_RECEIPT.json"
},
"test_ping": {
"http_status": 204,
"sent_utc": "2027-02-21T11:30:00Z",
"build_label": "fest-demo-2027-02-rc1"
},
"roster_crosswalk": {
"fest_day": "D1",
"discord_oncall": "alex",
"roster_sha256": "f4e2a1…",
"mention_role_id": "987654321098765432"
},
"fest_moderation_webhook_ok": true,
"gates": {
"M1_cousin_moderation": "pass",
"M2_webhook_url_lock": "pass",
"M3_p0_keyword_map": "pass",
"M4_test_ping": "pass",
"M5_roster_crosswalk": "pass",
"M6_receipt": "pass"
}
}
Then promote BUILD_RECEIPT (example keys from the P0 fix page):
"fest_moderation_webhook_ok": true,
"fest_mod_webhook_last_ping_utc": "2027-02-23T09:15:00Z"
Verification (fail-closed)
| Check | Pass when |
|---|---|
| Env resolves | DISCORD_FEST_OPS_WEBHOOK_URL non-empty |
| Test ping | HTTP 204/200 + message visible in #fest-ops |
| Keyword map | >= 5 P0 terms with valid rule IDs |
| Roster | discord_oncall non-empty for target fest day |
| Receipt | fest_moderation_webhook_receipt_v1.json present |
| BUILD_RECEIPT | fest_moderation_webhook_ok: true |
If webhook test pings but pager is silent, open Help #39 and follow the recovery path (stale AutoMod rule mapping vs webhook URL vs roster drift).
Common mistakes
- Latching only the listicle receipt (M1) and skipping M4 test ping.
- Committing webhook URLs into repo or HTML exports.
- Keeping old AutoMod rule IDs in
p0_keyword_map_v1.jsonafter thread churn. - Crosswalking roster for the wrong fest day (D1 vs D2).
- Assuming CSV ingest (Lesson 267) proves live webhook routing.
Next steps
- If you must patch behavior mid-fest, treat it as a webhook recovery incident and follow Help #39.
- For the next “live lane” step, complete Lesson 282 — hour-one funnel snapshot receipt so analytics and escalation do not drift.