{"openapi":"3.0.3","info":{"title":"IRLEvents API","version":"2.65.0","description":"Public REST API for IRLEvents — token-gated event platform with multi-chain NFT/token verification. Designed to be agent-friendly: every endpoint that accepts user JWTs also accepts long-lived `api_*` keys.\n\n### Authentication for users (browsers, mobile)\nMost write endpoints require a JWT Bearer token obtained via wallet signature, magic link, or OAuth.\nPass it as `Authorization: Bearer <jwt>`.\n\n### Authentication for agents / integrations (`api_*` keys)\nMint a key in the dashboard (Profile → API Keys) — format `api_<64 hex chars>`. Pass exactly the same way: `Authorization: Bearer api_...`. Keys are hashed at rest, can be revoked at any time, support optional expiration, and per-key rate limits. **Sensitive routes** (key management, billing, 2FA, OAuth unlink, account delete) only accept user JWTs — api keys are rejected there even if otherwise valid.\n\n### API key scopes (v2.56.0+)\nEvery new api key is granted a subset of these scopes; the API enforces them on each request. Default for new keys: `profile:read`, `events:read`, `rsvp:write` (a sensible 'personal assistant' set).\n\n- `profile:read` — read profile, wallets, cached on-chain assets\n- `profile:write` — trigger asset re-sync (slow; hits external NFT providers)\n- `events:read` — read events, eligibility, rsvp status, trending, leaderboards, stats\n- `events:write` — host actions: create/edit/delete events, check-in attendees\n- `rsvp:write` — RSVP and cancel RSVPs\n- `rewards:read` — read IRLRewards: points balance, claim history, rewards catalog, eligibility\n- `rewards:write` — claim rewards on the user's behalf, cancel pending claims\n\nInsufficient-scope responses return 403 with `{ code: 'INSUFFICIENT_SCOPE', required: [...], granted: [...] }`. Keys minted before scopes shipped have empty `scopes` and retain full access for back-compat.\n\n### Rate limits\nEndpoints are rate-limited per IP and per user. Chat send: 20/min + 1.5s cooldown. DMs: 5/hr per sender→recipient pair. API keys: 1000 req/hr default (configurable per key). Most other endpoints: generous defaults.\n\n### Data model notes\nWallet addresses are the canonical user identifiers. EVM addresses are lowercased; Solana/Bitcoin remain case-sensitive.","contact":{"name":"IRLEvents Support","url":"https://irlevents.io/contact"}},"servers":[{"url":"https://irlevents.io","description":"Production"},{"url":"https://dev.irlevents.io","description":"Dev/staging"}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"User JWT obtained via wallet signature, magic link, or OAuth. Required for sensitive operations (key management, billing, 2FA)."},"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"api_*","description":"Long-lived API key for agents and integrations. Mint from the dashboard. Same `Authorization: Bearer ...` header as JWT. Format: `api_<64 hex>`. Cannot access sensitive routes (key management, billing, 2FA, OAuth unlink). Each key carries a list of granted scopes (`profile:read`, `profile:write`, `events:read`, `events:write`, `rsvp:write`); requests outside the granted scopes return 403 INSUFFICIENT_SCOPE."}},"schemas":{"Error":{"type":"object","description":"Standard 4xx/5xx error shape. `code` is a stable SCREAMING_SNAKE_CASE machine identifier — agents should branch on this. `error` is a human-readable message. `details` carries optional structured context (required scopes, validation errors, etc.). Some legacy endpoints return `{ error: 'CODE', message: 'text' }` instead — for back-compat, parse `code ?? error` as the machine identifier.","required":["error","code"],"properties":{"error":{"type":"string","example":"API key has insufficient scope"},"code":{"type":"string","example":"INSUFFICIENT_SCOPE"},"details":{"type":"object","additionalProperties":true,"example":{"required":["profile:read"],"granted":["events:read"]}}}},"Profile":{"type":"object","properties":{"id":{"type":"string","description":"Canonical user id (wallet address, lowercased for EVM)"},"displayName":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"verified":{"type":"boolean"},"twitterHandle":{"type":"string","nullable":true},"discordHandle":{"type":"string","nullable":true}},"example":{"id":"0xabcdef1234567890abcdef1234567890abcdef12","displayName":"alice.eth","bio":"Onchain since '17. Building event tools.","avatarUrl":"/uploads/avatar-1234-512.webp","verified":true,"twitterHandle":"alice_onchain","discordHandle":"alice#0001"}},"Eligibility":{"type":"object","required":["eligible"],"properties":{"eligible":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Machine-readable reason on failure (e.g. NO_MATCHING_ASSET, EVENT_PAST)"},"message":{"type":"string","nullable":true,"description":"Human-readable explanation"},"matchedGroupId":{"type":"string","nullable":true,"description":"Gate group id the user qualified through"}},"example":{"eligible":true,"reason":null,"message":null,"matchedGroupId":"gate_main"}},"RsvpStatus":{"type":"object","required":["rsvped"],"properties":{"rsvped":{"type":"boolean"},"rsvpId":{"type":"string","nullable":true},"groupId":{"type":"string","nullable":true},"checkedIn":{"type":"boolean"},"checkedInAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time","nullable":true}},"example":{"rsvped":true,"rsvpId":"rsvp_clxyz123abc","groupId":"gate_main","checkedIn":false,"checkedInAt":null,"createdAt":"2026-05-09T14:32:00Z"}},"Event":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"date":{"type":"string","description":"ISO date (YYYY-MM-DD)"},"endDate":{"type":"string","nullable":true},"time":{"type":"string","nullable":true},"endTime":{"type":"string","nullable":true},"location":{"type":"string"},"venueName":{"type":"string","nullable":true},"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"imageUrl":{"type":"string","nullable":true},"createdBy":{"type":"string"},"gates":{"type":"object","nullable":true,"description":"Token-gate configuration with groups array"},"category":{"type":"string","nullable":true}},"example":{"id":"7s5TZhMQqrCs","title":"Toronto Tech Week VIP Kickoff Social","description":"Organized by Joel Hansen.","date":"2026-05-25","endDate":null,"time":"1:00 PM","endTime":"3:00 PM","location":"Toronto, Ontario, Canada","venueName":null,"latitude":43.6532,"longitude":-79.3832,"imageUrl":"/uploads/event-7s5TZhMQqrCs-1024.webp","createdBy":"0xabc...","gates":{"mode":"any","groups":[{"id":"gate_main","label":"Toronto Tech Week pass holders","requirements":[{"chainId":1,"standard":"erc721","contract":"0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D","minBalance":"1"}]}]},"category":"conference"}},"GateGroup":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"chainId":{"type":"integer"},"standard":{"type":"string","enum":["open","erc721","erc1155","erc20","spl-nft","spl-token","sol-native","btc-ordinal","btc-rune","btc-native","ada-nft","ada-token","ada-native","xrp-nft","xrp-token","xrp-native","sui-nft","sui-coin","sui-native","poap"]},"contract":{"type":"string"},"tokenId":{"type":"string","nullable":true},"poapEventId":{"type":"string","nullable":true,"description":"POAP event ID (when standard=poap)"},"minBalance":{"type":"string"}}},"RsvpRecord":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"userId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"canceledAt":{"type":"string","format":"date-time","nullable":true},"checkedInAt":{"type":"string","format":"date-time","nullable":true},"checkinToken":{"type":"string","description":"Event-specific JWT for QR code"},"groupId":{"type":"string","nullable":true,"description":"Gate group the user qualified through"}},"example":{"id":"rsvp_clxyz123abc","eventId":"7s5TZhMQqrCs","userId":"0xabcdef1234567890abcdef1234567890abcdef12","createdAt":"2026-05-09T14:32:00Z","canceledAt":null,"checkedInAt":null,"checkinToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","groupId":"gate_main"}},"Speaker":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"name":{"type":"string"},"title":{"type":"string","nullable":true},"company":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"twitterHandle":{"type":"string","nullable":true},"linkedInUrl":{"type":"string","nullable":true},"websiteUrl":{"type":"string","nullable":true}}},"ConferenceSession":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"trackName":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time","nullable":true},"speakers":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"}},"saved":{"type":"boolean","description":"True if current viewer has saved this session"},"attendeeCount":{"type":"integer"}}},"EventChain":{"type":"object","properties":{"id":{"type":"string"},"creatorId":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"requiredEventIds":{"type":"array","items":{"type":"string"}},"rewardType":{"type":"string","enum":["note","unlock_code"]},"rewardText":{"type":"string"},"rewardUnlockCode":{"type":"string","nullable":true,"description":"Only returned after user completes the chain"},"active":{"type":"boolean"}}},"ChatMessage":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"userId":{"type":"string"},"username":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"message":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}}}},"security":[{"bearer":[]}],"tags":[{"name":"Auth","description":"Session + OAuth sign-in"},{"name":"Profile","description":"User profile + wallets + leaderboards + stats"},{"name":"Events","description":"Event CRUD + discovery + trending + search + SSE stream"},{"name":"RSVPs","description":"RSVP lifecycle"},{"name":"Check-in","description":"QR code check-in flow + host-side check-in"},{"name":"Gates","description":"Token-gate configuration + eligibility"},{"name":"Agenda","description":"Speakers + sessions (conference toolkit)"},{"name":"Event Unlocks","description":"Attend-to-unlock event perks (aka chains)"},{"name":"Chat","description":"Live event chat (SSE)"},{"name":"Photos","description":"Event photo gallery"},{"name":"Comments","description":"Event discussion threads"},{"name":"POAP","description":"POAP gallery + gating"},{"name":"Webhooks","description":"Push delivery for real-time events"},{"name":"API Keys","description":"Mint, manage, audit api_* keys for agents"},{"name":"OAuth","description":"OAuth 2.0 Authorization Code flow for multi-tenant apps"},{"name":"Rewards","description":"IRLRewards: token-gated rewards, claims, points (cross-platform with IRLEvents)"}],"paths":{"/api/me":{"get":{"tags":["Profile"],"summary":"Alias for GET /api/profile (308 redirect)","description":"Convenience alias for `GET /api/profile`. Returns a 308 Permanent Redirect to `/api/profile` — every modern HTTP client (curl, fetch, axios, Python requests) follows it automatically. Same auth, same scope (`profile:read`), same response.","responses":{"308":{"description":"Permanent redirect to /api/profile","headers":{"Location":{"schema":{"type":"string","example":"/api/profile"}}}}}}},"/api/profile":{"get":{"tags":["Profile"],"summary":"Get current user's profile","description":"Requires `profile:read` scope when called with an api key. Returns the canonical Profile keyed off the caller's wallet/identity. Also reachable as `GET /api/me` (308 redirect).","responses":{"200":{"description":"Profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required.","code":"UNAUTHENTICATED"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This action requires one of: profile:read. Your key has: events:read.","code":"INSUFFICIENT_SCOPE","details":{"required":["profile:read"],"granted":["events:read"]}}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Profile not found","code":"PROFILE_NOT_FOUND"}}}}}},"put":{"tags":["Profile"],"summary":"Update profile fields","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}},"responses":{"200":{"description":"Updated profile"}}}},"/api/profile/assets/sync":{"post":{"tags":["Profile"],"summary":"Force-refresh on-chain holdings for the authenticated user","description":"Re-queries Alchemy (EVM, 13 chains), Helius (Solana), Hiro (Bitcoin Ordinals), and chain-native APIs (TON, Aptos, Tezos, Flow, Cosmos, POAP, Snapshot) for every wallet linked to the profile, then writes the result back to `Profile.assets`. **Slow (5-30s).** Don't poll — once per session is fine; the cache is fresh enough for eligibility checks. Requires `profile:write` scope on api/OAuth tokens.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"},"example":{}}}},"responses":{"200":{"description":"Sync complete","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"walletsSynced":{"type":"integer"},"chainsCovered":{"type":"integer"},"assetsUpdatedAt":{"type":"string","format":"date-time"}}},"example":{"success":true,"walletsSynced":3,"chainsCovered":13,"assetsUpdatedAt":"2026-05-10T12:00:00Z"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks profile:write scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This action requires one of: profile:write. Your key has: profile:read.","code":"INSUFFICIENT_SCOPE","details":{"required":["profile:write"],"granted":["profile:read"]}}}}},"500":{"description":"Upstream NFT provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to sync assets","code":"ASSET_SYNC_FAILED"}}}}}}},"/api/profiles/{id}/public":{"get":{"tags":["Profile"],"security":[],"summary":"Public creator profile","description":"No auth required. Returns the profile owner's display name, bio, avatar, verified status, follower counts, and a list of their hosted events.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Wallet address (EVM lowercased), Discord handle, or username","example":"0xabcdef1234567890abcdef1234567890abcdef12"}],"responses":{"200":{"description":"Public profile with stats and events"},"403":{"description":"Profile is private","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Profile is private","code":"PROFILE_PRIVATE"}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Profile not found","code":"PROFILE_NOT_FOUND"}}}}}}},"/api/profiles/{id}/poaps":{"get":{"tags":["POAP","Profile"],"security":[],"summary":"Public POAP gallery for a profile","description":"No auth required. Returns every POAP token owned by any wallet linked to the profile, sorted by event start date descending. Drawn from the cached `Profile.assets` snapshot.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"0xabcdef..."}],"responses":{"200":{"description":"POAP tokens array","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"eventId":"12345","name":"ETH Denver 2026","imageUrl":"https://assets.poap.xyz/...","eventStart":"2026-02-15","chain":"gnosis"}]}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events":{"get":{"tags":["Events"],"security":[],"summary":"List events with optional filters and cursor pagination","description":"Public — no auth required, but personalized when api key / JWT supplied (requires `events:read`).\n\n**Pagination is opt-in for back-compat:** without `?limit`, the response is a flat array of every matching event. With `?limit=N`, the response is a page of up to N events plus a `Link: <...>; rel=\"next\"` header and `X-Next-Cursor: <opaque>` header. Pass that cursor as `?cursor=...` to fetch the next page. Absence of those headers means you've reached the last page. Recommended for agents iterating large result sets.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200},"example":50,"description":"Page size (1–200). Omit for legacy unpaginated array response."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from a previous page's X-Next-Cursor header"},{"name":"seriesId","in":"query","schema":{"type":"string"},"description":"Restrict to events in this series"},{"name":"hashtag","in":"query","schema":{"type":"string"},"example":"ethdenver"}],"responses":{"200":{"description":"Array of events (paginated when ?limit is set)","headers":{"Link":{"schema":{"type":"string"},"description":"RFC 5988 link header — present when there's a next page","example":"<https://irlevents.io/api/events?limit=50&cursor=eyJ...>; rel=\"next\""},"X-Next-Cursor":{"schema":{"type":"string"},"description":"Opaque cursor for the next page (also embedded in the Link header)"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}}}},"400":{"description":"Invalid cursor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid cursor","code":"INVALID_CURSOR"}}}}}},"post":{"tags":["Events"],"summary":"Create a new event","description":"Create an event hosted by the authenticated caller. Requires `events:write` scope on api/OAuth tokens. Subject to subscription tier limits — `TIER_LIMIT_REACHED` (403) means upgrade required. Token gates can be set at create time via the `gates` field; see `GateGroup` schema for the requirement shape.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"},"example":{"title":"BAYC Holders Meetup — Las Vegas","date":"2026-06-15","time":"7:00 PM","endTime":"10:00 PM","timezone":"America/Los_Angeles","location":"Las Vegas, NV","venueName":"TBD","capacity":100,"category":"meetup","description":"Casual evening for Bored Ape holders during ETH Vegas.","gates":{"mode":"any","groups":[{"id":"gate_main","label":"BAYC holders","requirements":[{"chainId":1,"standard":"erc721","contract":"0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D","minBalance":"1"}]}]}}}}},"responses":{"201":{"description":"Event created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Insufficient scope or tier limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"insufficientScope":{"value":{"error":"This action requires one of: events:write. Your key has: events:read.","code":"INSUFFICIENT_SCOPE","details":{"required":["events:write"],"granted":["events:read"]}}},"tierLimit":{"value":{"error":"You've reached your monthly event limit (5/5). Upgrade your plan to create more events.","code":"TIER_LIMIT_REACHED","details":{"tier":"free","eventsThisMonth":5,"maxEventsPerMonth":5}}}}}}}}}},"/api/events/{id}":{"get":{"tags":["Events"],"security":[],"summary":"Get a single event by id","description":"Returns the full event including title, dates, location, host, gates config, and RSVP count. Public — no auth required.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"404":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Event not found","code":"EVENT_NOT_FOUND"}}}}}},"put":{"tags":["Events"],"summary":"Update event fields (creator or co-host only)","description":"Partial update — send only the fields you want to change. Requires `events:write` scope. Caller must be the event creator or a co-host.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"},"example":{"title":"BAYC Holders Meetup — Las Vegas (UPDATED)","capacity":150,"description":"Bigger venue confirmed."}}}},"responses":{"200":{"description":"Event updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"403":{"description":"Not the host or insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Event not found","code":"EVENT_NOT_FOUND"}}}}}},"delete":{"tags":["Events"],"summary":"Delete event (creator only)","description":"Permanently delete an event. Requires `events:write` scope. **Destructive** — RSVPs and check-ins are removed. Prefer setting status to 'cancelled' via PUT if you want to keep the record visible to attendees.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"403":{"description":"Not the creator or insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events/search":{"get":{"tags":["Events"],"security":[],"summary":"Keyword search across upcoming events","description":"Public — no auth required. Searches title, description, location, and category. Returns up to 50 matches. Pass `?includePast=1` to also search past events.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"example":"ethdenver"},{"name":"includePast","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Include past events in results"}],"responses":{"200":{"description":"Events array","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}}}},"400":{"description":"Missing `q` parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing `q` query parameter","code":"MISSING_FIELD"}}}}}}},"/api/events/stream":{"get":{"tags":["Events"],"summary":"Server-Sent Events stream of real-time event/RSVP/check-in changes","description":"Long-lived SSE connection. Same payload shape as webhooks (event.created, event.updated, event.deleted, rsvp.created, rsvp.canceled, checkin.completed) — useful for agents that can't host an HTTPS webhook endpoint. Requires `events:read` scope. Heartbeat comment every 25s. Optional filters via query params.","parameters":[{"name":"eventId","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to events referencing this event id","example":"7s5TZhMQqrCs"},{"name":"types","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated webhook event types to filter to","example":"rsvp.created,checkin.completed"}],"responses":{"200":{"description":"Server-Sent Events stream (text/event-stream)","content":{"text/event-stream":{"schema":{"type":"string"},"example":"retry: 5000\\n\\n: connected 2026-05-09T21:00:00Z\\n\\nevent: rsvp.created\\ndata: {\\\"type\\\":\\\"rsvp.created\\\",\\\"timestamp\\\":\\\"2026-05-09T21:00:01Z\\\",\\\"data\\\":{...}}\\n\\n"}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events/{id}/eligibility":{"get":{"tags":["Events","Gates"],"summary":"Check if the current user can RSVP to this event","description":"Requires `events:read` scope. Always call this before `POST /api/events/{id}/rsvp` — RSVP returns 403 NOT_ELIGIBLE for ineligible callers. For optional auth (no api key / JWT), returns `eligible: false` with reason `NO_PROFILE`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Eligibility with reason + matched gate group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Eligibility"}}}},"404":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Event not found","code":"EVENT_NOT_FOUND"}}}}}}},"/api/events/{id}/rsvp":{"post":{"tags":["RSVPs"],"summary":"Create an RSVP (runs eligibility + token lock)","description":"Requires `rsvp:write` scope. Re-syncs the user's on-chain assets (force=true), re-runs eligibility, locks the qualifying NFT in Redis (5-min TTL), creates the `RsvpRecord`, and returns it with a per-event `checkinToken` JWT. Pass an empty body `{}` for the simple case.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"walletId":{"type":"string"},"quantity":{"type":"integer"},"termsAccepted":{"type":"boolean"},"unlockReceipts":{"type":"array","items":{"type":"object"}}}},"example":{}}}},"responses":{"201":{"description":"RSVP created with checkinToken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RsvpRecord"}}}},"400":{"description":"Application/terms required (event-specific)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"applicationRequired":{"value":{"error":"This event requires an application. Please apply first.","code":"APPLICATION_REQUIRED","details":{"applyUrl":"/api/events/7s5TZhMQqrCs/apply"}}},"termsRequired":{"value":{"error":"You must accept the event terms to RSVP","code":"TERMS_REQUIRED"}},"eventCancelled":{"value":{"error":"This event has been cancelled","code":"EVENT_CANCELLED"}}}}}},"403":{"description":"Not eligible / insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notEligible":{"value":{"error":"You don't have the required NFT/token to RSVP for this event","code":"NOT_ELIGIBLE","details":{"reason":"NO_MATCHING_ASSET"}}},"insufficientScope":{"value":{"error":"This action requires one of: rsvp:write. Your key has: events:read.","code":"INSUFFICIENT_SCOPE","details":{"required":["rsvp:write"],"granted":["events:read"]}}}}}}},"409":{"description":"Token already locked or already RSVPed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"tokenLocked":{"value":{"error":"This NFT is already locked for another event in the same window","code":"TOKEN_LOCKED"}},"alreadyRsvped":{"value":{"error":"You already have an RSVP for this event","code":"ALREADY_RSVPED"}}}}}},"410":{"description":"Event full or past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"eventFull":{"value":{"error":"This event is at capacity","code":"EVENT_FULL"}},"eventPast":{"value":{"error":"This event has already started","code":"EVENT_PAST"}}}}}}}},"delete":{"tags":["RSVPs"],"summary":"Cancel an RSVP","description":"Requires `rsvp:write` scope. Frees the locked token so it can be reused for another event in the same window.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Canceled"},"404":{"description":"RSVP not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RSVP not found","code":"RSVP_NOT_FOUND"}}}}}}},"/api/events/{id}/rsvp/status":{"get":{"tags":["RSVPs"],"summary":"Current user's RSVP status for the event","description":"Requires `events:read` scope. Returns whether the caller has RSVPed, plus check-in status and which gate group they qualified through.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"RSVP status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RsvpStatus"}}}}}}},"/api/events/{id}/rsvps":{"get":{"tags":["RSVPs"],"summary":"List RSVPs for an event (creator/host only)","description":"Returns every RSVP record for the event including check-in status. Caller must be the creator or a co-host. Requires `events:read` scope on api/OAuth tokens (host privilege check is separate from scope).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Array of RSVPs","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RsvpRecord"}}}}},"403":{"description":"Caller is not the event host","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Only the event host can list RSVPs","code":"FORBIDDEN"}}}},"404":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/checkin/{token}":{"get":{"tags":["Check-in"],"security":[],"summary":"Validate a check-in token and preview RSVP details","description":"Used by the QR scanner UI. Decodes the per-event JWT from the QR code and returns the RSVP + event preview without actually checking in. Public — no auth required.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"},"description":"Per-event JWT from RsvpRecord.checkinToken"}],"responses":{"200":{"description":"RSVP + event preview"},"401":{"description":"Token invalid or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Check-in token invalid or expired","code":"INVALID_TOKEN"}}}}}},"post":{"tags":["Check-in"],"security":[],"summary":"Perform check-in. Marks RSVP.checkedInAt and triggers Event Unlocks chain completion checks.","description":"Marks the RSVP as checked in. Idempotent — checking in an already-checked-in RSVP returns the existing record. Response includes `completedChainIds` if this check-in completed any Event Unlocks chains the user was on.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Check-in confirmed","content":{"application/json":{"schema":{"type":"object","properties":{"rsvp":{"$ref":"#/components/schemas/RsvpRecord"},"completedChainIds":{"type":"array","items":{"type":"string"},"description":"Event Unlocks chains the user just completed"}}},"example":{"rsvp":{"id":"rsvp_clxyz...","eventId":"7s5TZhMQqrCs","userId":"0xabc...","checkedInAt":"2026-05-10T18:15:00Z","checkinToken":"eyJ...","groupId":"gate_main"},"completedChainIds":[]}}}},"401":{"description":"Token invalid or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events/{id}/checkin":{"post":{"tags":["Check-in"],"summary":"Host-side check-in by wallet address","description":"Mark an attendee as checked in at the door. Requires `events:write` scope on api/OAuth tokens. Caller must be the event host or a designated check-in staff member. Idempotent — checking in an already-checked-in attendee is a no-op (returns the existing record).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["wallet"],"properties":{"wallet":{"type":"string","description":"Attendee wallet address (EVM lowercased, or chain-native)"},"walletId":{"type":"string","description":"Legacy alias for `wallet`"}}},"example":{"wallet":"0xabcdef1234567890abcdef1234567890abcdef12"}}}},"responses":{"200":{"description":"Check-in confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RsvpRecord"}}}},"403":{"description":"Caller is not the host or check-in staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No RSVP found for that wallet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RSVP not found for that wallet","code":"RSVP_NOT_FOUND"}}}}}}},"/api/events/{id}/speakers":{"get":{"tags":["Agenda"],"security":[],"summary":"List speakers for an event","description":"Public — no auth required. Used by the conference toolkit's agenda page.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Array of speakers","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"}}}}}}},"post":{"tags":["Agenda"],"summary":"Add a speaker (host only)","description":"Caller must be the event host or co-host. Requires `events:write` scope on api/OAuth tokens.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Speaker"},"example":{"name":"Vitalik Buterin","title":"Co-founder","company":"Ethereum Foundation","twitterHandle":"VitalikButerin","bio":"..."}}}},"responses":{"201":{"description":"Speaker created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Speaker"}}}},"403":{"description":"Not the host","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events/{id}/sessions":{"get":{"tags":["Agenda"],"security":[],"summary":"List conference sessions","description":"Public — sessions returned with hydrated speaker objects and (when authed) a `saved` flag indicating whether the current viewer has added each session to their schedule.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Sessions array","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConferenceSession"}}}}}}},"post":{"tags":["Agenda"],"summary":"Create a session (host only)","description":"Caller must be the host. Requires `events:write` scope on api/OAuth tokens.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConferenceSession"},"example":{"title":"Opening Keynote","trackName":"Main Stage","location":"Grand Ballroom","startAt":"2026-06-15T09:00:00-07:00","endAt":"2026-06-15T10:00:00-07:00","speakerIds":["spk_abc123"]}}}},"responses":{"201":{"description":"Created session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConferenceSession"}}}},"403":{"description":"Not the host","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events/{id}/sessions/{sessionId}/rsvp":{"post":{"tags":["Agenda"],"summary":"Save a session to my schedule","description":"Adds the session to the caller's personal agenda. Idempotent.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"},{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"},"example":"ses_clxyz123"}],"responses":{"201":{"description":"Saved","content":{"application/json":{"schema":{"type":"object"},"example":{"saved":true,"sessionId":"ses_clxyz123"}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Agenda"],"summary":"Remove a session from my schedule","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"},{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"},"example":"ses_clxyz123"}],"responses":{"200":{"description":"Unsaved","content":{"application/json":{"schema":{"type":"object"},"example":{"saved":false}}}}}}},"/api/creator/chains":{"get":{"tags":["Event Unlocks"],"summary":"List the caller's event unlock chains","description":"Returns chains the authenticated creator owns, with current completion counts. Event Unlocks (aka chains) reward attendees who attend a sequence of related events with a custom note or unlock code.","responses":{"200":{"description":"Chains owned by the caller","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EventChain"}},"example":[{"id":"chain_abc","creatorId":"0xabc...","title":"ETH Vegas Tour","description":"Attend all 3 ETH Vegas side events to unlock","requiredEventIds":["e1","e2","e3"],"rewardType":"unlock_code","rewardText":"Discount code: VEGAS10","active":true,"completionCount":14}]}}}}},"post":{"tags":["Event Unlocks"],"summary":"Create a new event unlock chain","description":"Requires the caller to host at least 2 of the included events. The reward (note text or unlock code) is revealed to attendees only after they've checked in to every required event.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventChain"},"example":{"title":"ETH Vegas Tour","description":"Attend all 3 ETH Vegas side events to unlock","requiredEventIds":["7s5TZhMQqrCs","hMbcksbXKDrZ","UnIiYxI4ms8R"],"rewardType":"unlock_code","rewardText":"VEGAS10","active":true}}}},"responses":{"201":{"description":"Chain created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventChain"}}}},"400":{"description":"Need at least 2 owned events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Chains require at least 2 events you host","code":"INVALID_REQUEST"}}}}}}},"/api/events/{id}/chains":{"get":{"tags":["Event Unlocks"],"security":[],"summary":"Chains this event belongs to with viewer progress","description":"Public — anyone can see the chain metadata. The `rewardUnlockCode` field is revealed only to viewers who have completed the chain (checked into every required event).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Chains with `myProgress` embedded when authed","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"chain_abc","title":"ETH Vegas Tour","requiredEventIds":["e1","e2","e3"],"rewardType":"unlock_code","myProgress":{"checkedInIds":["e1","e2"],"completed":false}}]}}}}}},"/api/my/chain-completions":{"get":{"tags":["Event Unlocks"],"summary":"Chains the caller has completed","description":"Returns each completion record with the hydrated chain details (title, reward) so the user can see what they've unlocked.","responses":{"200":{"description":"Completions array","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"chainId":"chain_abc","chain":{"title":"ETH Vegas Tour","rewardType":"unlock_code","rewardText":"VEGAS10"},"completedAt":"2026-04-29T19:30:00Z"}]}}}}}},"/api/events/{id}/chat":{"get":{"tags":["Chat"],"summary":"Fetch the last 200 chat messages for an event","description":"Returns most-recent first. Use `/chat/stream` (SSE) for live updates instead of polling.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Array of chat messages","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessage"}}}}}}},"post":{"tags":["Chat"],"summary":"Send a chat message","description":"1000 character cap. Rate-limited to 20 messages/min per user with a 1.5s cooldown between messages.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string","maxLength":1000}}},"example":{"message":"Just landed — heading over now! 🚕"}}}},"responses":{"201":{"description":"Message created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatMessage"}}}},"429":{"description":"Rate limit exceeded (20/min or 1.5s cooldown)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Slow down — wait a moment before sending again","code":"RATE_LIMITED"}}}}}}},"/api/events/{id}/chat/stream":{"get":{"tags":["Chat"],"summary":"Server-Sent Events stream of new chat messages","description":"EventSource can't set the Authorization header, so pass the user JWT as `?token=<jwt>` instead. Each message arrives as `event: chat\\ndata: <ChatMessage JSON>`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"},{"name":"token","in":"query","required":false,"schema":{"type":"string"},"description":"User JWT (since EventSource can't set Authorization)"}],"responses":{"200":{"description":"text/event-stream of chat messages","content":{"text/event-stream":{"schema":{"type":"string"},"example":"event: chat\\ndata: {\\\"id\\\":\\\"msg_clxyz...\\\",\\\"eventId\\\":\\\"7s5T...\\\",\\\"userId\\\":\\\"0xabc...\\\",\\\"message\\\":\\\"Hi!\\\",\\\"createdAt\\\":\\\"2026-05-10T...\\\"}\\n\\n"}}}}}},"/api/events/{id}/photos":{"get":{"tags":["Photos"],"summary":"List event photos","description":"Public — but only approved photos are visible to non-hosts. Hosts see all photos including pending/rejected.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Photos array","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"photo_abc","url":"/uploads/event-photo-...-1024.webp","thumbUrl":"/uploads/event-photo-...-256.webp","uploadedBy":"0xabc...","uploadedAt":"2026-05-10T...","approved":true,"caption":"View from the venue!"}]}}}}}},"/api/events/{id}/comments":{"get":{"tags":["Comments"],"summary":"List event comments","description":"Public. Returns top-level comments with nested replies via `parentId`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"responses":{"200":{"description":"Comments array","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"cmt_abc","eventId":"7s5T...","userId":"0xabc...","username":"alice.eth","comment":"Looking forward to this!","parentId":null,"createdAt":"2026-05-08T..."}]}}}}},"post":{"tags":["Comments"],"summary":"Post a comment or reply","description":"Pass `parentId` to reply to an existing comment. Comments support markdown.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"7s5TZhMQqrCs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string"},"parentId":{"type":"string","nullable":true}}},"example":{"comment":"Will there be parking onsite?","parentId":null}}}},"responses":{"201":{"description":"Comment created","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"cmt_xyz","eventId":"7s5T...","userId":"0xabc...","comment":"Will there be parking onsite?","parentId":null,"createdAt":"2026-05-10T..."}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhooks/register":{"post":{"tags":["Webhooks"],"summary":"Register a webhook endpoint for real-time event delivery","description":"Subscribes the given URL to the listed event types. The response includes a one-time `secret` (`whsec_...`) — store it; you'll need it to verify HMAC-SHA256 signatures on every incoming delivery. Failed deliveries retry with backoff (1m → 5m → 30m → hourly, 10 attempts) before auto-disabling. Endpoint must be HTTPS and respond 2xx within 10s.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","description":"HTTPS endpoint where IRLEvents will POST deliveries"},"events":{"type":"array","items":{"type":"string"},"description":"Subset of supported event types from /api/webhooks/event-types"}}},"example":{"url":"https://yourapp.com/webhooks/irlevents","events":["event.created","rsvp.created","checkin.completed"]}}}},"responses":{"201":{"description":"Webhook registered — save the `secret` immediately","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"webhook_abc123","url":"https://yourapp.com/webhooks/irlevents","events":["event.created","rsvp.created","checkin.completed"],"secret":"whsec_5f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0","active":true,"createdAt":"2026-05-10T12:00:00Z","message":"Webhook registered successfully. Save the secret to verify webhook signatures."}}}},"400":{"description":"Invalid URL or unknown event type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhooks/event-types":{"get":{"tags":["Webhooks"],"security":[],"summary":"List supported webhook event types","description":"Returns the canonical list of webhook event type identifiers (event.created, rsvp.canceled, etc.). Public — no auth required.","responses":{"200":{"description":"String array of event type identifiers","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}},"example":["event.created","event.updated","event.deleted","rsvp.created","rsvp.canceled","checkin.completed"]}}}}}},"/api/events/trending":{"get":{"tags":["Events"],"security":[],"summary":"Trending events (most RSVPed in the last 14 days)","description":"Public, cached. Good first call for agents discovering what's happening on the platform right now. Optional `events:read` scope when called with an api/OAuth token (response is unchanged).","responses":{"200":{"description":"Array of events","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}}}}}}},"/api/creators/leaderboard":{"get":{"tags":["Profile"],"security":[],"summary":"Top creators by RSVPs and events hosted","description":"Cached leaderboard. Public.","responses":{"200":{"description":"Array of top creators","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"0xabc...","displayName":"alice.eth","verified":true,"eventCount":24,"totalRsvps":3201,"avatarUrl":"/uploads/avatar-...-512.webp"}]}}}}}},"/api/stats/public":{"get":{"tags":["Profile"],"security":[],"summary":"Cached platform-wide stats","description":"Total events, upcoming, RSVPs, check-ins, creators, chains active. Refreshed every few minutes.","responses":{"200":{"description":"Stats object","content":{"application/json":{"schema":{"type":"object"},"example":{"totalEvents":773,"upcomingEvents":155,"totalRsvps":16,"totalCheckIns":3,"totalCreators":3,"totalChains":2}}}}}}},"/api/users/{userId}/achievements":{"get":{"tags":["Profile"],"security":[],"summary":"Computed badges and progress for a user","description":"Public. Returns earned achievement badges (events attended, chains used, streaks) plus progress toward unearned ones.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"example":"0xabcdef..."}],"responses":{"200":{"description":"Achievements payload","content":{"application/json":{"schema":{"type":"object"},"example":{"earned":[{"id":"first_rsvp","name":"First RSVP","earnedAt":"2026-01-12T..."},{"id":"five_events","name":"Event Regular","earnedAt":"2026-03-04T..."}],"inProgress":[{"id":"chain_explorer","name":"Chain Explorer","progress":7,"target":10}]}}}}}}},"/api/users/{wallet}/events/eligible":{"get":{"tags":["Events","Gates"],"summary":"Every public event a wallet currently qualifies for","description":"Walks every upcoming public event and returns those whose gates the given wallet's cached assets satisfy. Requires `events:read` scope on api/OAuth tokens. Slow on accounts with many wallets — cache the result.","parameters":[{"name":"wallet","in":"path","required":true,"schema":{"type":"string"},"description":"Wallet address (EVM lowercased, or chain-native)","example":"0xabcdef..."}],"responses":{"200":{"description":"Array of eligible events with the matched gate group id","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"7s5TZhMQqrCs","title":"BAYC Holders Meetup","date":"2026-06-15","matchedGroupId":"gate_main"}]}}}}}},"/api/api-keys":{"get":{"tags":["API Keys"],"summary":"List the authenticated user's api keys","description":"JWT-only — api keys cannot manage other api keys. Returns each key's metadata (preview, scopes, lastUsedAt, etc.) plus the canonical scope catalog and default-scope set for new keys.","responses":{"200":{"description":"Keys + scope catalog","content":{"application/json":{"schema":{"type":"object"},"example":{"keys":[{"id":"ck_...","name":"Claude Personal Assistant","keyPreview":"api_a1b2c3d4...","active":true,"lastUsedAt":"2026-05-10T11:00:00Z","expiresAt":null,"createdAt":"2026-04-01T...","rateLimit":1000,"scopes":["profile:read","events:read","rsvp:write"]}],"availableScopes":{"profile:read":"Read your profile, wallets, and cached on-chain assets","events:read":"Read public events, eligibility, RSVP status, ..."},"defaultScopes":["profile:read","events:read","rsvp:write"]}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["API Keys"],"summary":"Mint a new api key","description":"JWT-only — api keys cannot mint other api keys. Subject to subscription tier (Business required) and a per-user active-key limit. The raw `key` is returned ONCE in the response — store it immediately, it can't be recovered. Pass `scopes` to grant only the permissions the key needs (least privilege); omit to use the default set (`profile:read`, `events:read`, `rsvp:write`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Human-readable key name"},"scopes":{"type":"array","items":{"type":"string"},"description":"Subset of available scopes; omit for default set"}}},"example":{"name":"Claude Personal Assistant","scopes":["profile:read","events:read","rsvp:write"]}}}},"responses":{"201":{"description":"Key created — raw `key` shown once","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"ck_...","name":"Claude Personal Assistant","key":"api_a1b2c3...64hex","keyPreview":"api_a1b2c3d4...","scopes":["profile:read","events:read","rsvp:write"],"createdAt":"2026-05-10T...","message":"Save this key now — it will not be shown again."}}}},"400":{"description":"Invalid scopes or missing name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Tier limit reached or API access not on plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"API access requires Business tier","code":"TIER_LIMIT_REACHED","details":{"upgradeRequired":true}}}}}}}},"/api/api-keys/{id}":{"patch":{"tags":["API Keys"],"summary":"Rename an api key or update its scopes","description":"JWT-only. Send `name` to rename, `scopes` to change permissions, or both. Empty scopes array is rejected (would over-grant) — pass at least one scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}}},"example":{"scopes":["profile:read","events:read","events:write","rsvp:write"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object"},"example":{"success":true}}}},"404":{"description":"Key not found (or not owned by caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["API Keys"],"summary":"Revoke an api key (sets active=false)","description":"JWT-only. Revocation is immediate — subsequent requests with the key 401.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"},"example":{"revoked":true}}}},"404":{"description":"Key not found (or not owned by caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api-keys/{id}/usage":{"get":{"tags":["API Keys"],"summary":"Per-key audit log","description":"JWT-only. Returns the most recent N requests made with this key plus aggregates (total calls in window, error count, success rate, top-20 endpoints by frequency). Default window: last 7 days. Use `?since=ISO` and `?limit=N` to override (limit max 500).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Lower bound on createdAt (default: 7 days ago)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500},"example":100}],"responses":{"200":{"description":"Usage payload","content":{"application/json":{"schema":{"type":"object"},"example":{"key":{"id":"ck_...","name":"Claude Personal Assistant","keyPreview":"api_a1b2c3d4..."},"window":{"since":"2026-05-03T00:00:00Z","until":"2026-05-10T12:00:00Z"},"summary":{"total":247,"errors":8,"successRate":96.8},"topEndpoints":[{"method":"GET","path":"/api/events/trending","count":89},{"method":"GET","path":"/api/events","count":71}],"recent":[{"method":"POST","path":"/api/events/7s5T.../rsvp","status":201,"durationMs":320,"at":"2026-05-10T11:58:00Z"}]}}}}}}},"/oauth/authorize":{"get":{"tags":["OAuth"],"security":[],"summary":"Consent screen for the OAuth Authorization Code flow","description":"Send users here to authorize a third-party app. The page (a React route on the frontend) validates the request via `/api/oauth/consent-info`, presents the requested scopes with descriptions, and on Approve calls `POST /api/oauth/authorize` to mint an authorization code. The user is then redirected to the app's `redirect_uri` with `?code=...&state=...`.\n\nIf the user is not signed in, they're bounced to `/login?returnTo=...` first.","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"},"example":"irl_a1b2c3d4..."},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"},"example":"https://myapp.com/oauth/callback"},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"scope","in":"query","required":true,"schema":{"type":"string"},"description":"Space-separated scope list","example":"profile:read events:read rsvp:write"},{"name":"state","in":"query","required":false,"schema":{"type":"string"},"description":"CSRF-prevention nonce; echoed back on the redirect"}],"responses":{"200":{"description":"Consent page rendered (HTML; React route)"}}}},"/api/oauth/consent-info":{"get":{"tags":["OAuth"],"security":[],"summary":"Validate an OAuth request and fetch display info for the consent page","description":"Called by the React consent page. Does NOT issue any tokens — just validates the client + redirect + scope and returns the client name and per-scope descriptions.","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string"},"description":"Space-separated scope list (default: 'profile:read')"}],"responses":{"200":{"description":"Client + scope display info","content":{"application/json":{"schema":{"type":"object"},"example":{"client":{"name":"EventBot SaaS","platformUrl":"https://eventbot.example.com"},"requestedScopes":[{"scope":"profile:read","description":"Read your profile, wallets, and cached on-chain assets"},{"scope":"events:read","description":"Read public events, eligibility, RSVP status, ..."}],"redirectUri":"https://myapp.com/oauth/callback"}}}},"400":{"description":"Invalid request, redirect_uri, or scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Client not found or inactive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Client not found or inactive","code":"INVALID_CLIENT"}}}}}}},"/api/oauth/authorize":{"get":{"tags":["OAuth"],"security":[],"summary":"Backwards-compat redirect to the React consent page at /oauth/authorize","description":"Earlier docs pointed apps at `/api/oauth/authorize`. This endpoint now 302-redirects to `/oauth/authorize` preserving query params. New integrations should point directly at `/oauth/authorize`.","responses":{"302":{"description":"Redirect to /oauth/authorize","headers":{"Location":{"schema":{"type":"string","example":"https://irlevents.io/oauth/authorize?client_id=..."}}}}}},"post":{"tags":["OAuth"],"summary":"Handle user consent (approve/deny)","description":"JWT-authed (the user's session). Called by the React consent page. Returns `{ redirectUrl }` — the page does `window.location = redirectUrl` to bounce back to the third-party app with `?code=...&state=...` (approve) or `?error=access_denied` (deny).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_id","redirect_uri","action"],"properties":{"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"scope":{"type":"string","description":"Space-separated scope list"},"state":{"type":"string"},"action":{"type":"string","enum":["approve","deny"]}}},"example":{"client_id":"irl_...","redirect_uri":"https://myapp.com/cb","scope":"profile:read events:read","state":"abc123","action":"approve"}}}},"responses":{"200":{"description":"Redirect URL for the React page to navigate to","content":{"application/json":{"schema":{"type":"object"},"example":{"redirectUrl":"https://myapp.com/cb?code=a1b2c3d4...&state=abc123"}}}},"400":{"description":"Invalid client/redirect/scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/oauth/token":{"post":{"tags":["OAuth"],"security":[],"summary":"Exchange authorization code for access token, or refresh an expired token","description":"Standard OAuth2 token endpoint. Two grant types supported: `authorization_code` (initial exchange) and `refresh_token` (rotation). Access tokens are 30-day lifetime, refresh tokens 90-day. On refresh, the OLD access token is revoked immediately — store the new pair, discard the old one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"},"examples":{"authorizationCode":{"summary":"Initial code-for-token exchange","value":{"grant_type":"authorization_code","code":"a1b2c3d4...","client_id":"irl_...","client_secret":"irlsecret_...","redirect_uri":"https://myapp.com/cb"}},"refreshToken":{"summary":"Refresh an expired access token","value":{"grant_type":"refresh_token","refresh_token":"oat_refresh_...","client_id":"irl_...","client_secret":"irlsecret_..."}}}}}},"responses":{"200":{"description":"Access + refresh token pair","content":{"application/json":{"schema":{"type":"object"},"example":{"access_token":"oat_a1b2c3d4...64hex","token_type":"Bearer","expires_in":2592000,"refresh_token":"oat_refresh_a1b2c3d4...","scope":"profile:read events:read rsvp:write"}}}},"400":{"description":"Invalid grant or expired code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authorization code is invalid or expired","code":"invalid_grant"}}}},"401":{"description":"Invalid client credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["OAuth"],"security":[],"summary":"RFC 8414 OAuth 2.0 Authorization Server Metadata","description":"Discovery document. Generic OAuth client libraries (Hydra, oauth2-client, simple-oauth2, etc.) auto-configure from this — they read it once and know the authorization_endpoint, token_endpoint, supported scopes, grant types, etc. without you hardcoding them.","responses":{"200":{"description":"Discovery metadata","content":{"application/json":{"schema":{"type":"object"},"example":{"issuer":"https://irlevents.io","authorization_endpoint":"https://irlevents.io/oauth/authorize","token_endpoint":"https://irlevents.io/api/oauth/token","scopes_supported":["profile:read","profile:write","events:read","events:write","rsvp:write","rewards:read","rewards:write"],"response_types_supported":["code"],"grant_types_supported":["authorization_code","refresh_token"],"token_endpoint_auth_methods_supported":["client_secret_post"],"service_documentation":"https://irlevents.io/agents"}}}}}}},"/api/oauth/clients":{"get":{"tags":["OAuth"],"summary":"List the user's OAuth clients (developer portal)","description":"JWT-only. Returns metadata for clients the authenticated user has registered. clientSecret is never returned (only shown once at create/regenerate time). Also returns the canonical SCOPE_CATALOG for UI rendering.","responses":{"200":{"description":"Owned clients + scope catalog","content":{"application/json":{"schema":{"type":"object"},"example":{"clients":[{"id":"ck_...","name":"My Event Bot","clientId":"irl_a1b2c3...","redirectUris":["https://myapp.com/cb"],"allowedScopes":["profile:read","events:read"],"platformUrl":"https://myapp.com","contactEmail":"dev@myapp.com","active":true,"activeGrants":12,"createdAt":"2026-05-10T..."}],"availableScopes":{"profile:read":"Read your profile, wallets, ...","events:read":"Read public events, ..."},"maxClientsPerUser":10}}}}}},"post":{"tags":["OAuth"],"summary":"Register a new OAuth client (self-service)","description":"JWT-only. Returns clientId + clientSecret in the response. The secret is shown ONCE — store it immediately, it cannot be recovered (only regenerated). Subject to per-user max-clients limit (default 10). Redirect URIs must use https (http://localhost is allowed for development).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","redirectUris","allowedScopes"],"properties":{"name":{"type":"string"},"redirectUris":{"type":"array","items":{"type":"string","format":"uri"}},"allowedScopes":{"type":"array","items":{"type":"string"}},"platformUrl":{"type":"string","format":"uri"},"contactEmail":{"type":"string","format":"email"}}},"example":{"name":"My Event Bot","redirectUris":["https://myapp.com/oauth/callback"],"allowedScopes":["profile:read","events:read","rsvp:write"],"platformUrl":"https://myapp.com","contactEmail":"dev@myapp.com"}}}},"responses":{"201":{"description":"Client created — clientSecret shown ONCE","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"ck_...","clientId":"irl_a1b2c3d4...32hex","clientSecret":"irlsecret_xyz789...64hex","message":"Save the clientSecret now — it cannot be recovered. You can regenerate it later if lost."}}}},"400":{"description":"Invalid name, redirect URI, or scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Max clients reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Maximum 10 OAuth clients per user","code":"TIER_LIMIT_REACHED"}}}}}}},"/api/oauth/clients/{id}":{"patch":{"tags":["OAuth"],"summary":"Update an OAuth client's metadata","description":"JWT-only. Update name, redirectUris, allowedScopes, platformUrl, contactEmail, or active status. clientId and clientSecret cannot be changed via PATCH — use the dedicated regenerate-secret endpoint.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"},"example":{"name":"New name","allowedScopes":["profile:read","events:read","rsvp:write"],"active":true}}}},"responses":{"200":{"description":"Updated client","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Client not found (or not owned by caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["OAuth"],"summary":"Soft-delete + revoke all active grants for a client","description":"JWT-only. Sets active=false on the client and deletes every OAuthAccessToken issued for it. Existing user grants stop working immediately.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"},"example":{"revoked":true,"tokensRevoked":12}}}},"404":{"description":"Client not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/oauth/clients/{id}/regenerate-secret":{"post":{"tags":["OAuth"],"summary":"Generate a new clientSecret","description":"JWT-only. Old secret stops working immediately. Use this if the old secret is compromised. Coordinate the rotation with your deployed integration — there's a window where neither old nor new will work in production until you push the new secret.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"New secret (shown once)","content":{"application/json":{"schema":{"type":"object"},"example":{"clientId":"irl_...","clientSecret":"irlsecret_NEW...","message":"Old secret deactivated. Update your integration with the new value — it cannot be recovered later."}}}}}}},"/api/oauth/grants":{"get":{"tags":["OAuth"],"summary":"List the user's active OAuth grants","description":"JWT-only. Returns every third-party app that has a non-expired access token for this user, with its name, requested scopes, and authorize/expire dates. The user-facing 'Authorized Apps' UI reads this.","responses":{"200":{"description":"Active grants","content":{"application/json":{"schema":{"type":"object"},"example":{"grants":[{"id":"tok_...","clientName":"EventBot SaaS","platformUrl":"https://eventbot.example.com","contactEmail":"support@eventbot.example.com","scopes":["profile:read","events:read"],"scopeDescriptions":[{"scope":"profile:read","description":"Read your profile, wallets, and cached on-chain assets"}],"expiresAt":"2026-06-09T...","createdAt":"2026-05-10T..."}]}}}}}}},"/api/oauth/grants/{id}":{"delete":{"tags":["OAuth"],"summary":"Revoke an OAuth grant","description":"JWT-only. Revocation is immediate — the access token (and its refresh token) are deleted. The third-party app must re-prompt the user to authorize again.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"},"example":{"revoked":true}}}},"404":{"description":"Grant not found (or not owned by caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/rewards":{"get":{"tags":["Rewards"],"security":[],"summary":"List active IRLRewards","description":"Token-gated rewards. Returns active rewards by default — pass ?status=ended for past. Optional `rewards:read` scope when called with an api/OAuth token (response unchanged but personalizes claim status if present).","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["discount","merch","digital","loyalty_redeem","airdrop"]}},{"name":"category","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","ended","all"]},"example":"active"},{"name":"featured","in":"query","schema":{"type":"boolean"}},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","ending_soon","popular"]}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":100},"example":20},{"name":"offset","in":"query","schema":{"type":"integer"},"example":0},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"creator","in":"query","schema":{"type":"string"},"description":"Filter by creator wallet/profile id"}],"responses":{"200":{"description":"Array of active rewards"}}}},"/api/rewards/{id}":{"get":{"tags":["Rewards"],"security":[],"summary":"Get a single reward","description":"Returns title, description, type, image, gates, supply, creator info. Optional `rewards:read` scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reward detail"},"404":{"description":"Reward not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Reward not found","code":"NOT_FOUND"}}}}}}},"/api/rewards/{id}/eligibility":{"get":{"tags":["Rewards"],"summary":"Check whether the caller can claim this reward","description":"Returns `{ eligible, reason }`. Always call before `POST /api/rewards/:id/claim` — claim returns 403 NOT_ELIGIBLE for ineligible callers. Requires `rewards:read` scope on api/OAuth tokens.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Eligibility result","content":{"application/json":{"schema":{"type":"object"},"example":{"eligible":true,"reason":null,"matchedGroupId":"gate_main"}}}}}}},"/api/rewards/{id}/claim":{"post":{"tags":["Rewards"],"summary":"Claim a reward on the user's behalf","description":"Re-checks eligibility, deducts points (loyalty_redeem rewards), generates a claim record. Requires `rewards:write` scope. Failure modes: NOT_ELIGIBLE (403), ALREADY_CLAIMED (409), REWARD_EXHAUSTED (410), INSUFFICIENT_POINTS (400).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"},"example":{}}}},"responses":{"201":{"description":"Claim created","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"claim_abc","rewardId":"rwd_xyz","userId":"0xabc...","status":"pending","claimToken":"rct_...","createdAt":"2026-05-10T..."}}}},"403":{"description":"Not eligible or insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already claimed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"You've already claimed this reward","code":"ALREADY_CLAIMED"}}}},"410":{"description":"Reward exhausted (out of supply)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"All claims for this reward are taken","code":"REWARD_EXHAUSTED"}}}}}},"delete":{"tags":["Rewards"],"summary":"Cancel a pending reward claim","description":"Refunds any deducted points. Requires `rewards:write` scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Claim canceled","content":{"application/json":{"schema":{"type":"object"},"example":{"canceled":true}}}},"404":{"description":"No active claim found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/rewards/{id}/claim/status":{"get":{"tags":["Rewards"],"summary":"Get the caller's claim status for a reward","description":"Returns the user's current claim record on this reward (status, fulfilled-at, claim token). Requires `rewards:read` scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Claim status object"}}}},"/api/my/claims":{"get":{"tags":["Rewards"],"summary":"Caller's full reward claim history","description":"Returns every claim the user has made, with reward details + status. Requires `rewards:read` scope.","responses":{"200":{"description":"Claims array","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":"claim_abc","rewardId":"rwd_xyz","status":"fulfilled","createdAt":"2026-04-01T...","reward":{"title":"10% off","type":"discount","imageUrl":"/uploads/reward-...-512.webp"}}]}}}}}},"/api/my/points":{"get":{"tags":["Rewards"],"summary":"Caller's IRLRewards points balance and ledger","description":"Returns current balance plus the per-transaction history (earn/spend, source, timestamp). Requires `rewards:read` scope.","responses":{"200":{"description":"Balance + history","content":{"application/json":{"schema":{"type":"object"},"example":{"balance":350,"history":[{"type":"earn","amount":100,"source":"rsvp_completed","eventId":"7s5T...","at":"2026-04-15T..."},{"type":"spend","amount":-50,"source":"reward_claim","rewardId":"rwd_abc","at":"2026-04-20T..."}]}}}}}}},"/api/my/rewards":{"get":{"tags":["Rewards"],"summary":"Rewards the caller is currently eligible to claim","description":"Walks every active reward and returns those whose gates the user satisfies. Requires `rewards:read` scope.","responses":{"200":{"description":"Eligible rewards array"}}}},"/api/events/{eventId}/rewards":{"get":{"tags":["Rewards"],"security":[],"summary":"List rewards tied to a specific event","description":"Public — returns rewards configured to require attendance / RSVP at this event.","parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rewards array"}}}}}}