Webhook Payloads Full Closed Beta
Complete payload schema reference for loyalty egress webhook deliveries. For a guided introduction, see the Webhooks guide and Resolution Modes.
Closed Beta
Webhook payloads are part of the Closed Beta loyalty integration. Schemas described here are subject to change. Contact us for access.
Delivery Format
Every webhook delivery is an HTTP POST with:
- Content-Type:
application/json - Body: A single JSON object matching one of the three resolution mode schemas below
- Headers: See Webhook Headers
Delivery Headers
| Header | Type | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
X-FanFest-Signature | t=<unix_ts>,v1=<hex_hmac> | HMAC-SHA256 signature (see verification) |
X-FanFest-Timestamp | string (Unix seconds) | When the signature was generated |
X-FanFest-Event-Id | string (UUID) | Unique event ID for idempotency |
TypeScript Interfaces
Union Type
typescript
type EgressPayload =
| AggregatedEgressPayload
| DayAggregatedEgressPayload
| HighFidelityEgressPayload;All three interfaces share the resolution field as a discriminator. Use it to narrow the type:
typescript
function handleWebhook(payload: EgressPayload): void {
switch (payload.resolution) {
case 'aggregated':
console.log(`Total points: ${payload.total_points}`);
break;
case 'day_aggregated':
console.log(`Points on ${payload.date}: ${payload.points}`);
break;
case 'high_fidelity':
console.log(`Action ${payload.user_action_id}: ${payload.points} pts`);
break;
}
}AggregatedEgressPayload
All-time totals per user, per channel, per action.
typescript
interface AggregatedEgressPayload {
/** Discriminator — always "aggregated" */
resolution: 'aggregated';
/** FanFest channel ID */
channel_id: string;
/** User who performed the action */
user_id: string;
/** Action type (e.g., "quiz_answer", "poll_vote") */
app_action_name: string;
/** Sum of all settled points for this user + channel + action (all time) */
total_points: number;
/** Total number of times this action was performed */
total_occurrences: number;
/** Community IDs associated with the triggering action */
community_ids: string[];
/** ISO 8601 timestamp of when the payload was generated */
timestamp: string;
}Example payload:
json
{
"resolution": "aggregated",
"channel_id": "ch_abc123",
"user_id": "usr_xyz789",
"app_action_name": "quiz_answer",
"total_points": 1250,
"total_occurrences": 47,
"community_ids": ["com_111", "com_222"],
"timestamp": "2025-06-15T14:32:00.000Z"
}DayAggregatedEgressPayload
Daily bucketed totals per user, per channel, per action.
typescript
interface DayAggregatedEgressPayload {
/** Discriminator — always "day_aggregated" */
resolution: 'day_aggregated';
/** FanFest channel ID */
channel_id: string;
/** User who performed the action */
user_id: string;
/** Action type */
app_action_name: string;
/** UTC date in YYYY-MM-DD format */
date: string;
/** Sum of settled points for this user + channel + action on this UTC day */
points: number;
/** Number of times this action was performed on this UTC day */
occurrences: number;
/** Community IDs associated with the triggering action */
community_ids: string[];
}Example payload:
json
{
"resolution": "day_aggregated",
"channel_id": "ch_abc123",
"user_id": "usr_xyz789",
"app_action_name": "quiz_answer",
"date": "2025-06-15",
"points": 75,
"occurrences": 3,
"community_ids": ["com_111"]
}HighFidelityEgressPayload
Individual action records with full detail.
typescript
interface HighFidelityEgressPayload {
/** Discriminator — always "high_fidelity" */
resolution: 'high_fidelity';
/** FanFest channel ID */
channel_id: string;
/** User who performed the action */
user_id: string;
/** Unique identifier for this specific action record */
user_action_id: string;
/** Action type */
app_action_name: string;
/** Points awarded for this specific action (sum of settled transactions) */
points: number;
/** Occurrences recorded for this action (typically 1) */
occurrences: number;
/** Community IDs associated with this action */
community_ids: string[];
/** ISO 8601 timestamp of when the action was originally recorded */
created_at: string;
}Example payload:
json
{
"resolution": "high_fidelity",
"channel_id": "ch_abc123",
"user_id": "usr_xyz789",
"user_action_id": "ua_def456",
"app_action_name": "quiz_answer",
"points": 25,
"occurrences": 1,
"community_ids": ["com_111", "com_222"],
"created_at": "2025-06-15T14:32:00.000Z"
}Field Reference
Common Fields (All Modes)
| Field | Type | Description |
|---|---|---|
resolution | string | Discriminator: "aggregated", "day_aggregated", or "high_fidelity" |
channel_id | string | The FanFest channel ID |
user_id | string | The user who performed the loyalty action |
app_action_name | string | The action type identifier (e.g., "quiz_answer", "poll_vote", "referral", "check_in") |
community_ids | string[] | Community IDs associated with the action |
Aggregated-Only Fields
| Field | Type | Description |
|---|---|---|
total_points | number | Sum of all settled points for this user + channel + action (all time) |
total_occurrences | number | Total number of times this action was performed |
timestamp | string | ISO 8601 — when the payload was generated (not when the action occurred) |
Day Aggregated-Only Fields
| Field | Type | Description |
|---|---|---|
date | string | UTC date in YYYY-MM-DD format |
points | number | Sum of settled points for this day |
occurrences | number | Number of occurrences for this day |
High Fidelity-Only Fields
| Field | Type | Description |
|---|---|---|
user_action_id | string | Unique ID for this action record — use for deduplication |
points | number | Points for this specific action |
occurrences | number | Occurrences for this specific action (typically 1) |
created_at | string | ISO 8601 — when the action was originally recorded |
Validation Notes
- All
pointsfields include only settled transactions. On-hold transactions are excluded. total_pointsandpointsare integers (no fractional points).community_idsmay be an empty array if the action has no community associations.app_action_nameis never null in webhook payloads — if the underlying data has no action name, it is delivered as an empty string.timestamp(aggregated mode) andcreated_at(high fidelity mode) are always valid ISO 8601 strings with timezone designator (Z).date(day aggregated mode) is always inYYYY-MM-DDformat using UTC.
