4.1 Tenant API Keys (X-API-Key)
Section 4 — API & Game Key Management
4.1 Tenant API Keys (X-API-Key)
Tenant API Keys are issued by tenant owners or admins to allow external systems to read data from Gamers Lab. They are passed in the X-API-Key request header.
What they can do:
- Read match history
- Read leaderboards
- Read event records
- Read tenant logs
- Subscribe to streaming endpoints (if enabled)
- Access is controlled via allow/deny flags, live scope, and data profiles (field exclusion, PII masking)
- A four-stage middleware pipeline enforces: use-case → read-only → rate limit → post-response usage tracking
They cannot write game data. The only non-read methods allowed for tenant API keys are the explicit allowAuth authentication endpoints.
Security characteristics:
- Stored as
key_hash(never plaintext) - Flagged (
allow_data_api,allow_live_events,allow_active_match_data,live_events_scope) - Access shaped by profiles (
LiveEventsProfileId,HistoricalDataProfileId) that control field visibility and PII masking - Rate limited (Redis Lua-based atomic minute/hour counters)
- Can be revoked
- Can expire
Subscriber keys (subtype of Tenant API Keys): Subscriber keys are still tenant API keys, but they are issued to end users (subscribers) and tracked separately for billing and access control.
- Stored in
bus_tenant_api_keyswithcreated_by_type = 'subscriber'andcreated_by_user_id - Subscription state lives in
bus_tenant_subscriptions - Same read-only guarantees, auth POST allowlist, and flags as other tenant API keys
Key count limits:
- Default max read keys (API keys): 3 per tenant.
- Per-tenant overrides are supported. Platform admins can bypass count limits.
Read key rate limits (per key):
- Default: 60 requests/minute, 1,000 requests/hour.
- Derived from tenant/company plan. Subscriber keys use subscriber plan limits when set.
- Per-key limits are capped by the plan ceiling.
Rate limit behavior:
- Exceeding limits returns HTTP 429 with
Retry-AfterandX-RateLimit-Resetheaders. - Read rate limiting is fail-open by default, except for live/streaming paths which are fail-closed.
Key management:
- Keys can be rotated, revoked, or deleted.
- Rate limits are adjusted via plan changes, not per-key edits.
- Key names: 1–100 characters. Descriptions: up to 500 characters.
4.1.1 Key Lifecycle
A tenant API key is a read-only credential used by dashboards, analytics tools, and subscriber integrations to query game data. Unlike player JWTs or game auth keys, it carries no session and does not expire by inactivity — it is active until explicitly rotated, revoked, or deleted.
The secret value is shown once at creation or rotation. The platform stores only a SHA-256 hash of the key and never exposes the plaintext again after that single response.
Key format: Keys use the prefix sk_live_ followed by cryptographically random bytes. A short prefix (the first visible characters) is stored alongside the hash so keys can be identified in the UI without revealing the secret.
| Stored | Not stored |
|---|---|
| SHA-256 hash of the full key | Full plaintext key |
| Key prefix (for display) | — |
Key fields:
| Field | Default | Mutable | Notes |
|---|---|---|---|
name | — | Yes | Display name; 1–100 chars |
description | — | Yes | Optional context; max 500 chars |
expiresAt | null | No | Optional expiry; null = never expires |
allowDataApi | true | Yes | Access to historical data query endpoints |
allowAuth | false | Yes | Allows player JWT validation and hosted third-party auth calls |
allowLiveEvents | false | Yes | Access to SSE live event streams |
allowActiveMatchData | true | Yes | Access to active (in-progress) match data |
liveEventsScope | all | Yes | none, all, self, or team |
liveEventsProfileId | null | Yes | Profile controlling field exclusions for SSE |
historicalDataProfileId | null | Yes | Profile controlling field visibility for historical queries |
hostedAuthAppName | null | Yes | Optional display/app name for hosted third-party auth |
hostedAuthRedirectUris | [] | Yes | Allowed redirect URIs for hosted third-party auth flows |
corsAllowedOrigins | [] | Yes | Allowed browser origins for hosted auth/API-key browser use |
rateLimitPerMinute | 60 | No | Set at creation; not configurable per-key post-create |
rateLimitPerHour | 1000 | No | Set at creation; not configurable per-key post-create |
isActive | true | Yes | Can be toggled via update |
isPublic | false | No | Reserved for subscriber/public keys |
Use-case flags:
Every key has four boolean flags that gate which endpoint categories it can access. The middleware rejects requests to any category the key does not permit.
| Flag | Default | Gates access to |
|---|---|---|
allowDataApi | true | All historical data query endpoints |
allowAuth | false | Player JWT validation and hosted third-party auth endpoints (/start, /token, /refresh) |
allowLiveEvents | false | Server-Sent Events (SSE) live streams |
allowActiveMatchData | true | Active match data (in-progress matches) |
A key with all flags false would authenticate successfully but be rejected at the use-case enforcement stage for every endpoint.
Lifecycle operations: All operations require a SaaS JWT with owner or admin role on the tenant.
| Operation | Effect |
|---|---|
| Create | Generates a new key; plaintext returned once; hash stored |
| List | Returns all keys for the tenant; plaintext never included |
| Get | Returns a single key by ID; prefix and metadata only |
| Update | Updates mutable fields (name, description, flags, profiles, hosted auth/CORS settings, isActive) |
| Rotate | Generates a new secret in-place on the same key ID; old secret immediately invalidated; new plaintext returned once |
| Revoke | Soft-deactivates the key — sets isActive = false, records revokedAt and revokedBy; row is retained for audit history. Re-enabling through update clears revoked metadata. |
| Delete | Hard-deletes the key row from the database; no record remains |
Rotate vs Revoke vs Delete:
| Rotate | Revoke | Delete | |
|---|---|---|---|
| Key still works after | No — immediately | No | No |
| Row retained | Yes | Yes | No |
revokedAt set | No | Yes | — |
| New key issued | Yes — same ID | No | No |
| Use case | Periodic credential refresh | Deactivation with audit trail | Clean removal (e.g. created in error) |
Limits:
| Limit | Value |
|---|---|
| Max active keys per tenant | 3 |
| Default rate limit per minute | 60 |
| Default rate limit per hour | 1,000 |
name length | 100 chars |
description length | 500 chars |
The 3-key cap applies per tenant. Attempting to create a fourth key returns an error. Revoked keys still count toward the limit if the row is retained; use delete to free the slot.
Cache and invalidation: Validated keys are cached in Redis to avoid a database lookup on every request. Rotation and revocation both invalidate the cache entry immediately, so the old secret stops working as soon as the operation completes — there is no window where the old key remains valid after rotation.
Audit logging: All write operations (create, update, rotate, revoke, delete) are recorded in the tenant audit log. Tenant-owned key rotation is an in-place secret replacement on the same key ID. Subscriber key rotation creates a replacement row and links it to the previous row via rotatedFromId.
Limitations:
- Rate limits are fixed at creation.
rateLimitPerMinuteandrateLimitPerHourare not updatable after creation. To change rate limits, rotate or recreate the key. - Plaintext is shown once. If the key value is lost after creation or rotation, the only recovery is to rotate the key and store the new value.
expiresAtis stored but expiry is checked at validation time — there is no background job that auto-revokes expired keys.
Code references:
- Controller:
GamersLabRestAPI/Saas/ApiKeys/Controllers/ApiKeysController.cs - Service:
GamersLabRestAPI/Saas/ApiKeys/Services/ApiKey/ApiKeyService.cs - Key generator:
GamersLabRestAPI/Saas/ApiKeys/Services/ApiKey/ApiKeyGenerator.cs - Repository:
GamersLabRestAPI/Saas/ApiKeys/Repositories/NpgsqlApiKeyRepository.cs - Model:
GamersLabRestAPI/Saas/ApiKeys/Models/TenantApiKey.cs - Auth handler:
GamersLabRestAPI/Saas/ApiKeys/Services/ApiKey/ApiKeyAuthenticationHandler.cs
4.1.2 Live Events Profiles
A live events profile is a JSON configuration that controls which events are streamed and how they are presented to a caller connected to a Server-Sent Events (SSE) endpoint. Every SSE connection resolves the effective profile for the requesting API key and applies its rules to each outbound event before it is written to the stream.
The API key must have allowLiveEvents = true to access SSE stream endpoints. The profile is referenced via liveEventsProfileId on the key.
Profiles are stored in bus_live_events_profiles. Each row holds the profile name, description, a profile_json blob containing the full configuration, and an is_system flag that protects platform-defined profiles from modification or deletion.
System profiles: Four system profiles are pre-defined and available to all tenants. They cannot be updated or deleted.
| Profile | ID suffix | Default behavior |
|---|---|---|
| All | …0101 | Full visibility — all stream types delivered to all callers |
| Team | …0102 | Match-scope types (matchStart, matchEnd, matchResults, matchEvents) use team visibility; player-scope types (playerEvents, sessionLogins, sessionLogouts) use self visibility |
| Self | …0103 | All stream types restricted to the authenticated player's own data only |
| None | …0104 | No events streamed — all types blocked |
When no profile is configured for an API key and no tenant default is set, the platform selects the fallback system profile based on the API key's liveEventsScope setting (all, team, self, or none). This differs from historical data profiles, which always fall back to All.
Profile configuration fields: Every profile JSON object must include version (must be 1) and enabled (bool). All other fields are optional.
| Field | Type | Description |
|---|---|---|
version | int | Must be 1. Required. |
enabled | bool | If false, no events are streamed regardless of other settings. Required. |
visibilityByType | { streamType: visibilityLevel } | Per-type visibility overrides. Omitted types default to all. |
delaysSeconds | { streamType: int } | Seconds to delay delivery per stream type. Must be ≥ 0. Omitted types have no delay. |
identityRedaction | object | Global identity redaction applied to every streamed event. |
identityRedactionByType | { streamType: redactionObject } | Per-type identity redaction; merged with global. |
includeHostByType | [ streamType ] | Stream types for which the match host's data is always included. |
overrides | object | Event allow/deny filter lists. |
Stream types: The 7 recognized stream type keys:
| Key | Description |
|---|---|
matchStart | Emitted when a match transitions to started state |
matchEnd | Emitted when a match transitions to ended state |
matchResults | Match outcome records published at match end |
matchEvents | In-match game events as they are recorded |
playerEvents | Player-scoped game events |
sessionLogins | Player session login events |
sessionLogouts | Player session logout events |
Visibility levels: Used in visibilityByType. Stream types not listed in the map default to all.
| Value | Meaning |
|---|---|
none | Stream type is completely blocked — no events of this type are sent |
all | Events delivered regardless of caller identity |
match | Caller must be an authenticated participant in the match |
registered | Caller must be a registered (authenticated) player |
team | Caller must share the same team as the event subject |
self | Caller must be the specific player the event belongs to |
host | Only the match host receives events of this type |
Levels that require a player identity (self, team, match, registered, host) require the SSE connection to carry a valid player token. Connections using only an X-API-Key without a player token will not receive those stream types.
Delivery delays: delaysSeconds is a { streamType: int } map. When set for a stream type, the platform holds matching events for the specified number of seconds before writing them to the SSE stream. Values must be ≥ 0; negative values are rejected at save time.
Identity redaction: Identity redaction strips identifying fields from streamed event payloads.
identityRedaction— applies globally to every stream type.identityRedactionByType— applies per stream type; merged with the global setting. Redaction can only be made more restrictive by overrides, never relaxed.
The four redactable fields for live events:
| Field | What it hides |
|---|---|
playerId | The player's platform UUID |
matchPlayerId | The player's match-scoped UUID |
teamId | The team UUID |
teamLabel | The human-readable team name |
Live events identity redaction has a smaller surface than historical data profiles (4 fields vs 8) — session and device identifiers are not present in SSE payloads.
Event override lists: The overrides object controls which events are included in event-carrying stream types (matchEvents, playerEvents). All lists are case-insensitive and deduplicated on save.
| Field | Effect |
|---|---|
allowEventKeys | Include only events whose eventKey is in this list. An empty or absent list means all keys are allowed. |
denyEventKeys | Exclude events whose eventKey is in this list. Applied after allowEventKeys. |
allowEventCategories | Include only events whose definition category is in this list. |
denyEventCategories | Exclude events whose definition category is in this list. |
allowEventValues | Include only events whose eventValue is in this list. |
denyEventValues | Exclude events whose eventValue is in this list. |
Allow lists act as inclusive filters (only matching records pass). Deny lists are always evaluated after allow lists and always exclude matching records regardless of other settings.
Profile resolution order: For each SSE connection, the platform resolves the effective profile in this order:
- The
liveEventsProfileIdon the API key (if set). - The tenant's default live events profile (stored in
bus_tenants.default_live_events_profile_id). - The scope-mapped system profile based on the API key's
liveEventsScope:none→ None,self→ Self,team→ Team,all(or unset) → All.
If the resolved profile row cannot be found (e.g. deleted after assignment), the platform logs a warning and falls back to the scope-mapped system profile.
Override merging for subscriber keys: Subscriber and public API keys can carry additional profile overrides in their metadata (ApiKeyProfileOverrides.LiveEvents). When present, these overrides are merged on top of the resolved base profile:
- Overrides can only make visibility more restrictive, never more permissive.
- Identity redaction fields can only be enabled by an override, not disabled.
- Invalid override entries are logged as warnings and skipped.
Available operations: All operations require a SaaS JWT with owner or admin role on the tenant.
| Operation | Notes |
|---|---|
| List | Returns all profiles including the 4 system profiles; flags isDefault on the tenant's current default |
| Get | Returns a single profile by ID; 404 if not found or not scoped to this tenant |
| Create | Creates a new tenant profile; version and enabled required; all stream type keys, visibility values, and delay values are validated |
| Update | Updates name, description, and/or profile JSON; system profiles cannot be updated |
| Delete | Hard-deletes the profile row; system profiles cannot be deleted; all active API key caches for the tenant are invalidated immediately |
| Set Default | Sets the tenant's default profile; applies to all API keys that have no explicit profile assigned |
Caching: Two Redis cache entries are maintained per profile resolution (5-minute TTL each): live-events-profile:effective:{apiKeyId} and live-events-profile:tenant-default:{tenantId}. Both are invalidated on create, update, delete, and set-default operations.
Limitations:
- System profiles are immutable.
- Fallback depends on the key's scope setting — unlike historical data profiles (which always fall back to All), live events profiles fall back to the scope-mapped system profile.
- Version must be
1. - Delay cannot be negative.
- 4 redactable fields only — session, device, and IP identifiers are not available in SSE payloads.
- Overrides can only restrict; subscriber key overrides cannot relax a base profile.
- 5-minute cache lag.
Code references:
- Controller:
GamersLabRestAPI/Saas/ApiKeys/Controllers/LiveEventsProfilesController.cs - Service:
GamersLabRestAPI/Saas/ApiKeys/Services/LiveEventsProfileService.cs - Model:
GamersLabRestAPI/Saas/ApiKeys/Models/LiveEventsProfile.cs - Stream types:
GamersLabRestAPI/Saas/Business/Constants/LiveEventStreamTypes.cs - Live events evaluator:
GamersLabRestAPI/Game/Data/Services/LiveEventsProfileEvaluator.cs - Override evaluator:
GamersLabRestAPI/Saas/ApiKeys/Services/ProfileOverrideEvaluator.cs
4.1.3 Historical Data Profiles
A historical data profile is a JSON configuration attached to a tenant that controls which fields and data types are visible when a caller queries historical game data through read endpoints (e.g. match details, player stats, event records). Every historical data query resolves the effective profile for the requesting API key and applies its rules before returning data.
The API key must have allowDataApi = true to access historical data query endpoints. The profile is referenced via historicalDataProfileId on the key.
Profiles are stored in bus_historical_data_profiles. Each row holds the profile name, description, a profile_json blob, and an is_system flag that protects platform-defined profiles from modification or deletion.
System profiles: Four system profiles are pre-defined and available to all tenants. They cannot be updated or deleted.
| Profile | ID suffix | Default behavior |
|---|---|---|
| All | …0201 | Full visibility — all fields returned for all data types |
| Team | …0202 | Restricts visibility to team-level — a caller sees their own team's data and aggregated match data |
| Self | …0203 | Restricts visibility to the authenticated player's own data only |
| None | …0204 | No data returned — all types blocked |
When no profile is configured for an API key and no tenant default is set, the platform falls back to the All system profile.
Profile configuration fields: Every profile JSON object must include version (must be 1) and enabled (bool). All other fields are optional.
| Field | Type | Description |
|---|---|---|
version | int | Must be 1. Required. |
enabled | bool | If false, the profile returns none for every data type regardless of other settings. Required. |
visibilityByType | { dataType: visibilityLevel } | Per-type visibility overrides. Omitted types default to all. |
identityRedaction | object | Global identity redaction applied to every response. |
identityRedactionByType | { dataType: redactionObject } | Per-type identity redaction; merged with global. |
includeHostByType | [ dataType ] | Data types for which the host player's data is included even when visibility would normally exclude it. |
overrides | object | Event allow/deny filter lists. |
historicalOptions | object | Additional options; currently delayTillMatchCompletedByType. |
Data types: The 14 recognized data type keys:
| Key | Description |
|---|---|
matchList | Paginated list of matches |
matchDetail | Full match metadata and result |
matchPlayers | Player roster within a match |
matchResults | Match outcome records |
matchEvents | In-match event records |
playerEvents | Player-scoped event records |
sessionLogins | Session login records |
sessionLogouts | Session logout records |
timeline | Chronological match/player/session timeline. V2 is cursor-paged and omits device, platform, client version, and IP fields. |
playerSummary | Aggregated player statistics |
playerMatches | A player's match history |
playerLookup | Player identity resolution |
playerStats | Detailed player stats |
leaderboards | Ranked leaderboard results |
Visibility levels: Used in visibilityByType. Types not listed in the map default to all.
| Value | Meaning |
|---|---|
none | Data type is completely blocked — endpoint returns no records for this type |
all | Full data returned regardless of caller identity |
match | Caller must be an authenticated participant in the match |
registered | Caller must be a registered (authenticated) player |
team | Caller must share the same team as the subject |
self | Caller must be the specific player whose data is requested |
host | Only the match host can access this data |
Levels that require a player identity require the request to carry a valid player token. Requests using only an X-API-Key without a player token will be denied access to those data types.
Identity redaction: Removes or hashes identifying fields from responses.
identityRedaction— applies globally to every data type.identityRedactionByType— applies per data type; merged with the global setting. Redaction can only be made more restrictive by overrides, never relaxed.
The eight redactable fields:
| Field | What it hides |
|---|---|
playerId | The player's platform UUID |
matchPlayerId | The player's match-scoped UUID |
teamId | The team UUID |
teamLabel | The human-readable team name |
loginSessionId | The session UUID |
sessionId | Internal session tracking ID |
deviceId | Device fingerprint |
ipAddress | Client IP address |
Event override lists: The overrides object controls which event records appear in event-returning data types (matchEvents, playerEvents). Same allow/deny list semantics as live events profiles — allow lists act as inclusive filters; deny lists are evaluated after allow lists.
Delay until match completed: historicalOptions.delayTillMatchCompletedByType is a { dataType: bool } map. When set to true for a data type, records of that type are withheld until the match is in a completed state.
Include host by type: includeHostByType is an array of data type keys. For listed types, the match host's data is always returned even when the visibility level would otherwise exclude it.
Profile resolution order:
- The
historicalDataProfileIdon the API key (if set). - The tenant's default profile (stored in
bus_tenants.default_historical_data_profile_id). - The All system profile (open access, no restrictions).
If the resolved profile row cannot be found, the platform logs a warning and falls back to the All system profile.
Override merging for subscriber keys: Same merging rules as live events profiles — overrides can only restrict, not relax. Invalid entries are logged and skipped.
Available operations: All operations require a SaaS JWT with owner or admin role on the tenant.
| Operation | Notes |
|---|---|
| List | Returns all profiles including the 4 system profiles; flags isDefault on the tenant's current default |
| Get | Returns a single profile by ID; 404 if not found or not scoped to this tenant |
| Create | Creates a new tenant profile; version and enabled required; all type keys and visibility values are validated |
| Update | Updates name, description, and/or profile JSON; system profiles cannot be updated |
| Delete | Hard-deletes the profile row; system profiles cannot be deleted; all active API key caches for the tenant are invalidated immediately |
| Set Default | Sets the tenant's default profile; applies to all API keys that have no explicit profile assigned |
Caching: Two Redis cache entries (5-minute TTL each): historical-data-profile:effective:{apiKeyId} and historical-data-profile:tenant-default:{tenantId}. Both invalidated on create, update, delete, and set-default.
Limitations:
- System profiles are immutable.
- Fallback to All on missing profiles — if an API key references a deleted profile, the platform silently falls back to All. No error is returned to the caller.
- Version must be
1. - Overrides can only restrict.
- No per-field visibility — visibility is controlled at the data-type level, not individual JSON field level. Use identity redaction for identity fields, event override lists for event records.
- 5-minute cache lag.
Code references:
- Controller:
GamersLabRestAPI/Saas/ApiKeys/Controllers/HistoricalDataProfilesController.cs - Service:
GamersLabRestAPI/Saas/ApiKeys/Services/HistoricalDataProfileService.cs - Model:
GamersLabRestAPI/Saas/ApiKeys/Models/HistoricalDataProfile.cs - Data types:
GamersLabRestAPI/Core/Constants/HistoricalDataTypes.cs - Historical evaluator:
GamersLabRestAPI/Game/Data/Services/HistoricalDataProfileEvaluator.cs
Profile Type Comparison
Both profile systems share the same architecture and operations. The differences are in scope, type vocabulary, and a few feature-specific fields.
| Historical Data Profiles | Live Events Profiles | |
|---|---|---|
| DB table | bus_historical_data_profiles | bus_live_events_profiles |
| Applies to | Read/query endpoints | SSE live event streams |
| Key flag required | allowDataApi | allowLiveEvents |
| Key field | historicalDataProfileId | liveEventsProfileId |
| Type vocabulary | 14 data types | 7 stream types |
Feature comparison:
| Feature | Historical | Live Events |
|---|---|---|
version required | Yes — must be 1 | Yes — must be 1 |
enabled required | Yes | Yes |
visibilityByType | Yes — 14 data types | Yes — 7 stream types |
| Identity redaction (global) | Yes — 8 fields | Yes — 4 fields |
| Identity redaction (per type) | Yes | Yes |
includeHostByType | Yes | Yes |
| Event allow/deny override lists | Yes | Yes |
| Delivery delay | delayTillMatchCompletedByType (bool per type) | delaysSeconds (int seconds per type, ≥ 0) |
| Scope-aware fallback | No — always falls back to All | Yes — falls back to system profile matching liveEventsScope on the key |
| Subscriber key overrides | Yes | Yes |
| Override merging rules | More restrictive only | More restrictive only |
Identity redaction field coverage:
Historical data profiles expose more redactable fields because query responses include session and device data that SSE payloads do not.
| Field | Historical | Live Events |
|---|---|---|
playerId | Yes | Yes |
matchPlayerId | Yes | Yes |
teamId | Yes | Yes |
teamLabel | Yes | Yes |
loginSessionId | Yes | — |
sessionId | Yes | — |
deviceId | Yes | — |
ipAddress | Yes | — |
System profile IDs:
| Profile | Historical ID suffix | Live Events ID suffix |
|---|---|---|
| All | …0201 | …0101 |
| Team | …0202 | …0102 |
| Self | …0203 | …0103 |
| None | …0204 | …0104 |
Both use a 5-minute Redis TTL. Cache keys follow the same pattern with different prefixes. Operations (List, Get, Create, Update, Delete, Set Default) are identical for both and require a SaaS JWT with owner or admin role.
4.1.4 Rate Limiting
Per-key quota enforcement using Redis Lua scripts for atomic increment and check. Minute and hour counters with configurable limits. Returns 429 with Retry-After and X-RateLimit-Reset headers on exceeded limits.
Read key rate limits (per key):
- Default: 60 requests/minute, 1,000 requests/hour.
- Derived from tenant/company plan. Subscriber keys use subscriber plan limits when set.
- Per-key limits are capped by the plan ceiling.
Rate limit behavior:
- Exceeding limits returns HTTP 429 with
Retry-AfterandX-RateLimit-Resetheaders. - Read rate limiting is fail-open by default, except for live/streaming paths which are fail-closed.
- Rate limits are adjusted via plan changes, not per-key edits.
4.1.5 Usage Tracking & Analytics
Every request authenticated with an X-API-Key is tracked by ApiKeyUsageTrackingMiddleware. Recording happens post-response — after the full pipeline has executed and the response has been written — so it has no impact on response latency from the caller's perspective.
Fields recorded per request to bus_api_key_usage:
| Field | Description |
|---|---|
endpoint | Route template (e.g. api/bus_tenants/{tenantId}/matches/{id}) |
path | Actual request path |
method | HTTP method |
request_kind | read (GET/HEAD/OPTIONS) or write (all others) |
status_code | HTTP response status code |
response_time_ms | Elapsed milliseconds from request start to response completion |
request_bytes | Content-Length of the request body (if present) |
response_bytes | Content-Length of the response body (if present) |
ip_address | Client IP address |
user_agent | User-Agent header value |
is_throttled | true if the request was rate-limited (429) |
quota_bucket | Which quota bucket the request was charged against |
billing_outcome | How the request was classified for billing |
billable_units | Number of units charged (0 for non-accepted outcomes) |
billed_workspace_id | Company or subscriber workspace the usage is attributed to |
occurred_at | UTC timestamp of the request |
Tracking can be suppressed for specific endpoints by setting HttpContextItemKeys.SkipApiKeyUsageTracking = true in HttpContext.Items.
Billing classification: Each recorded request is assigned a quota bucket and a billing outcome by UsageBillingClassifier.
Quota buckets:
| Bucket | When used |
|---|---|
tenant_read_requests | Standard X-API-Key GET/query requests |
subscriber_read_requests | Requests made by subscriber or public keys |
tenant_write_requests | Game key write/ingest requests |
live_connect_credits | SSE live event connection establishment |
live_connection_minutes | SSE live event connection duration |
Billing outcomes:
| Outcome | Meaning | Billable units |
|---|---|---|
accepted | Request succeeded (2xx/3xx) | ≥ 1 |
rejected | Request failed (4xx/5xx, not throttled, not malformed) | 0 |
denied | Request was rate-limited (429) | 0 |
malformed | Request was a 400 Bad Request | 0 |
idempotent_replay | Request was a duplicate of a previously processed write | 0 |
Only accepted requests consume billable units.
Analytics queries: All analytics endpoints require a SaaS JWT with owner or admin role on the tenant. Default window: last 30 days. Pagination: default 100, max 500.
Per-key analytics response includes:
| Section | Contents |
|---|---|
statistics | Total requests, requests in last 24h / 7d / 30d, average response time, success/failure counts, first and last used timestamps |
endpointUsage | Per-endpoint breakdown: request count, average response time, success and error counts |
hourlyUsage | Time series: request count, average response time, error count — one bucket per hour for the last 24 hours |
dailyUsage | Time series: request count, average response time, error count — one bucket per day for the last 30 days |
statusCodeDistribution | Map of HTTP status code → request count |
topIpAddresses | Ranked list of calling IPs: request count, last seen timestamp |
Tenant-wide analytics response includes:
| Section | Contents |
|---|---|
overallStatistics | Same fields as per-key statistics, aggregated across all keys |
apiKeyUsage | Per-key summary: key name, prefix, total requests, requests last 24h, last used, active status |
dailyTrend | Daily time series for the tenant as a whole |
Daily usage returns a flat day-by-day breakdown (default last 90 days), optionally filtered to a single key:
| Field | Description |
|---|---|
date | UTC day |
readCount | Number of read (GET) requests |
writeCount | Number of write requests |
readBytes | Total request body bytes for reads |
writeBytes | Total request body bytes for writes |
Limitations:
- Post-response only. If the process crashes after writing the response but before the tracking call completes, that request's record is lost. Usage records are best-effort, not transactional.
response_bytesdepends onContent-Length. Streamed or chunked responses without aContent-Lengthheader will have a nullresponse_bytesvalue.rows_returnedis not auto-populated. The field exists but is only set by controllers that explicitly write it toHttpContext.Items.- No raw log access. The analytics endpoints return pre-aggregated views. Individual request records in
bus_api_key_usageare not exposed through the API.
Code references:
- Middleware:
GamersLabRestAPI/Saas/ApiKeys/Middleware/ApiKeyUsageTrackingMiddleware.cs - Billing classifier:
GamersLabRestAPI/Saas/Quota/Services/UsageBillingClassifier.cs - Analytics controller:
GamersLabRestAPI/Saas/ApiKeys/Controllers/ApiKeyAnalyticsController.cs - Analytics service:
GamersLabRestAPI/Saas/ApiKeys/Services/UsageAnalyticsService.cs - Usage model:
GamersLabRestAPI/Saas/ApiKeys/Models/ApiKeyUsage.cs
4.1.6 Middleware Pipeline
[NO DEDICATED DOC]
The API key middleware pipeline enforces three inbound stages for every X-API-Key request, then records usage after the response is produced:
-
Use-case enforcement — checks the key's boolean flags (
allowDataApi,allowAuth,allowLiveEvents,allowActiveMatchData) against the requested endpoint's required use-case. A key that is missing the required flag is rejected even if it is otherwise valid and active. -
Read-only enforcement — rejects non-read HTTP methods (POST, PUT, PATCH, DELETE) from a tenant API key except for the explicit hosted-auth allowlist: player JWT validation and third-party auth start/token/refresh endpoints. Those auth endpoints still require
allowAuth = true. -
Rate limit enforcement — checks the per-key Redis Lua atomic counters for the minute and hour windows. Returns 429 with
Retry-AfterandX-RateLimit-Resetheaders if the limit is exceeded. Fail-open by default (except live/streaming paths which are fail-closed). -
Post-response usage tracking — after the response has been written, records the request to
bus_api_key_usagewith billing classification. This stage runs post-response and has no effect on response latency.
The pipeline is implemented as ASP.NET middleware components. HttpContext.Items is used to pass state between stages (quota bucket, billing outcome, skip-tracking flag).
Code references:
- Auth handler:
GamersLabRestAPI/Saas/ApiKeys/Services/ApiKey/ApiKeyAuthenticationHandler.cs - Usage tracking middleware:
GamersLabRestAPI/Saas/ApiKeys/Middleware/ApiKeyUsageTrackingMiddleware.cs - Middleware helper:
GamersLabRestAPI/Core/Middleware/UsageTrackingMiddlewareHelper.cs