# Fan Passport Collector Profile Sharing API Specification **Milestone:** #7 Partner Integration **Artifact:** Collector profile sharing API specification **Status:** Proposed implementation contract **Last updated:** 2026-06-17 This document specifies the API surface for sharing Fan Passport collector profiles with fans, approved partners, and public share links. It is designed to support proposed integrations with Panini and other sticker/card providers, but it does **not** assume that any commercial agreement, credentials, catalogue licence, trademark permission, inventory feed, or production API access has already been granted by those providers. The specification is intentionally provider-neutral. It can support official partner albums, first-party virtual albums, licensed sticker/card datasets, and community-visible collection summaries without exposing sensitive user data or unauthorised third-party intellectual property. --- ## 1. Goals The Collector Profile Sharing API allows Fan Passport users to: 1. Share a controlled collector profile with other fans. 2. Share album progress, completion stats, achievements, and selected inventory information. 3. Create public, private, or partner-specific share links. 4. Grant and revoke partner access to selected profile and collection data. 5. Support future exchange and trading workflows by exposing duplicate/wishlist summaries where consented. 6. Preserve privacy, safety, and legal compliance by default. The API allows approved partners to: 1. Read user-authorised collector profile data. 2. Read user-authorised album summaries and item-level collection data. 3. Verify profile/share-link authenticity. 4. Link an external partner collector account to a Fan Passport collector identity. 5. Receive consent-aware profile update notifications via webhooks. --- ## 2. Non-goals The following are intentionally outside this API specification and belong to separate service contracts: 1. Payment processing for paid packs, premium albums, or marketplace transactions. 2. Legal contract terms for licensed content from Panini or any other card/sticker provider. 3. Physical goods fulfilment. 4. Official tournament ticketing or attendance verification. 5. Direct trading/exchange execution. This API may expose shareable duplicate/wishlist information, but full trade lifecycle design is covered separately in the virtual album and item-exchange platform design. 6. Anti-money-laundering controls for cash marketplaces. The proposed exchange platform should avoid cash consideration unless a future regulated marketplace is explicitly approved. --- ## 3. API principles | Principle | Requirement | |---|---| | Privacy by default | Profiles and item-level collections are private unless the user opts in. | | Consent is granular | Users choose which data categories each partner or share link may access. | | Partner-neutral model | Album and item identifiers support multiple providers and first-party albums. | | Least privilege | Tokens must carry only the scopes needed for the requested operation. | | Revocable access | Users can revoke partner access and public share links at any time. | | No sensitive serial leakage | Serial numbers, redemption codes, purchase details, device identifiers, and precise location history are never exposed through public profile APIs. | | Safety first | Children, vulnerable users, and users with private profiles receive stronger defaults and sharing restrictions. | | Auditability | Consent changes, partner reads, public share-link creation, and revocation events are logged. | | Compatibility | API uses conventional REST, JSON, OAuth 2.1 style flows, RFC 7807-style errors, and cursor pagination. | --- ## 4. Environments and base URLs Recommended environment layout: | Environment | Base URL | Use | |---|---|---| | Sandbox | `https://sandbox-api.fanpassport.example/v1` | Partner development, test users, synthetic albums. | | Staging | `https://staging-api.fanpassport.example/v1` | Pre-production integration testing with approved partners. | | Production | `https://api.fanpassport.example/v1` | Live users and approved production partners. | Final production hostnames should be set by the implementation team. The examples in this document use `https://api.fanpassport.example/v1`. --- ## 5. Data classification The API separates collector data into categories used by consent screens, scopes, audit logs, and privacy controls. | Category | Examples | Default exposure | |---|---|---| | `profile_basic` | Display name, avatar, country/region, favourite team, public bio | Private unless user creates a share link or grants partner access | | `profile_stats` | Total collected count, album completion percentage, achievement count | Private unless shared | | `album_summary` | Album title, provider, total slots, completed slots, duplicate count, wishlist count | Private unless shared | | `album_items` | Item names, rarity, owned/needed/duplicate status, tradability flag | Private; requires explicit opt-in | | `achievements` | Badges, challenge completions, trivia streaks | Private unless shared | | `exchange_intent` | Wants, duplicates available for trade, preferred exchange regions | Private; requires explicit opt-in | | `external_links` | Linked partner account aliases or verified external album references | Private; requires explicit opt-in | | `safety_limited` | Age band, minor/guardian restriction flags | Never directly exposed to public clients; used internally for policy decisions | | `sensitive_private` | Email, phone, date of birth, login identifiers, exact location, payment data, purchase history, device identifiers, fraud signals | Never exposed through this API | --- ## 6. Identity model ### 6.1 Collector identifier Each user has a stable API-facing collector identifier: ```text collectorId = col_01JZ9J6F1K8T39E0Y2R6M5B4XA ``` Rules: 1. `collectorId` is not the internal database user ID. 2. It is safe to show in authenticated partner APIs but should not be treated as a secret. 3. Public share URLs should use `shareToken`, not raw `collectorId`, unless the user has opted into a public profile handle. ### 6.2 Public handle Users may optionally reserve a public handle: ```text handle = lionesscollector26 ``` Rules: 1. Handles must be unique, moderated, and revocable for policy violations. 2. Handle-based profile access must respect profile visibility settings. 3. Underage accounts should not be eligible for public searchable handles unless guardian-approved and jurisdictionally permitted. ### 6.3 Partner subject mapping For approved partners, Fan Passport maintains a mapping between: ```text partnerId + partnerUserId <-> collectorId ``` Rules: 1. A partner cannot infer or request mappings for users who have not consented. 2. Partner user IDs are stored hashed or encrypted where feasible. 3. Unlinking a partner account must revoke active partner tokens for that collector. --- ## 7. Authentication and authorization ### 7.1 Supported clients | Client type | Authentication method | Typical use | |---|---|---| | First-party web/mobile app | OAuth 2.1 Authorization Code + PKCE | User profile sharing settings, own profile reads | | Approved partner app | OAuth 2.1 Authorization Code + PKCE | User-authorised partner access | | Approved partner backend | Client Credentials with signed JWT client assertion or mTLS | Server-to-server calls, webhooks, consent verification | | Public share viewer | No login or optional share password | Viewing user-created public/private share links | ### 7.2 Token format Access tokens should be JWTs signed by Fan Passport or opaque tokens introspected by the API gateway. JWT access tokens should contain at minimum: ```json { "iss": "https://auth.fanpassport.example", "sub": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "aud": "fanpassport-api", "exp": 1799999999, "iat": 1799996399, "jti": "tok_01JZ9MJZ82YH5PM1VMSK7A6E2P", "client_id": "partner_panini_sandbox", "scope": "profile:read albums:read.summary achievements:read", "consent_id": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "tenant": "fanpassport" } ``` ### 7.3 OAuth grant flows #### 7.3.1 User-authorised partner access Use Authorization Code + PKCE. 1. Partner redirects user to Fan Passport authorization URL. 2. Fan Passport authenticates the user. 3. Fan Passport displays consent screen with requested data categories and purpose. 4. User approves or rejects. 5. Fan Passport redirects to approved partner redirect URI with authorization code. 6. Partner exchanges code for access token and optional refresh token. 7. API enforces scopes, consent status, and user privacy settings on each request. Recommended authorization URL shape: ```text GET https://auth.fanpassport.example/oauth/authorize ?response_type=code &client_id=partner_panini_sandbox &redirect_uri=https%3A%2F%2Fpartner.example%2Foauth%2Fcallback &scope=profile:read%20albums:read.summary%20achievements:read &state=opaque-csrf-value &code_challenge=BASE64URL_SHA256 &code_challenge_method=S256 &audience=fanpassport-api ``` #### 7.3.2 Partner server-to-server access Use Client Credentials only for non-user-specific operations or for operations where the partner has a valid consent-bound subject reference. Server-to-server requests must include one of: 1. mTLS client certificate issued/approved by Fan Passport. 2. Signed JWT client assertion using partner-registered JWKS. Server-to-server tokens must not grant item-level user data unless tied to a valid `consent_id` or authorised partner subject mapping. #### 7.3.3 Public share access Public share endpoints may be accessed without OAuth. Access is controlled by: 1. High-entropy `shareToken`. 2. Optional password/PIN set by the user. 3. Optional expiry time. 4. Optional view count limit. 5. Optional audience restrictions. 6. Abuse detection and rate limiting. ### 7.4 Scopes | Scope | Allows | |---|---| | `profile:read` | Read approved basic collector profile fields. | | `profile:write` | Update own profile sharing preferences and public display fields. First-party only unless explicitly approved. | | `albums:read.summary` | Read approved album-level progress summaries. | | `albums:read.items` | Read approved item-level collection status. Requires explicit consent. | | `achievements:read` | Read approved achievement and badge summaries. | | `exchange:read.intent` | Read approved duplicate/wishlist/trade-intent summaries. | | `share-links:read` | Read user-created share-link metadata for own account. | | `share-links:write` | Create, update, or revoke share links for own account. | | `consent:read` | Read own consent grants and partner access records. | | `consent:write` | Create, update, or revoke consent grants. | | `partners:link` | Link/unlink an external partner account. | | `webhooks:manage` | Register and manage partner webhook subscriptions. Partner backend only. | | `offline_access` | Issue refresh token where permitted by consent and partner policy. | ### 7.5 Authorization matrix | Endpoint group | Public share token | User token | Partner user token | Partner client token | |---|---:|---:|---:|---:| | Own profile read/write | No | Yes | No | No | | Collector profile read | Only via share endpoint | Yes, if own or public/consented | Yes, if consented | Only consent-bound | | Album summaries | Only via share endpoint | Yes, if own or public/consented | Yes, if consented | Only consent-bound | | Item-level album data | Only if share permits | Yes, if own or consented | Yes, if explicit consent | Only explicit consent-bound | | Achievements | Only if share permits | Yes, if own or consented | Yes, if consented | Only consent-bound | | Consent records | No | Own account only | No | Metadata only where required | | Share-link management | No | Own account only | No | No | | Partner webhooks | No | No | No | Approved partner only | --- ## 8. Consent and privacy handling ### 8.1 Consent requirements Consent must be: 1. **Specific:** tied to a partner, share link, purpose, and data categories. 2. **Granular:** users can approve profile summary without approving item-level collection data. 3. **Informed:** consent screen must name the recipient and explain what will be shared. 4. **Revocable:** users can revoke at any time. 5. **Time-bound:** partner consents should expire by default. 6. **Auditable:** consent creation, modification, and revocation are logged. 7. **Jurisdiction-aware:** age, region, and guardian rules must be enforced. ### 8.2 Default consent durations | Access type | Default expiry | Maximum recommended expiry | |---|---:|---:| | Public share link | 30 days | 365 days | | Private share link | 7 days | 90 days | | Partner profile summary | 180 days | 365 days | | Partner album summary | 180 days | 365 days | | Partner item-level collection data | 90 days | 180 days | | Partner exchange intent | 30 days | 90 days | | Refresh token | Matches consent | Matches consent | ### 8.3 Data minimisation rules The API must not expose the following through collector sharing endpoints: 1. Email address. 2. Phone number. 3. Date of birth. 4. Exact age. 5. Payment details. 6. Purchase history. 7. Device identifiers. 8. Login provider identifiers. 9. Exact location or match attendance timestamps. 10. Fraud/risk scoring details. 11. Raw moderation reports. 12. Physical mailing address. 13. Redemption codes, pack codes, serial numbers, or unredeemed entitlements. ### 8.4 Minor and guardian rules Implementation must support a policy engine that can vary by jurisdiction. Recommended baseline: 1. Users under the configured digital consent age cannot create public profiles. 2. Users under the configured digital consent age cannot expose item-level duplicates/wishlists to strangers. 3. Guardian approval is required for partner sharing where legally required. 4. Minor profiles should not be search-indexable. 5. Direct contact information must never be exposed. 6. Exchange/trade intent for minors should be limited to guardian-approved, platform-mediated flows. ### 8.5 Revocation effects When a user revokes a consent grant or share link: 1. New API reads must fail immediately. 2. Cached responses must expire within the maximum cache TTL for that endpoint, recommended 60 seconds for profile data and 15 seconds for item-level data. 3. Refresh tokens associated with the consent must be revoked. 4. Webhook subscriptions tied only to that consent must no longer receive user-specific events. 5. The audit log records actor, timestamp, revoked scopes, and recipient. 6. Partners must be contractually required to delete or stop using previously retrieved data where required by privacy law and partner agreement. ### 8.6 Public indexing Default public share-link responses should include headers discouraging indexing unless the user explicitly opts into a public searchable profile: ```text X-Robots-Tag: noindex, nofollow Cache-Control: private, max-age=60 ``` For opt-in public handles: ```text Cache-Control: public, max-age=300 ``` --- ## 9. Common API conventions ### 9.1 Headers Clients should send: ```text Accept: application/json Content-Type: application/json Authorization: Bearer X-Request-Id: Idempotency-Key: ``` Responses include: ```text Content-Type: application/json X-Request-Id: req_01JZ9N00N8D9P8G41KYJC7A1QZ RateLimit-Limit: 300 RateLimit-Remaining: 299 RateLimit-Reset: 1799999999 ``` ### 9.2 Versioning The base URL carries the major version: ```text /v1 ``` Backward-compatible changes may add: 1. New optional request fields. 2. New response fields. 3. New enum values. 4. New endpoints. Breaking changes require a new major version. ### 9.3 Date and time format All timestamps use ISO 8601 UTC: ```text 2026-06-17T12:30:00Z ``` ### 9.4 Money and payments No payment fields are included in this API. ### 9.5 Pagination List endpoints use cursor pagination: Request: ```text GET /v1/collectors/{collectorId}/albums?limit=25&cursor=eyJwYWdlIjoyfQ ``` Response: ```json { "data": [], "pagination": { "limit": 25, "nextCursor": "eyJwYWdlIjozfQ", "hasMore": true } } ``` Rules: 1. `limit` default is 25. 2. Maximum `limit` is 100 unless otherwise documented. 3. Cursors are opaque and may expire. ### 9.6 Sorting Where supported: ```text ?sort=completionPercent,-updatedAt ``` A leading `-` means descending. ### 9.7 Filtering Filters use explicit query parameters: ```text ?provider=panini&edition=world-cup-2026&visibility=public ``` ### 9.8 Idempotency Mutating requests should accept `Idempotency-Key`. Required for: 1. Creating share links. 2. Updating consent grants. 3. Linking partner accounts. 4. Registering webhooks. The same key, method, path, authenticated actor, and request body should return the same result for at least 24 hours. ### 9.9 ETags and conditional requests Profile and album read endpoints should return `ETag`. Clients may send: ```text If-None-Match: "profile-7f83b165" ``` If unchanged, API may return: ```text 304 Not Modified ``` --- ## 10. Core resource schemas The following schemas describe canonical JSON shapes. Implementations may add fields, but must not remove or reinterpret these fields within `/v1`. ### 10.1 `CollectorProfile` ```json { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "regionCode": "GB-ENG", "preferredLanguage": "en", "favouriteTeams": [ { "teamId": "team_england_men", "name": "England", "code": "ENG", "type": "national_team" } ], "bio": "Collecting every England memory from 2026.", "joinedAt": "2026-05-01T10:00:00Z", "profileVisibility": "private", "shareSummary": { "albumCount": 3, "completedAlbumCount": 1, "totalItemsOwned": 412, "totalUniqueItemsOwned": 350, "totalDuplicateItems": 62, "wishlistItemCount": 28, "achievementCount": 19, "challengeCompletionPercent": 41.7 }, "badgesPreview": [ { "achievementId": "ach_watch_all_england_matches", "name": "England Loyalist", "iconUrl": "https://cdn.fanpassport.example/badges/england-loyalist.png", "earnedAt": "2026-06-28T21:45:00Z" } ], "links": { "self": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/profile", "albums": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums", "achievements": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/achievements" } } ``` Field notes: | Field | Notes | |---|---| | `collectorId` | API-facing collector ID. | | `handle` | Optional public handle. Omitted if not set or not visible to requester. | | `avatarUrl` | Must point to moderated media or a generated avatar. | | `countryCode` | ISO 3166-1 alpha-2 if shared. | | `regionCode` | ISO 3166-2 if shared and not privacy-restricted. | | `profileVisibility` | One of `private`, `link_only`, `public`, `partner_only`. | | `shareSummary` | Only contains categories allowed by requester consent/share settings. | | `badgesPreview` | Limited preview; full list requires achievements permission. | ### 10.2 `AlbumSummary` ```json { "albumId": "alb_worldcup_2026_panini_virtual", "provider": { "providerId": "provider_panini", "name": "Panini", "relationship": "proposed_partner" }, "title": "World Cup 2026 Virtual Sticker Album", "edition": "world-cup-2026", "albumType": "virtual_sticker_album", "coverImageUrl": "https://cdn.fanpassport.example/albums/worldcup-2026-cover.png", "visibility": "shared", "completion": { "totalSlots": 638, "uniqueOwned": 412, "missing": 226, "completionPercent": 64.58, "completedGroups": 4, "totalGroups": 16 }, "duplicates": { "totalDuplicateItems": 71, "tradableDuplicateItems": 53 }, "wishlist": { "wantedItems": 38, "priorityWantedItems": 9 }, "lastUpdatedAt": "2026-07-02T18:33:10Z", "lastSyncedAt": "2026-07-02T18:31:00Z", "links": { "self": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums/alb_worldcup_2026_panini_virtual", "items": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums/alb_worldcup_2026_panini_virtual/items" } } ``` Provider relationship values: | Value | Meaning | |---|---| | `first_party` | Created and operated by Fan Passport. | | `licensed_partner` | Operated under signed licence/partner agreement. | | `proposed_partner` | Designed for a prospective partner; not active until agreement exists. | | `user_imported` | User-provided collection metadata. | | `community_catalogue` | Community-maintained catalogue subject to moderation and rights review. | ### 10.3 `AlbumItem` ```json { "itemId": "itm_wc2026_eng_bellingham_001", "externalItemId": "PANINI-WC2026-ENG-001", "albumId": "alb_worldcup_2026_panini_virtual", "providerId": "provider_panini", "itemType": "sticker", "slotNumber": "ENG-01", "name": "England Midfielder", "displayTitle": "Jude Bellingham", "team": { "teamId": "team_england_men", "name": "England", "code": "ENG" }, "category": "player", "rarity": "standard", "ownership": { "status": "duplicate", "ownedCount": 3, "duplicateCount": 2, "wishlistPriority": null, "tradable": true, "lockedReason": null }, "media": { "thumbnailUrl": "https://cdn.fanpassport.example/items/itm_wc2026_eng_bellingham_001-thumb.png", "imageRights": "partner_or_platform_licensed" }, "updatedAt": "2026-07-02T18:33:10Z" } ``` Field notes: 1. `externalItemId` is optional and may be omitted for unlicensed or proposed catalogues. 2. `displayTitle` may be generic if licensing does not permit full player/card naming. 3. `ownedCount` should be capped or bucketed for public display if abuse risk is detected. 4. `lockedReason` may be `in_active_trade`, `account_restricted`, `partner_restricted`, `event_reward_locked`, or `not_tradable`. ### 10.4 `AchievementSummary` ```json { "achievementId": "ach_predict_giant_killing", "name": "Giant Killer", "description": "Correctly predicted a lower-ranked team would beat a favourite.", "category": "prediction", "tier": "gold", "iconUrl": "https://cdn.fanpassport.example/badges/giant-killer.png", "earnedAt": "2026-06-24T22:02:00Z", "visibility": "shared" } ``` ### 10.5 `ShareLink` ```json { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "shareToken": "sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "url": "https://fanpassport.example/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "name": "England collector card", "visibility": "link_only", "audience": "fans", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "includeItemLevelData": false, "includeExchangeIntent": false, "passwordProtected": false, "expiresAt": "2026-08-01T00:00:00Z", "maxViews": null, "viewCount": 18, "createdAt": "2026-07-01T12:00:00Z", "updatedAt": "2026-07-01T12:00:00Z", "revokedAt": null } ``` ### 10.6 `ConsentRecord` ```json { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "recipientType": "partner", "recipientId": "partner_panini_sandbox", "recipientName": "Panini Sandbox", "purpose": "Show your Fan Passport collection progress inside the partner album experience.", "status": "active", "scopes": [ "profile:read", "albums:read.summary", "achievements:read" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "createdAt": "2026-06-20T09:15:00Z", "updatedAt": "2026-06-20T09:15:00Z", "expiresAt": "2026-12-20T09:15:00Z", "revokedAt": null, "legalBasis": "consent", "guardianApproved": false } ``` Consent status values: | Value | Meaning | |---|---| | `pending` | Created but not yet active, for example awaiting guardian approval. | | `active` | Current and enforceable. | | `expired` | Time-bound access has expired. | | `revoked` | User or guardian revoked access. | | `superseded` | Replaced by a newer consent record. | | `denied` | User rejected requested access. | ### 10.7 `PartnerAccountLink` ```json { "linkId": "lnk_01JZ9Q0ZPQ54KRYW8HJJTVM0EP", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "partnerId": "partner_panini_sandbox", "partnerDisplayName": "Panini Sandbox", "partnerUserAlias": "panini_user_7H2K", "status": "linked", "linkedAt": "2026-06-21T14:25:00Z", "lastVerifiedAt": "2026-07-02T10:00:00Z", "unlinkedAt": null } ``` ### 10.8 `WebhookSubscription` ```json { "webhookId": "whk_01JZ9QB5WK5VN4Q9DK8GY6XP82", "partnerId": "partner_panini_sandbox", "url": "https://partner.example/webhooks/fanpassport", "events": [ "collector.profile.updated", "collector.album.updated", "collector.consent.revoked" ], "status": "active", "createdAt": "2026-06-21T10:00:00Z", "updatedAt": "2026-06-21T10:00:00Z" } ``` --- ## 11. Endpoint summary ### 11.1 User profile and sharing | Method | Path | Purpose | Required auth | |---|---|---|---| | `GET` | `/me/collector-profile` | Read own collector profile including private management fields | User token | | `PATCH` | `/me/collector-profile` | Update own public profile fields and sharing defaults | User token with `profile:write` | | `GET` | `/collectors/{collectorId}/profile` | Read collector profile according to visibility/consent | User or partner token with `profile:read` | | `GET` | `/profiles/{handle}` | Read public handle profile if enabled | Optional auth | | `POST` | `/me/share-links` | Create a profile share link | User token with `share-links:write` | | `GET` | `/me/share-links` | List own share links | User token with `share-links:read` | | `GET` | `/me/share-links/{shareId}` | Get own share-link metadata | User token with `share-links:read` | | `PATCH` | `/me/share-links/{shareId}` | Update own share link | User token with `share-links:write` | | `DELETE` | `/me/share-links/{shareId}` | Revoke own share link | User token with `share-links:write` | | `GET` | `/share/{shareToken}` | Resolve and view a share link | Public or optional auth | ### 11.2 Albums and items | Method | Path | Purpose | Required auth | |---|---|---|---| | `GET` | `/collectors/{collectorId}/albums` | List visible album summaries | User or partner token with `albums:read.summary` | | `GET` | `/collectors/{collectorId}/albums/{albumId}` | Get visible album summary | User or partner token with `albums:read.summary` | | `GET` | `/collectors/{collectorId}/albums/{albumId}/items` | List visible album items and ownership statuses | User or partner token with `albums:read.items` | | `GET` | `/share/{shareToken}/albums` | List albums exposed by a share link | Public share token | | `GET` | `/share/{shareToken}/albums/{albumId}` | Get album exposed by a share link | Public share token | | `GET` | `/share/{shareToken}/albums/{albumId}/items` | Get item-level data if share permits | Public share token, item sharing enabled | ### 11.3 Achievements | Method | Path | Purpose | Required auth | |---|---|---|---| | `GET` | `/collectors/{collectorId}/achievements` | List visible achievement summaries | User or partner token with `achievements:read` | | `GET` | `/share/{shareToken}/achievements` | List achievements exposed by share link | Public share token | ### 11.4 Consent | Method | Path | Purpose | Required auth | |---|---|---|---| | `GET` | `/me/consents` | List own consent records | User token with `consent:read` | | `GET` | `/me/consents/{consentId}` | Read own consent record | User token with `consent:read` | | `POST` | `/me/consents` | Create a consent grant outside OAuth redirect flow | User token with `consent:write` | | `PATCH` | `/me/consents/{consentId}` | Update own consent grant | User token with `consent:write` | | `DELETE` | `/me/consents/{consentId}` | Revoke own consent grant | User token with `consent:write` | | `GET` | `/partners/me/consents/{consentId}` | Partner verifies status of a consent known to it | Partner client token | ### 11.5 Partner account linking | Method | Path | Purpose | Required auth | |---|---|---|---| | `POST` | `/me/partner-links` | Link an external partner account | User token with `partners:link` | | `GET` | `/me/partner-links` | List linked partner accounts | User token | | `DELETE` | `/me/partner-links/{linkId}` | Unlink external partner account | User token with `partners:link` | | `GET` | `/partners/me/account-links/{linkId}` | Partner verifies a link known to it | Partner client token | ### 11.6 Partner webhooks | Method | Path | Purpose | Required auth | |---|---|---|---| | `POST` | `/partners/me/webhooks` | Register webhook subscription | Partner client token with `webhooks:manage` | | `GET` | `/partners/me/webhooks` | List webhook subscriptions | Partner client token with `webhooks:manage` | | `PATCH` | `/partners/me/webhooks/{webhookId}` | Update webhook subscription | Partner client token with `webhooks:manage` | | `DELETE` | `/partners/me/webhooks/{webhookId}` | Disable webhook subscription | Partner client token with `webhooks:manage` | --- ## 12. Endpoint details ## 12.1 Read own collector profile ```http GET /v1/me/collector-profile Authorization: Bearer Accept: application/json ``` Required scope: ```text profile:read ``` Response `200`: ```json { "data": { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "regionCode": "GB-ENG", "preferredLanguage": "en", "favouriteTeams": [ { "teamId": "team_england_men", "name": "England", "code": "ENG", "type": "national_team" } ], "bio": "Collecting every England memory from 2026.", "joinedAt": "2026-05-01T10:00:00Z", "profileVisibility": "private", "shareSummary": { "albumCount": 3, "completedAlbumCount": 1, "totalItemsOwned": 412, "totalUniqueItemsOwned": 350, "totalDuplicateItems": 62, "wishlistItemCount": 28, "achievementCount": 19, "challengeCompletionPercent": 41.7 }, "sharingDefaults": { "allowPublicHandle": false, "allowSearchIndexing": false, "defaultShareLinkExpiryDays": 30, "defaultDataCategories": [ "profile_basic", "profile_stats", "album_summary" ] }, "privacyRestrictions": { "minorRestricted": false, "guardianApprovalRequired": false, "itemLevelSharingAllowed": true, "exchangeIntentSharingAllowed": true } } } ``` Implementation notes: 1. `sharingDefaults` and `privacyRestrictions` are returned only on `/me`. 2. Private account safety flags must not be exposed on public profile endpoints. --- ## 12.2 Update own collector profile ```http PATCH /v1/me/collector-profile Authorization: Bearer Content-Type: application/json Idempotency-Key: 8c22be91-6b4c-4fd2-8b4f-76db1c7b7f66 ``` Required scope: ```text profile:write ``` Request: ```json { "displayName": "Lioness Collector", "handle": "lionesscollector26", "avatarMediaId": "med_01JZ9T1H3K1T5W0JDX7DABJ4WT", "countryCode": "GB", "regionCode": "GB-ENG", "preferredLanguage": "en", "favouriteTeamIds": [ "team_england_men" ], "bio": "Collecting every England memory from 2026.", "profileVisibility": "link_only", "sharingDefaults": { "allowPublicHandle": false, "allowSearchIndexing": false, "defaultShareLinkExpiryDays": 30, "defaultDataCategories": [ "profile_basic", "profile_stats", "album_summary" ] } } ``` Response `200`: ```json { "data": { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "regionCode": "GB-ENG", "preferredLanguage": "en", "favouriteTeams": [ { "teamId": "team_england_men", "name": "England", "code": "ENG", "type": "national_team" } ], "bio": "Collecting every England memory from 2026.", "profileVisibility": "link_only", "updatedAt": "2026-07-02T19:00:00Z" } } ``` Validation: | Field | Rule | |---|---| | `displayName` | 2-40 display characters after trimming. Moderated. | | `handle` | 3-24 characters, lowercase letters, numbers, underscore; must be unique. | | `avatarMediaId` | Must refer to uploaded media owned by the user and passed moderation. | | `bio` | Max 160 characters; moderated. | | `profileVisibility` | `private`, `link_only`, `public`, or `partner_only`. | | `allowSearchIndexing` | Can be true only when `profileVisibility` is `public` and account is eligible. | --- ## 12.3 Read collector profile by ID ```http GET /v1/collectors/{collectorId}/profile Authorization: Bearer Accept: application/json ``` Required scope: ```text profile:read ``` Path parameters: | Parameter | Type | Required | Description | |---|---|---:|---| | `collectorId` | string | Yes | API-facing collector identifier. | Response `200`: ```json { "data": { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "favouriteTeams": [ { "teamId": "team_england_men", "name": "England", "code": "ENG", "type": "national_team" } ], "bio": "Collecting every England memory from 2026.", "profileVisibility": "partner_only", "shareSummary": { "albumCount": 3, "completedAlbumCount": 1, "totalUniqueItemsOwned": 350, "achievementCount": 19 } }, "meta": { "accessBasis": "partner_consent", "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "dataCategories": [ "profile_basic", "profile_stats" ] } } ``` Access outcomes: | Condition | Response | |---|---| | Requester is profile owner | `200` with owner-visible fields. | | Profile is public and not restricted | `200` with public fields. | | Valid consent grants requester scope | `200` with consented fields. | | Valid share link exists but not used | `403` requiring share endpoint. | | No access | `404` to avoid profile enumeration, unless requester owns profile. | --- ## 12.4 Read profile by public handle ```http GET /v1/profiles/{handle} Accept: application/json ``` Authentication: 1. Optional. 2. If authenticated, the response may include additional consented fields. Response `200`: ```json { "data": { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "profileVisibility": "public", "shareSummary": { "albumCount": 3, "completedAlbumCount": 1, "totalUniqueItemsOwned": 350, "achievementCount": 19 } }, "meta": { "accessBasis": "public_profile" } } ``` Response `404` must be used when the handle is absent, suspended, private, or not visible to the requester. --- ## 12.5 Create share link ```http POST /v1/me/share-links Authorization: Bearer Content-Type: application/json Idempotency-Key: 82f5f2d1-27a8-48ce-9b77-47b0e8c0de31 ``` Required scope: ```text share-links:write ``` Request: ```json { "name": "My World Cup 2026 collection", "visibility": "link_only", "audience": "fans", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "includeItemLevelData": false, "includeExchangeIntent": false, "password": null, "expiresAt": "2026-08-01T00:00:00Z", "maxViews": null } ``` Response `201`: ```json { "data": { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "shareToken": "sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "url": "https://fanpassport.example/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "name": "My World Cup 2026 collection", "visibility": "link_only", "audience": "fans", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "includeItemLevelData": false, "includeExchangeIntent": false, "passwordProtected": false, "expiresAt": "2026-08-01T00:00:00Z", "maxViews": null, "viewCount": 0, "createdAt": "2026-07-01T12:00:00Z", "updatedAt": "2026-07-01T12:00:00Z", "revokedAt": null } } ``` Validation: | Field | Rule | |---|---| | `name` | 1-80 characters. | | `visibility` | `link_only`, `private_password`, or `public`; public requires eligibility. | | `allowedDataCategories` | Must be allowed by account safety policy. | | `includeItemLevelData` | Requires `album_items` category and item-level sharing eligibility. | | `includeExchangeIntent` | Requires `exchange_intent` category and exchange-sharing eligibility. | | `password` | Required for `private_password`; minimum 8 characters; stored as password hash only. | | `expiresAt` | Must not exceed maximum expiry allowed by account policy. | | `maxViews` | Optional integer from 1 to 1,000,000. | Safety requirements: 1. Share token must be generated from at least 128 bits of entropy. 2. Passwords must never be returned. 3. Link creation should trigger risk checks for spam/abuse. 4. Item-level data and exchange intent must be opt-in, not inherited silently from previous links. --- ## 12.6 List own share links ```http GET /v1/me/share-links?status=active&limit=25 Authorization: Bearer ``` Required scope: ```text share-links:read ``` Query parameters: | Parameter | Type | Required | Description | |---|---|---:|---| | `status` | string | No | `active`, `expired`, `revoked`, or `all`. Default `active`. | | `limit` | integer | No | 1-100. Default 25. | | `cursor` | string | No | Opaque pagination cursor. | Response `200`: ```json { "data": [ { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "url": "https://fanpassport.example/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "name": "My World Cup 2026 collection", "visibility": "link_only", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "includeItemLevelData": false, "includeExchangeIntent": false, "passwordProtected": false, "expiresAt": "2026-08-01T00:00:00Z", "viewCount": 18, "createdAt": "2026-07-01T12:00:00Z", "revokedAt": null } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false } } ``` --- ## 12.7 Update share link ```http PATCH /v1/me/share-links/{shareId} Authorization: Bearer Content-Type: application/json Idempotency-Key: 72654d06-9ec3-42a4-bc28-89fa79ad6f15 ``` Required scope: ```text share-links:write ``` Request: ```json { "name": "England collection progress", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary" ], "includeItemLevelData": false, "includeExchangeIntent": false, "expiresAt": "2026-07-15T00:00:00Z", "maxViews": 500 } ``` Response `200`: ```json { "data": { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "url": "https://fanpassport.example/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "name": "England collection progress", "visibility": "link_only", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary" ], "includeItemLevelData": false, "includeExchangeIntent": false, "passwordProtected": false, "expiresAt": "2026-07-15T00:00:00Z", "maxViews": 500, "viewCount": 18, "createdAt": "2026-07-01T12:00:00Z", "updatedAt": "2026-07-02T12:00:00Z", "revokedAt": null } } ``` Rules: 1. Reducing shared categories must take effect immediately. 2. Expired or revoked links cannot be reactivated with `PATCH`; create a new share link instead. 3. Updating a password must require `currentPassword` or recent user re-authentication. --- ## 12.8 Revoke share link ```http DELETE /v1/me/share-links/{shareId} Authorization: Bearer ``` Required scope: ```text share-links:write ``` Response `200`: ```json { "data": { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "status": "revoked", "revokedAt": "2026-07-02T13:00:00Z" } } ``` Alternative response `204 No Content` is acceptable if the implementation does not return a body. If using `204`, it must be consistent across delete endpoints. --- ## 12.9 Resolve public share link ```http GET /v1/share/{shareToken} Accept: application/json ``` Optional headers: ```text Authorization: Bearer X-Share-Password: ``` Response `200`: ```json { "data": { "shareId": "shr_01JZ9P2P3G70JTCG53E3Q6P6MS", "collector": { "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "handle": "lionesscollector26", "displayName": "Lioness Collector", "avatarUrl": "https://cdn.fanpassport.example/avatars/col_01JZ9J6.png", "countryCode": "GB", "favouriteTeams": [ { "teamId": "team_england_men", "name": "England", "code": "ENG", "type": "national_team" } ], "shareSummary": { "albumCount": 3, "completedAlbumCount": 1, "totalUniqueItemsOwned": 350, "achievementCount": 19 } }, "albumsPreview": [ { "albumId": "alb_worldcup_2026_panini_virtual", "provider": { "providerId": "provider_panini", "name": "Panini", "relationship": "proposed_partner" }, "title": "World Cup 2026 Virtual Sticker Album", "edition": "world-cup-2026", "completion": { "totalSlots": 638, "uniqueOwned": 412, "missing": 226, "completionPercent": 64.58 } } ], "achievementsPreview": [ { "achievementId": "ach_predict_giant_killing", "name": "Giant Killer", "category": "prediction", "tier": "gold", "earnedAt": "2026-06-24T22:02:00Z" } ] }, "meta": { "accessBasis": "share_link", "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "itemLevelDataAvailable": false, "exchangeIntentAvailable": false, "expiresAt": "2026-08-01T00:00:00Z" } } ``` Security and privacy behavior: | Condition | Response | |---|---| | Invalid token | `404` | | Revoked token | `404` | | Expired token | `410` | | Password required and missing | `401` with `share_password_required` | | Password incorrect | `403` with `share_password_invalid` | | View limit exceeded | `410` with `share_view_limit_exceeded` | | Account later restricted | `403` or `404` depending on policy | | Item-level data not included | Omit item links and return `itemLevelDataAvailable: false` | --- ## 12.10 List collector albums ```http GET /v1/collectors/{collectorId}/albums?provider=provider_panini&limit=25 Authorization: Bearer Accept: application/json ``` Required scope: ```text albums:read.summary ``` Response `200`: ```json { "data": [ { "albumId": "alb_worldcup_2026_panini_virtual", "provider": { "providerId": "provider_panini", "name": "Panini", "relationship": "proposed_partner" }, "title": "World Cup 2026 Virtual Sticker Album", "edition": "world-cup-2026", "albumType": "virtual_sticker_album", "coverImageUrl": "https://cdn.fanpassport.example/albums/worldcup-2026-cover.png", "visibility": "shared", "completion": { "totalSlots": 638, "uniqueOwned": 412, "missing": 226, "completionPercent": 64.58, "completedGroups": 4, "totalGroups": 16 }, "duplicates": { "totalDuplicateItems": 71, "tradableDuplicateItems": 53 }, "wishlist": { "wantedItems": 38, "priorityWantedItems": 9 }, "lastUpdatedAt": "2026-07-02T18:33:10Z", "lastSyncedAt": "2026-07-02T18:31:00Z" } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false }, "meta": { "accessBasis": "partner_consent", "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z" } } ``` Rules: 1. If consent allows album summary but not duplicates/wishlist, omit `duplicates` and `wishlist`. 2. If provider rights do not allow provider branding in the response, use a first-party album representation and omit protected names/media. 3. For proposed partners, mark `relationship` as `proposed_partner`. --- ## 12.11 Get collector album ```http GET /v1/collectors/{collectorId}/albums/{albumId} Authorization: Bearer Accept: application/json ``` Required scope: ```text albums:read.summary ``` Response `200`: ```json { "data": { "albumId": "alb_worldcup_2026_panini_virtual", "provider": { "providerId": "provider_panini", "name": "Panini", "relationship": "proposed_partner" }, "title": "World Cup 2026 Virtual Sticker Album", "edition": "world-cup-2026", "albumType": "virtual_sticker_album", "coverImageUrl": "https://cdn.fanpassport.example/albums/worldcup-2026-cover.png", "visibility": "shared", "completion": { "totalSlots": 638, "uniqueOwned": 412, "missing": 226, "completionPercent": 64.58, "completedGroups": 4, "totalGroups": 16 }, "groups": [ { "groupId": "grp_england", "name": "England", "totalSlots": 26, "uniqueOwned": 22, "missing": 4, "completionPercent": 84.62 } ], "duplicates": { "totalDuplicateItems": 71, "tradableDuplicateItems": 53 }, "wishlist": { "wantedItems": 38, "priorityWantedItems": 9 }, "lastUpdatedAt": "2026-07-02T18:33:10Z", "lastSyncedAt": "2026-07-02T18:31:00Z" } } ``` --- ## 12.12 List album items ```http GET /v1/collectors/{collectorId}/albums/{albumId}/items?ownershipStatus=duplicate&tradable=true&limit=50 Authorization: Bearer Accept: application/json ``` Required scope: ```text albums:read.items ``` Query parameters: | Parameter | Type | Required | Description | |---|---|---:|---| | `ownershipStatus` | string | No | `owned`, `missing`, `duplicate`, `wishlist`, `any`. | | `tradable` | boolean | No | Filter by tradability. | | `teamId` | string | No | Filter by team. | | `category` | string | No | Filter by item category. | | `rarity` | string | No | Filter by rarity. | | `limit` | integer | No | 1-100. Default 25. | | `cursor` | string | No | Opaque pagination cursor. | Response `200`: ```json { "data": [ { "itemId": "itm_wc2026_eng_bellingham_001", "externalItemId": "PANINI-WC2026-ENG-001", "albumId": "alb_worldcup_2026_panini_virtual", "providerId": "provider_panini", "itemType": "sticker", "slotNumber": "ENG-01", "name": "England Midfielder", "displayTitle": "Jude Bellingham", "team": { "teamId": "team_england_men", "name": "England", "code": "ENG" }, "category": "player", "rarity": "standard", "ownership": { "status": "duplicate", "ownedCount": 3, "duplicateCount": 2, "wishlistPriority": null, "tradable": true, "lockedReason": null }, "media": { "thumbnailUrl": "https://cdn.fanpassport.example/items/itm_wc2026_eng_bellingham_001-thumb.png", "imageRights": "partner_or_platform_licensed" }, "updatedAt": "2026-07-02T18:33:10Z" } ], "pagination": { "limit": 50, "nextCursor": null, "hasMore": false }, "meta": { "accessBasis": "partner_consent", "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "itemLevelDataShared": true } } ``` Privacy constraints: 1. Requires explicit `album_items` data category consent. 2. Should not return acquisition source or purchase timestamp. 3. Should not return unique serials, redemption codes, or blockchain wallet addresses. 4. Public share links should not expose rare/high-value item quantities if fraud risk is high; the API may bucket values such as `ownedCount: 2` with `ownedCountDisplay: "2+"`. --- ## 12.13 List albums through share link ```http GET /v1/share/{shareToken}/albums?limit=25 Accept: application/json ``` Response `200`: ```json { "data": [ { "albumId": "alb_worldcup_2026_panini_virtual", "provider": { "providerId": "provider_panini", "name": "Panini", "relationship": "proposed_partner" }, "title": "World Cup 2026 Virtual Sticker Album", "edition": "world-cup-2026", "completion": { "totalSlots": 638, "uniqueOwned": 412, "missing": 226, "completionPercent": 64.58 } } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false } } ``` --- ## 12.14 List item-level data through share link ```http GET /v1/share/{shareToken}/albums/{albumId}/items?ownershipStatus=duplicate Accept: application/json ``` Response when item-level sharing is enabled: ```json { "data": [ { "itemId": "itm_wc2026_eng_bellingham_001", "albumId": "alb_worldcup_2026_panini_virtual", "itemType": "sticker", "slotNumber": "ENG-01", "displayTitle": "Jude Bellingham", "team": { "teamId": "team_england_men", "name": "England", "code": "ENG" }, "category": "player", "rarity": "standard", "ownership": { "status": "duplicate", "duplicateCount": 2, "tradable": true } } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false } } ``` Response when item-level sharing is not enabled: ```http 403 Forbidden Content-Type: application/problem+json ``` ```json { "type": "https://api.fanpassport.example/problems/insufficient_share_permission", "title": "Share link does not allow this data", "status": 403, "code": "insufficient_share_permission", "detail": "This share link does not include item-level album data.", "instance": "/v1/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ/albums/alb_worldcup_2026_panini_virtual/items", "requestId": "req_01JZ9X6FDSB09WSY4DF6PHASVB" } ``` --- ## 12.15 List achievements ```http GET /v1/collectors/{collectorId}/achievements?category=prediction&limit=25 Authorization: Bearer Accept: application/json ``` Required scope: ```text achievements:read ``` Response `200`: ```json { "data": [ { "achievementId": "ach_predict_giant_killing", "name": "Giant Killer", "description": "Correctly predicted a lower-ranked team would beat a favourite.", "category": "prediction", "tier": "gold", "iconUrl": "https://cdn.fanpassport.example/badges/giant-killer.png", "earnedAt": "2026-06-24T22:02:00Z", "visibility": "shared" } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false } } ``` Rules: 1. Hidden, safety-restricted, or deprecated achievements must be omitted. 2. Achievements containing sensitive inference, such as exact location or purchase activity, should be converted to safe display text or excluded. --- ## 12.16 List own consents ```http GET /v1/me/consents?status=active&recipientType=partner Authorization: Bearer Accept: application/json ``` Required scope: ```text consent:read ``` Response `200`: ```json { "data": [ { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "recipientType": "partner", "recipientId": "partner_panini_sandbox", "recipientName": "Panini Sandbox", "purpose": "Show your Fan Passport collection progress inside the partner album experience.", "status": "active", "scopes": [ "profile:read", "albums:read.summary", "achievements:read" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "createdAt": "2026-06-20T09:15:00Z", "updatedAt": "2026-06-20T09:15:00Z", "expiresAt": "2026-12-20T09:15:00Z", "revokedAt": null, "legalBasis": "consent", "guardianApproved": false } ], "pagination": { "limit": 25, "nextCursor": null, "hasMore": false } } ``` --- ## 12.17 Create consent grant This endpoint supports first-party consent management outside the OAuth redirect flow, for example connecting a partner inside the Fan Passport settings UI. ```http POST /v1/me/consents Authorization: Bearer Content-Type: application/json Idempotency-Key: 69952ec6-6778-4af7-8232-f2e70d928e0d ``` Required scope: ```text consent:write ``` Request: ```json { "recipientType": "partner", "recipientId": "partner_panini_sandbox", "purpose": "Show my Fan Passport collection progress in the partner album experience.", "scopes": [ "profile:read", "albums:read.summary", "achievements:read" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "expiresAt": "2026-12-20T09:15:00Z" } ``` Response `201`: ```json { "data": { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "recipientType": "partner", "recipientId": "partner_panini_sandbox", "recipientName": "Panini Sandbox", "purpose": "Show my Fan Passport collection progress in the partner album experience.", "status": "active", "scopes": [ "profile:read", "albums:read.summary", "achievements:read" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "createdAt": "2026-06-20T09:15:00Z", "updatedAt": "2026-06-20T09:15:00Z", "expiresAt": "2026-12-20T09:15:00Z", "revokedAt": null, "legalBasis": "consent", "guardianApproved": false } } ``` Rules: 1. Partner must be approved and active. 2. Requested scopes must be allowed for that partner. 3. Requested data categories must map to requested scopes. 4. If guardian approval is required, return `202 Accepted` with status `pending`. --- ## 12.18 Update consent grant ```http PATCH /v1/me/consents/{consentId} Authorization: Bearer Content-Type: application/json Idempotency-Key: 7051d5af-6cd1-4478-b19b-bb4259032b0b ``` Required scope: ```text consent:write ``` Request: ```json { "scopes": [ "profile:read", "albums:read.summary" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "expiresAt": "2026-10-01T00:00:00Z" } ``` Response `200`: ```json { "data": { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "status": "active", "scopes": [ "profile:read", "albums:read.summary" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "updatedAt": "2026-07-02T14:00:00Z", "expiresAt": "2026-10-01T00:00:00Z" } } ``` Rules: 1. Reducing access should update the current consent in place. 2. Expanding access may require a new explicit consent interaction and may create a superseding consent record. 3. Existing refresh tokens exceeding the new scopes must be revoked. --- ## 12.19 Revoke consent grant ```http DELETE /v1/me/consents/{consentId} Authorization: Bearer ``` Required scope: ```text consent:write ``` Response `200`: ```json { "data": { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "status": "revoked", "revokedAt": "2026-07-02T14:30:00Z" } } ``` Side effects: 1. Associated refresh tokens revoked. 2. Partner subject mapping retained only if still needed for audit/account-link purposes. 3. User-specific webhooks stop. 4. Audit event emitted. 5. Partner revocation webhook sent where configured. --- ## 12.20 Partner verifies consent ```http GET /v1/partners/me/consents/{consentId} Authorization: Bearer Accept: application/json ``` Required scope: ```text consent:read ``` Response `200`: ```json { "data": { "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "recipientId": "partner_panini_sandbox", "status": "active", "scopes": [ "profile:read", "albums:read.summary", "achievements:read" ], "dataCategories": [ "profile_basic", "profile_stats", "album_summary", "achievements" ], "albumIds": [ "alb_worldcup_2026_panini_virtual" ], "expiresAt": "2026-12-20T09:15:00Z", "updatedAt": "2026-06-20T09:15:00Z" } } ``` Rules: 1. Partner can verify only consents where it is the recipient. 2. Response does not include user profile data. 3. If consent is unknown to partner, return `404`. --- ## 12.21 Link partner account ```http POST /v1/me/partner-links Authorization: Bearer Content-Type: application/json Idempotency-Key: 2931aa28-8f3f-4a15-878c-a4c9e5b53a11 ``` Required scope: ```text partners:link ``` Request: ```json { "partnerId": "partner_panini_sandbox", "partnerAuthorizationCode": "partner_one_time_code_abc123", "requestedConsentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z" } ``` Response `201`: ```json { "data": { "linkId": "lnk_01JZ9Q0ZPQ54KRYW8HJJTVM0EP", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "partnerId": "partner_panini_sandbox", "partnerDisplayName": "Panini Sandbox", "partnerUserAlias": "panini_user_7H2K", "status": "linked", "linkedAt": "2026-06-21T14:25:00Z", "lastVerifiedAt": "2026-07-02T10:00:00Z", "unlinkedAt": null } } ``` Rules: 1. Partner linking requires a one-time code, signed assertion, or OAuth token from the partner proving the user controls the external account. 2. Partner account linking does not automatically grant data access beyond the attached consent. 3. Unlinking should revoke partner-specific refresh tokens. --- ## 12.22 Unlink partner account ```http DELETE /v1/me/partner-links/{linkId} Authorization: Bearer ``` Required scope: ```text partners:link ``` Response `200`: ```json { "data": { "linkId": "lnk_01JZ9Q0ZPQ54KRYW8HJJTVM0EP", "status": "unlinked", "unlinkedAt": "2026-07-02T15:00:00Z" } } ``` --- ## 12.23 Register partner webhook ```http POST /v1/partners/me/webhooks Authorization: Bearer Content-Type: application/json Idempotency-Key: 2ad02672-34cf-462e-a7d5-cba9e978cd24 ``` Required scope: ```text webhooks:manage ``` Request: ```json { "url": "https://partner.example/webhooks/fanpassport", "events": [ "collector.profile.updated", "collector.album.updated", "collector.consent.revoked" ], "description": "Sandbox profile sync webhook" } ``` Response `201`: ```json { "data": { "webhookId": "whk_01JZ9QB5WK5VN4Q9DK8GY6XP82", "partnerId": "partner_panini_sandbox", "url": "https://partner.example/webhooks/fanpassport", "events": [ "collector.profile.updated", "collector.album.updated", "collector.consent.revoked" ], "status": "active", "createdAt": "2026-06-21T10:00:00Z", "updatedAt": "2026-06-21T10:00:00Z" } } ``` Webhook URL rules: 1. Must be HTTPS. 2. Must not point to localhost, private IP ranges, or link-local addresses. 3. Must pass endpoint verification challenge. 4. Partner must rotate secrets at least every 180 days. --- ## 13. Webhooks ### 13.1 Event types | Event | Trigger | |---|---| | `collector.profile.updated` | Consented collector changes shareable profile fields. | | `collector.album.updated` | Consented collector album summary or item state changes. | | `collector.achievements.updated` | Consented collector earns or changes visible achievements. | | `collector.consent.updated` | Consent scope/category/expiry changed. | | `collector.consent.revoked` | Consent revoked or expired. | | `collector.partner_link.unlinked` | User unlinks partner account. | ### 13.2 Webhook delivery Webhook request: ```http POST /webhooks/fanpassport HTTP/1.1 Host: partner.example Content-Type: application/json X-FanPassport-Webhook-Id: evt_01JZ9QSFNYDASWPWZ5FWNXRW3N X-FanPassport-Timestamp: 2026-07-02T16:00:00Z X-FanPassport-Signature: v1=base64url_hmac_sha256_signature ``` Payload: ```json { "eventId": "evt_01JZ9QSFNYDASWPWZ5FWNXRW3N", "eventType": "collector.album.updated", "occurredAt": "2026-07-02T16:00:00Z", "partnerId": "partner_panini_sandbox", "collectorId": "col_01JZ9J6F1K8T39E0Y2R6M5B4XA", "consentId": "cns_01JZ9MK7VDMS9RWSMT1AP0YK9Z", "resource": { "type": "album", "id": "alb_worldcup_2026_panini_virtual", "url": "https://api.fanpassport.example/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums/alb_worldcup_2026_panini_virtual" } } ``` Delivery rules: 1. Sign payload using HMAC-SHA256 over timestamp + raw body, or use asymmetric signatures for high-assurance partners. 2. Include replay protection with timestamp tolerance, recommended 5 minutes. 3. Retry with exponential backoff for 24 hours. 4. Disable webhook after repeated failures, recommended after 50 consecutive failed attempts or 7 days of failure. 5. Do not include item-level data in webhook payloads; partners must fetch using consented API access. --- ## 14. Rate limits Rate limits should be enforced by API gateway and application-level abuse controls. ### 14.1 Standard limits | Actor / endpoint class | Limit | |---|---:| | Public share reads per IP | 60 requests/minute and 1,000 requests/day | | Public handle reads per IP | 120 requests/minute and 5,000 requests/day | | Authenticated user reads | 300 requests/minute and 10,000 requests/day | | Authenticated user writes | 60 requests/minute and 2,000 requests/day | | Share-link creation | 10 requests/minute and 100 requests/day | | Consent changes | 30 requests/minute and 500 requests/day | | Item-level album reads | 120 requests/minute and 20,000 items/day per actor | | Partner user-token reads | 600 requests/minute per client and 100,000 requests/day | | Partner server-to-server reads | 600 requests/minute per client and contract-specific daily quota | | Partner webhook registration changes | 10 requests/minute and 100 requests/day | | Data export requests | 5 requests/day | ### 14.2 Burst handling Recommended token bucket behavior: 1. Small bursts up to 2x minute limit may be allowed for authenticated partners with good standing. 2. Public endpoints should use stricter burst protection. 3. Repeated 429 responses should trigger client backoff. 4. Suspicious traffic should be challenged or blocked before application processing. ### 14.3 Rate-limit response ```http 429 Too Many Requests Content-Type: application/problem+json Retry-After: 60 RateLimit-Limit: 60 RateLimit-Remaining: 0 RateLimit-Reset: 1799999999 ``` ```json { "type": "https://api.fanpassport.example/problems/rate_limit_exceeded", "title": "Rate limit exceeded", "status": 429, "code": "rate_limit_exceeded", "detail": "Too many requests for this endpoint. Retry after 60 seconds.", "instance": "/v1/share/sht_5zL5x0p4z7PpY7mW9sV2B1nQ", "requestId": "req_01JZ9R0MZ7HTBAQDKW0EGQV8J8", "retryAfterSeconds": 60 } ``` --- ## 15. Error format All JSON errors use `application/problem+json` and follow RFC 7807 conventions with Fan Passport extensions. Schema: ```json { "type": "https://api.fanpassport.example/problems/validation_failed", "title": "Validation failed", "status": 400, "code": "validation_failed", "detail": "One or more request fields are invalid.", "instance": "/v1/me/share-links", "requestId": "req_01JZ9R7EQ0BC2Y5WGH3EB2GK7K", "errors": [ { "field": "expiresAt", "reason": "must_not_exceed_maximum_expiry", "message": "Share links for this account can expire no later than 2026-08-01T00:00:00Z." } ] } ``` Required fields: | Field | Type | Required | Description | |---|---|---:|---| | `type` | URI string | Yes | Stable documentation URL for the problem type. | | `title` | string | Yes | Human-readable short title. | | `status` | integer | Yes | HTTP status code. | | `code` | string | Yes | Stable machine-readable code. | | `detail` | string | Yes | Human-readable detail safe for the requester. | | `instance` | string | Yes | Request path or opaque incident URI. | | `requestId` | string | Yes | Correlation ID for support/debugging. | | `errors` | array | No | Field-level validation issues. | ### 15.1 Common errors | HTTP | Code | Meaning | |---:|---|---| | 400 | `invalid_request` | Malformed request body, bad query parameter, or invalid JSON. | | 400 | `validation_failed` | Request shape is valid but field values are not. | | 401 | `authentication_required` | Missing access token. | | 401 | `invalid_token` | Token expired, malformed, revoked, wrong audience, or invalid signature. | | 401 | `share_password_required` | Share link requires a password. | | 403 | `insufficient_scope` | Token lacks required OAuth scope. | | 403 | `consent_required` | User has not granted required data access. | | 403 | `consent_expired` | Consent existed but expired. | | 403 | `consent_revoked` | Consent was revoked. | | 403 | `privacy_restricted` | Account safety/privacy policy blocks sharing. | | 403 | `partner_not_approved` | Partner is not authorised for this operation. | | 403 | `insufficient_share_permission` | Share link does not expose requested data category. | | 403 | `share_password_invalid` | Supplied share password is incorrect. | | 404 | `resource_not_found` | Resource absent or not visible to requester. | | 409 | `conflict` | State conflict, such as duplicate handle. | | 409 | `idempotency_conflict` | Same idempotency key used with different request body. | | 410 | `share_link_expired` | Share link expired. | | 410 | `share_view_limit_exceeded` | Share link maximum views reached. | | 422 | `moderation_rejected` | Submitted profile field failed moderation. | | 423 | `resource_locked` | Resource temporarily locked, for example during trade settlement. | | 429 | `rate_limit_exceeded` | Rate limit exceeded. | | 500 | `internal_error` | Unexpected server error. | | 502 | `partner_upstream_error` | Partner service failed during account verification. | | 503 | `service_unavailable` | Temporary outage or maintenance. | ### 15.2 Insufficient scope example ```http 403 Forbidden Content-Type: application/problem+json ``` ```json { "type": "https://api.fanpassport.example/problems/insufficient_scope", "title": "Insufficient scope", "status": 403, "code": "insufficient_scope", "detail": "This endpoint requires scope albums:read.items.", "instance": "/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums/alb_worldcup_2026_panini_virtual/items", "requestId": "req_01JZ9RM7VHD11WAZTQ1N3NWY1A", "requiredScopes": [ "albums:read.items" ] } ``` ### 15.3 Consent required example ```http 403 Forbidden Content-Type: application/problem+json ``` ```json { "type": "https://api.fanpassport.example/problems/consent_required", "title": "Consent required", "status": 403, "code": "consent_required", "detail": "The collector has not granted this partner access to item-level album data.", "instance": "/v1/collectors/col_01JZ9J6F1K8T39E0Y2R6M5B4XA/albums/alb_worldcup_2026_panini_virtual/items", "requestId": "req_01JZ9RT8YW9T3K9YF5HP32FDE3", "requiredDataCategories": [ "album_items" ] } ``` --- ## 16. Partner approval and onboarding requirements Before a partner can access production APIs, Fan Passport should require: 1. Signed partner agreement. 2. Approved data processing agreement where personal data is shared. 3. Approved content/licence terms for any protected sticker/card catalogue, imagery, trademarks, or player likenesses. 4. Security review. 5. Privacy review. 6. Production redirect URI allowlisting. 7. JWKS or mTLS certificate registration. 8. Webhook endpoint verification. 9. Rate-limit tier assignment. 10. Sandbox certification against test cases. 11. Incident contact and support escalation channel. 12. Revocation/deletion process confirmation. ### 16.1 Partner metadata Partner registry entry: ```json { "partnerId": "partner_panini_sandbox", "displayName": "Panini Sandbox", "status": "sandbox_active", "relationship": "proposed_partner", "allowedScopes": [ "profile:read", "albums:read.summary", "albums:read.items", "achievements:read", "exchange:read.intent" ], "allowedDataCategories": [ "profile_basic", "profile_stats", "album_summary", "album_items", "achievements", "exchange_intent" ], "redirectUris": [ "https://partner.example/oauth/callback" ], "jwksUri": "https://partner.example/.well-known/jwks.json", "webhookAllowedDomains": [ "partner.example" ], "privacyPolicyUrl": "https://partner.example/privacy", "termsUrl": "https://partner.example/terms", "supportEmail": "privacy@partner.example", "createdAt": "2026-06-01T00:00:00Z" } ``` Partner status values: | Status | Meaning | |---|---| | `sandbox_pending` | Registered but not approved for sandbox calls. | | `sandbox_active` | Approved for sandbox with synthetic data. | | `production_pending` | Under production review. | | `production_active` | Approved for live users. | | `suspended` | Temporarily blocked. | | `terminated` | Permanently disabled. | --- ## 17. Public profile and share-page rendering contract The API should support both native app rendering and web share pages. ### 17.1 Share page display sections A public share page may render these sections only if present in API response: 1. Collector header. 2. Favourite teams. 3. Album progress cards. 4. Achievement highlights. 5. Wishlist summary. 6. Duplicate/tradable summary. 7. Safe call-to-action: “Open Fan Passport” or “Request exchange through Fan Passport.” ### 17.2 Required safety UX Share pages must include: 1. Report profile/share link control. 2. Block collector control for authenticated viewers. 3. No direct display of email, phone, external messaging handles, or exact location. 4. Clear expiry/visibility indicator. 5. “Data shared by collector” notice. 6. Partner relationship disclaimer where provider is not officially integrated. Recommended disclaimer for proposed partner data: ```text This virtual album is designed for compatibility with a proposed sticker/card provider integration. Official partner content, branding, and inventory sync require a signed agreement with the provider. ``` --- ## 18. Audit logging Audit logs should capture: | Event | Actor | Required fields | |---|---|---| | `profile.updated` | User | Changed fields, request ID, IP/device risk metadata | | `share_link.created` | User | Share ID, data categories, expiry | | `share_link.updated` | User | Share ID, previous/new data categories | | `share_link.revoked` | User | Share ID, revocation time | | `share_link.viewed` | Public/authenticated viewer | Share ID, coarse IP hash, user agent hash, timestamp | | `consent.created` | User/guardian | Recipient, scopes, data categories, expiry | | `consent.updated` | User/guardian | Previous/new scopes/categories | | `consent.revoked` | User/guardian/system | Recipient, revocation reason | | `partner.profile.read` | Partner | Partner ID, collector ID, consent ID, endpoint | | `partner.album.read` | Partner | Partner ID, collector ID, album ID, consent ID | | `partner.account_linked` | User/partner | Partner ID, link ID | | `partner.account_unlinked` | User/system | Partner ID, link ID | Retention recommendation: 1. Security/audit logs: 18-24 months unless legal requirements differ. 2. Public share view logs: minimise and aggregate where possible; raw logs no longer than necessary. 3. Consent records: retain as required to prove consent and revocation history. --- ## 19. Caching and freshness | Endpoint | Recommended cache | |---|---:| | `/share/{shareToken}` | 60 seconds, private/noindex by default | | `/profiles/{handle}` | 300 seconds if public and indexable; otherwise 60 seconds | | `/collectors/{collectorId}/profile` | 60 seconds | | Album summaries | 60 seconds | | Item-level album data | 15 seconds | | Achievements | 300 seconds | | Consent records | No shared cache | | Share-link management | No shared cache | Invalidation triggers: 1. Profile updated. 2. Share link changed/revoked. 3. Consent changed/revoked. 4. Album inventory updated. 5. User account safety restriction changed. 6. Partner suspended. --- ## 20. Security controls ### 20.1 API security Required: 1. TLS 1.2+; TLS 1.3 preferred. 2. Strict redirect URI matching for OAuth. 3. PKCE for all public clients. 4. Token audience validation. 5. Scope and consent enforcement on every request. 6. Request ID propagation. 7. Centralised rate limiting. 8. Abuse monitoring for profile scraping. 9. SSRF protection for webhook registration. 10. Idempotency for mutating requests. 11. Secrets stored in managed secret storage. 12. Principle-of-least-privilege service credentials. 13. Structured logging without sensitive payload leakage. ### 20.2 Public sharing abuse prevention Required: 1. High-entropy share tokens. 2. Optional password protection. 3. Expiry dates. 4. Maximum view counts. 5. Report/block flows. 6. Automated detection for spammy profile text, offensive handles, and impersonation. 7. Ability to globally revoke all share links for a restricted account. 8. Ability to disable item-level sharing during active abuse campaigns. ### 20.3 Partner abuse prevention Required: 1. Partner-level rate limits. 2. Scope allowlists per partner. 3. Per-partner anomaly detection. 4. Consent-bound access logs. 5. Periodic partner recertification. 6. Emergency partner suspension. 7. Webhook signing. 8. Contractual deletion obligations after consent revocation. --- ## 21. Moderation and safety handling Profile and sharing fields that require moderation: 1. Display name. 2. Handle. 3. Bio. 4. Avatar. 5. Share-link name. 6. Any user-generated album nickname. 7. Any user-generated trade/exchange note exposed through future APIs. Moderation actions: | Action | Effect | |---|---| | `allow` | Content visible. | | `soft_block` | Content hidden pending review; user can edit. | | `hard_block` | Content rejected; user cannot reuse same content. | | `shadow_limit` | Sharing/discovery reduced while risk review is pending. | | `account_restrict` | Disable public profile, share links, and exchange intent. | API behavior: 1. Moderation rejection on write returns `422 moderation_rejected`. 2. Existing public content later rejected should be removed from share responses. 3. Partner responses should omit rejected user-generated content even if consented. --- ## 22. Data export and deletion considerations Although detailed DSAR/export APIs may belong to account services, profile-sharing implementation must support: 1. Exporting active share links. 2. Exporting consent history. 3. Exporting partner access history at a reasonable level. 4. Deleting/revoking public share links upon account deletion. 5. Notifying partners of revoked/deleted consent where required. 6. Removing public handles upon account deletion. 7. Retaining minimal audit records where legally permitted/required. Recommended endpoint, if account services do not already provide one: ```http POST /v1/me/privacy/export Authorization: Bearer ``` This endpoint is optional for the collector sharing API if a broader platform privacy export API already exists. --- ## 23. Example integration flows ## 23.1 Fan creates a public collection card 1. Fan opens Fan Passport sharing settings. 2. App calls `GET /v1/me/collector-profile`. 3. Fan selects album summary and achievements, excluding item-level data. 4. App calls `POST /v1/me/share-links`. 5. Fan receives `url`. 6. Public viewer opens `/share/{shareToken}`. 7. Share page displays safe profile summary, album progress, and badge highlights. Primary endpoints: ```text GET /v1/me/collector-profile POST /v1/me/share-links GET /v1/share/{shareToken} GET /v1/share/{shareToken}/albums GET /v1/share/{shareToken}/achievements ``` ## 23.2 Partner shows Fan Passport progress inside its app 1. Partner sends user through OAuth Authorization Code + PKCE. 2. Fan Passport displays consent request: - basic profile - album summary - achievements 3. User approves. 4. Partner receives access token. 5. Partner calls: - `GET /v1/collectors/{collectorId}/profile` - `GET /v1/collectors/{collectorId}/albums` - `GET /v1/collectors/{collectorId}/achievements` 6. Partner caches data within permitted TTL. 7. If user revokes consent, partner receives `collector.consent.revoked` webhook and stops using the data. ## 23.3 Fan shares duplicates for exchange discovery 1. Fan creates a link with: - `album_summary` - `album_items` - `exchange_intent` - `includeItemLevelData: true` - `includeExchangeIntent: true` 2. API verifies user is eligible for exchange-intent sharing. 3. Public viewer can see duplicate/wishlist summary but no direct contact details. 4. Any exchange request must route through the Fan Passport platform, not external contact information. Primary endpoints: ```text POST /v1/me/share-links GET /v1/share/{shareToken} GET /v1/share/{shareToken}/albums/{albumId}/items?ownershipStatus=duplicate ``` --- ## 24. Implementation checklist ### 24.1 API gateway - Enforce TLS. - Validate JWTs or introspect opaque tokens. - Apply rate limits. - Attach request IDs. - Apply IP reputation controls for public endpoints. - Enforce maximum request body size. - Emit structured logs. ### 24.2 Application service - Enforce consent and privacy policy per field. - Resolve share links safely. - Generate high-entropy share tokens. - Manage idempotency keys. - Apply moderation status to profile fields. - Emit audit events. - Send webhooks without leaking sensitive payloads. - Handle partner suspension immediately. ### 24.3 Data layer - Store collector API IDs separately from internal user IDs. - Store consent history immutably or append-only. - Hash public share passwords. - Encrypt partner subject mappings where feasible. - Support efficient album/item filtering. - Support revocation and cache invalidation. - Store webhook secrets securely. ### 24.4 Partner operations - Maintain partner registry. - Review redirect URIs. - Review privacy policy and terms links. - Issue sandbox credentials. - Certify against sandbox test cases. - Assign production quotas. - Monitor partner API usage. - Provide emergency suspension runbook. --- ## 25. Sandbox test cases Approved partners should pass these cases before production access: | Case | Expected result | |---|---| | OAuth with valid PKCE and approved scopes | Access token issued. | | OAuth requesting unapproved scope | Authorization fails with clear error. | | Read profile with consent | `200` with consented fields only. | | Read item-level data without consent | `403 consent_required` or `403 insufficient_scope`. | | Consent revoked then profile read | `403 consent_revoked` or `404` depending endpoint policy. | | Expired share link | `410 share_link_expired`. | | Password-protected share without password | `401 share_password_required`. | | Duplicate idempotency key same body | Same response as first request. | | Duplicate idempotency key different body | `409 idempotency_conflict`. | | Rate limit exceeded | `429` with `Retry-After`. | | Webhook signature verification | Partner correctly validates signature. | | Partner suspension | Tokens fail and reads are blocked. | | Minor-restricted account item-level share | `403 privacy_restricted`. | --- ## 26. Metrics Recommended operational and product metrics: | Metric | Purpose | |---|---| | Share links created per day | Sharing adoption. | | Share link views per day | Viral reach. | | Share conversion to signup/open | Growth loop performance. | | Partner consent grants | Partner integration adoption. | | Consent revocation rate | Trust/UX signal. | | Item-level sharing opt-in rate | Exchange readiness. | | Public profile reports | Safety signal. | | API 4xx by endpoint/code | Integration quality. | | API 5xx by endpoint | Reliability. | | Rate-limit events by actor | Abuse and quota tuning. | | Webhook delivery success rate | Partner sync health. | | Average consented response latency | Partner UX health. | --- ## 27. OpenAPI conversion notes This Markdown specification can be converted into an OpenAPI 3.1 contract. Recommended choices: 1. Use `securitySchemes` for OAuth2 Authorization Code + PKCE and Client Credentials. 2. Represent public share endpoints with optional security. 3. Use reusable schemas for: - `CollectorProfile` - `AlbumSummary` - `AlbumItem` - `AchievementSummary` - `ShareLink` - `ConsentRecord` - `PartnerAccountLink` - `WebhookSubscription` - `Problem` - `Pagination` 4. Use `oneOf` sparingly for public vs authenticated profile variants to keep partner SDK generation stable. 5. Mark sensitive management-only fields as absent from public schemas rather than nullable. --- ## 28. Production readiness gates The API should not be launched publicly until all of the following are complete: 1. Legal review of provider naming, album imagery, trademarks, and player likeness usage. 2. Privacy impact assessment for profile sharing and partner consent. 3. Minor/guardian policy implementation. 4. Partner onboarding workflow and registry. 5. Moderation workflow for user-generated profile fields. 6. Revocation propagation and cache invalidation tested. 7. Audit logging implemented. 8. Rate limiting and abuse detection configured. 9. Webhook signature verification documented and tested. 10. Public report/block UX wired to moderation. 11. Sandbox test suite available for partners. 12. Incident response runbook for partner abuse or data leakage. --- ## 29. Key design decisions 1. **Public profile reads use share tokens by default** to reduce scraping and accidental exposure. 2. **Partner access is consent-bound** rather than blanket partner access to all users. 3. **Item-level data requires explicit opt-in** because duplicates/wishlists can reveal value and invite abuse. 4. **Provider relationship is explicit** so proposed Panini/card-provider integrations are not misrepresented as active licensed partnerships. 5. **No direct contact details are exposed**; future exchange workflows must remain platform-mediated. 6. **Error responses are machine-readable** to support partner SDKs and automated retry/backoff behavior. 7. **Webhook payloads are minimal** so consented partners fetch fresh data rather than receiving broad data dumps.