openapi: 3.1.0
info:
  title: Zelcar Games Creators API
  version: "1.0.0"
  description: |
    Contract between the Creators portal (creators.zelcar.games, built by Centro) and the game backend (Kaizer).
    Two audiences share the same resources:
    1. **Portal calls** — authenticated with a Firebase ID token (`Authorization: Bearer <firebase-id-token>`); the backend verifies it with the Firebase Admin SDK and maps `uid` → creator.
    2. **Creator API calls** — authenticated with a personal API key (`Authorization: Bearer zc_live_…`) created in the portal; scoped and revocable. Same paths under `/v1`.
    Admin endpoints require the Centro service token.

    Business rules the backend enforces (see plan/programa-creadores.md): base = NET store revenue; Partner 30 % / Elite 40 % (≥ USD 3,000 net in last 6 months); 6-month window per player; launch bonus 50 % on first 30 days for players joining ≤ 2026-11-30 with a USD 20,000 budget; USD 100 cap per player; 60-day hold; one level only; codes invalid in BE/NL.
    Clicks are counted by the portal (Firestore), never by the backend.
servers:
  - url: https://sfb.zelcarserver.com/api/creators
    description: Production (Kaizer)
  - url: http://localhost:8787/api/creators
    description: Centro mock server (api-mock/server.js)
tags:
  - name: creator
  - name: codes
  - name: stats
  - name: money
  - name: public
  - name: keys
  - name: admin
components:
  securitySchemes:
    firebase: { type: http, scheme: bearer, bearerFormat: Firebase ID token }
    apiKey: { type: http, scheme: bearer, bearerFormat: zc_live_… }
    admin: { type: http, scheme: bearer, bearerFormat: Centro service token }
  schemas:
    Creator:
      type: object
      required: [id, display_name, master_code, tier, tier_rate, window_months, per_player_cap_usd]
      properties:
        id: { type: string, example: cr_01HZX8M2K9 }
        display_name: { type: string }
        handle: { type: string, example: "@kalen.mike" }
        master_code: { type: string, pattern: "^[A-Z0-9-]{4,20}$", example: KALEN-MIKE }
        game: { type: string, enum: [mfw], description: Program annex the creator is enrolled in. More games later. }
        tier: { type: string, enum: [partner, elite] }
        tier_rate: { type: number, example: 0.30 }
        elite_threshold_usd: { type: number, example: 3000 }
        net_last_6m_usd: { type: number, description: Net generated by referred players inside their windows in the last 6 calendar months (drives the tier). }
        country: { type: string, example: US }
        language: { type: string, example: en }
        verified: { type: boolean }
        verified_network: { type: string, enum: [youtube, tiktok, instagram, twitch] }
        followers: { type: integer }
        joined_at: { type: string, format: date }
        advance_usd: { type: number, description: Anchor advance still to recover (0 for most creators). }
        per_player_cap_usd: { type: number, example: 100 }
        window_months: { type: integer, example: 6 }
        launch_bonus:
          type: object
          properties: { rate: { type: number, example: 0.5 }, until: { type: string, format: date, example: 2026-11-30 }, first_days: { type: integer, example: 30 }, budget_usd: { type: number }, budget_used_usd: { type: number } }
        ranking_opt_in: { type: boolean }
        agreement:
          type: object
          description: Click-wrap evidence stored by the backend.
          properties: { version: { type: string }, accepted_at: { type: string, format: date-time }, lang: { type: string }, sha256: { type: string }, ip: { type: string }, ua: { type: string } }
    Code:
      type: object
      required: [code, label, active, created_at]
      properties:
        code: { type: string, example: KALEN-MIKE-3 }
        label: { type: string, example: "Stream Thursday" }
        platform: { type: string, enum: [tiktok, youtube, instagram, twitch, other] }
        active: { type: boolean }
        created_at: { type: string, format: date }
        link: { type: string, example: https://zelcar.games/c/KALEN-MIKE-3 }
        qr_png_url: { type: string }
    StatRow:
      type: object
      properties:
        date: { type: string, format: date, description: Present when group_by=day }
        code: { type: string, description: Present when group_by=code }
        label: { type: string }
        platform: { type: string }
        clicks: { type: integer, description: Filled by the portal from its own click log; backend may return 0. }
        installs: { type: integer, description: First logins attributed to the code (AppsFlyer + in-game redemption). }
        tutorials: { type: integer, description: af_tutorial_completion by attributed players. }
        payers: { type: integer, description: Attributed players with ≥ 1 validated purchase in the range. }
        net_usd: { type: number }
        commission_usd: { type: number }
    Transaction:
      type: object
      description: One validated store transaction of a referred player, exactly as the store's financial report states it.
      properties:
        id: { type: string }
        date: { type: string, format: date }
        player: { type: string, description: Anonymous, stable per player (never public_user_id). }
        code: { type: string }
        store: { type: string, enum: [apple, google, steam, epic] }
        country: { type: string }
        product_id: { type: string, example: zp_1050 }
        product: { type: string, example: "1,050 Zelcar Gems" }
        gross_usd: { type: number }
        store_fee_usd: { type: number }
        store_fee_pct: { type: number, example: 0.30 }
        tax_usd: { type: number }
        refund_usd: { type: number, description: Negative when refunded/charged back. }
        net_usd: { type: number }
        rate: { type: number, description: Rate applied (0.30 / 0.40 / 0.50 bonus). }
        launch_bonus: { type: boolean }
        capped: { type: boolean, description: True if the USD 100 per-player cap reduced this line. }
        commission_usd: { type: number }
        clears_at: { type: string, format: date, description: date + 60 days. }
    Statement:
      type: object
      properties:
        month: { type: string, example: 2026-10 }
        currency: { type: string, example: USD }
        commission_usd: { type: number }
        refunds_usd: { type: number }
        advance_recovered_usd: { type: number }
        holding_usd: { type: number }
        cleared_usd: { type: number }
        payable_usd: { type: number }
        min_payout_usd: { type: number, example: 50 }
        transactions: { type: array, items: { $ref: "#/components/schemas/Transaction" } }
    Payout:
      type: object
      properties:
        id: { type: string }
        cut_date: { type: string, format: date }
        amount_usd: { type: number }
        status: { type: string, enum: [scheduled, paid, skipped, failed] }
        reason: { type: string, enum: [below_minimum, holding_60_days, no_tax_form, not_verified] }
        reference: { type: string, nullable: true, description: PayPal batch/item id. }
    PayoutSettings:
      type: object
      properties:
        method: { type: string, enum: [paypal] }
        paypal_email: { type: string, format: email }
        tax_form: { type: string, enum: [W-9, W-8BEN], nullable: true }
        tax_status: { type: string, enum: [missing, pending, accepted, rejected] }
        min_usd: { type: number, example: 50 }
        hold_days: { type: integer, example: 60 }
    ApiKey:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        scopes: { type: array, items: { type: string, enum: [read:stats, read:codes, read:statements, write:codes] } }
        prefix: { type: string, example: "zc_live_ab12…" }
        key: { type: string, description: Only returned once, on creation. }
        created_at: { type: string, format: date-time }
        last_used_at: { type: string, format: date-time, nullable: true }
    Error:
      type: object
      properties: { error: { type: string }, code: { type: string } }
security: [ { firebase: [] }, { apiKey: [] } ]
paths:
  /creators:
    post:
      tags: [creator]
      summary: "1 · Create (or complete) the creator profile after sign-up"
      description: Idempotent per Firebase uid. Master code must be unique; reject with 409 if taken. Also used to update profile fields.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { display_name: {type: string}, handle: {type: string}, network: {type: string}, country: {type: string}, language: {type: string}, master_code: {type: string}, game: {type: string, default: mfw}, agreement: {type: object} } } } } }
      responses: { "200": { description: Creator, content: { application/json: { schema: { $ref: "#/components/schemas/Creator" } } } }, "409": { description: Code taken, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } } }
  /creators/me:
    get:
      tags: [creator]
      summary: "2 · My profile, tier and program parameters"
      responses: { "200": { description: Creator, content: { application/json: { schema: { $ref: "#/components/schemas/Creator" } } } } }
    patch:
      tags: [creator]
      summary: Update preferences (ranking_opt_in, language)
      requestBody: { content: { application/json: { schema: { type: object, properties: { ranking_opt_in: {type: boolean}, language: {type: string} } } } } }
      responses: { "200": { description: Creator } }
  /creators/me/verify:
    post:
      tags: [creator]
      summary: "3 · Record channel ownership verification"
      description: The portal performs the check (OAuth with the network or temporary code in bio) and posts the result with evidence; the backend stores it.
      requestBody: { content: { application/json: { schema: { type: object, properties: { network: {type: string}, handle: {type: string}, method: {type: string, enum: [oauth, bio_code]}, followers: {type: integer}, evidence_url: {type: string} } } } } }
      responses: { "200": { description: ok, content: { application/json: { schema: { type: object, properties: { verified: {type: boolean}, network: {type: string} } } } } } }
  /creators/me/codes:
    get:
      tags: [codes]
      summary: "4a · List my master code and sub-codes"
      responses: { "200": { description: Codes, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Code" } } } } } }
    post:
      tags: [codes]
      summary: "4b · Create a sub-code (all attribute to me)"
      description: If `code` is omitted the backend assigns `<MASTER>-<n>`. Sub-codes redeem in-game exactly like the master code.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [label], properties: { label: {type: string}, platform: {type: string}, code: {type: string, pattern: "^[A-Z0-9-]{4,32}$"} } } } } }
      responses: { "201": { description: Code, content: { application/json: { schema: { $ref: "#/components/schemas/Code" } } } }, "409": { description: exists } }
  /creators/me/codes/{code}:
    patch:
      tags: [codes]
      summary: Rename or pause/resume a sub-code
      parameters: [ { name: code, in: path, required: true, schema: { type: string } } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { label: {type: string}, active: {type: boolean} } } } } }
      responses: { "200": { description: Code } }
  /creators/me/stats:
    get:
      tags: [stats]
      summary: "5 · Installs, tutorials, payers, net and commission per code or per day"
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: group_by, in: query, schema: { type: string, enum: [code, day], default: code } }
      responses: { "200": { description: Rows, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/StatRow" } } } } } }
  /creators/me/statement:
    get:
      tags: [money]
      summary: "6 · Monthly statement with every transaction"
      parameters: [ { name: month, in: query, schema: { type: string, example: 2026-10 } } ]
      responses: { "200": { description: Statement, content: { application/json: { schema: { $ref: "#/components/schemas/Statement" } } } } }
  /creators/me/payout:
    get:
      tags: [money]
      summary: "7a · Payout method and tax status (portal only — never via API key)"
      security: [ { firebase: [] } ]
      responses: { "200": { description: Settings, content: { application/json: { schema: { $ref: "#/components/schemas/PayoutSettings" } } } } }
    put:
      tags: [money]
      summary: "7b · Update payout method (portal only)"
      security: [ { firebase: [] } ]
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/PayoutSettings" } } } }
      responses: { "200": { description: Settings } }
  /creators/me/payouts:
    get:
      tags: [money]
      summary: Payout history
      responses: { "200": { description: List, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Payout" } } } } } }
  /creators/me/api-keys:
    get:
      tags: [keys]
      summary: "11a · List my API keys (no secrets)"
      security: [ { firebase: [] } ]
      responses: { "200": { description: Keys, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/ApiKey" } } } } } }
    post:
      tags: [keys]
      summary: "11b · Create an API key (secret returned once)"
      security: [ { firebase: [] } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { label: {type: string}, scopes: { type: array, items: { type: string } } } } } } }
      responses: { "201": { description: Key, content: { application/json: { schema: { $ref: "#/components/schemas/ApiKey" } } } } }
  /creators/me/api-keys/{id}:
    delete:
      tags: [keys]
      summary: Revoke an API key
      security: [ { firebase: [] } ]
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { "204": { description: revoked } }
  /media:
    get:
      tags: [public]
      summary: Media kit index (served by the portal; listed here for API-key clients)
      responses: { "200": { description: Items } }
  /ranking:
    get:
      tags: [public]
      summary: Public ranking by players brought in (tutorial completed), opt-in only, no money. Worldwide top 100 + per-region boards (query scope=global|region&region=NA|LATAM|EU|MENA|APAC); a board is returned only when it has ≥ min_creators (default 50), otherwise {available:false, creators:n}.
      security: []
      responses: { "200": { description: Rows, content: { application/json: { schema: { type: array, items: { type: object, properties: { rank: {type: integer}, display_name: {type: string}, handle: {type: string}, players: {type: integer}, conversion: {type: number} } } } } } } }
  /news:
    get:
      tags: [public]
      summary: Program announcements
      security: []
      responses: { "200": { description: Items } }
  /admin/payouts:
    get:
      tags: [admin]
      summary: "8 · Payout batch for a cut: creators with cleared ≥ USD 50, verified, tax form on file"
      security: [ { admin: [] } ]
      parameters: [ { name: cut_date, in: query, required: true, schema: { type: string, format: date } } ]
      responses: { "200": { description: Batch, content: { application/json: { schema: { type: array, items: { type: object, properties: { creator_id: {type: string}, paypal_email: {type: string}, amount_usd: {type: number}, lines: {type: integer} } } } } } } }
  /admin/payouts/{id}/paid:
    post:
      tags: [admin]
      summary: "9 · Mark a payout as paid with the PayPal reference"
      security: [ { admin: [] } ]
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { reference: {type: string}, paid_at: {type: string, format: date-time} } } } } }
      responses: { "200": { description: ok } }
  /admin/creators:
    get:
      tags: [admin]
      summary: "10a · List creators (filters: status, tier, search)"
      security: [ { admin: [] } ]
      responses: { "200": { description: Creators } }
  /admin/creators/{id}:
    put:
      tags: [admin]
      summary: "10b · Block, set advance, force tier, flag duplicate"
      security: [ { admin: [] } ]
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { status: {type: string, enum: [active, blocked, review]}, advance_usd: {type: number}, tier_override: {type: string, nullable: true}, note: {type: string} } } } } }
      responses: { "200": { description: Creator } }
