openapi: 3.0.3
info:
  title: Playmez partner API
  version: "1.0"
  description: |
    Server-to-server calls a partner makes to open Playmez games for its own users.
    Authenticate with the `X-Api-Key` header. Keep the key on your server.
servers:
  - url: https://api.playmez.com
security:
  - apiKey: []
paths:
  /partner/tables:
    post:
      summary: Create a shared table your players join by code
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [game, hostExternalUserId]
              properties:
                game: { type: string, example: callbreak }
                hostExternalUserId: { type: string, example: user-123 }
                hostDisplayName: { type: string }
                name: { type: string, maxLength: 40 }
                maxPlayers: { type: integer, minimum: 1, maximum: 100 }
                computerFill: { type: boolean, description: Computer players fill empty seats when the first human sits (practice, XP only). }
      responses:
        "201":
          description: Table created
          content:
            application/json:
              schema:
                type: object
                properties:
                  tableCode: { type: string, example: K7Q2MZ }
                  tableId: { type: string }
                  game: { type: string }
                  name: { type: string }
                  maxPlayers: { type: integer }
  /partner/tables/{code}:
    get:
      summary: Seats and status of a table you created
      security:
        - ApiKey: []
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Table status
          content:
            application/json:
              schema:
                type: object
                properties:
                  tableCode: { type: string }
                  status: { type: string, enum: [OPEN, CLOSED] }
                  phase: { type: string }
                  seated: { type: integer }
                  connected: { type: integer }
                  maxPlayers: { type: integer }
  /partner/launch:
    post:
      summary: Open a game for one of your users
      description: Returns a one-time launch URL, valid for five minutes, that a page or app frames.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LaunchRequest"
      responses:
        "200":
          description: Launch created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LaunchResponse"
        "400":
          description: "`INVALID_GAME` or a validation error"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: "`INVALID_API_KEY`: unknown, rotated or switched-off key"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`ACCOUNT_BANNED`: this player is banned on Playmez"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit, 600 launches per minute per key
  /partner/me:
    get:
      summary: Your partner record (health check)
      responses:
        "200":
          description: The partner behind the key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Partner"
        "401":
          description: "`INVALID_API_KEY`"
  /partner/tournaments:
    get:
      summary: Your tournaments, newest first
      responses:
        "200":
          description: Tournaments
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Tournament"
    post:
      summary: Create a tournament for your players
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, startsAt, endsAt]
              properties:
                name: { type: string, minLength: 3, maxLength: 60 }
                description: { type: string, maxLength: 300 }
                game:
                  $ref: "#/components/schemas/GameId"
                startsAt: { type: string, format: date-time }
                endsAt: { type: string, format: date-time, description: At most 31 days after startsAt. }
                autoEnrol: { type: boolean, default: true, description: Every player who plays is in; false means only enrolled players count. }
                prizeFirst: { type: integer, minimum: 0, maximum: 100000, default: 0 }
                prizeSecond: { type: integer, minimum: 0, maximum: 100000, default: 0 }
                prizeThird: { type: integer, minimum: 0, maximum: 100000, default: 0 }
      responses:
        "201":
          description: Created (RUNNING when the window already started)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tournament"
        "400":
          description: "`INVALID_WINDOW`, `TOO_LONG`, `INVALID_GAME`"
  /partner/tournaments/{id}:
    get:
      summary: One tournament with standings
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Tournament and standings (live while running, final afterwards)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Tournament"
                  - type: object
                    properties:
                      standings:
                        type: array
                        items:
                          $ref: "#/components/schemas/Standing"
        "404":
          description: "`TOURNAMENT_NOT_FOUND`"
  /partner/tournaments/{id}/entries:
    post:
      summary: Enrol your players by their external ids
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [externalUserIds]
              properties:
                externalUserIds:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: How many were enrolled and which ids are unknown
          content:
            application/json:
              schema:
                type: object
                properties:
                  enrolled: { type: integer }
                  unknown:
                    type: array
                    items: { type: string }
  /partner/tournaments/{id}/cancel:
    post:
      summary: Cancel a scheduled or running tournament
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Cancelled
  /partner/webhook:
    get:
      summary: Your webhook settings
      responses:
        "200":
          description: The URL and whether a signing secret exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, nullable: true }
                  hasSecret: { type: boolean }
    put:
      summary: Set the webhook URL
      description: |
        Playmez posts signed events to this URL. The signing secret is returned only the first time
        it is created; later calls return `secret: null`. Rotate it with `/partner/webhook/rotate-secret`.
        URLs must be https and reachable from the internet.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, example: "https://example.com/playmez/webhook" }
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookConfig"
        "400":
          description: "`INVALID_WEBHOOK_URL`"
    delete:
      summary: Switch webhooks off
      responses:
        "200":
          description: Cleared
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookConfig"
  /partner/webhook/rotate-secret:
    post:
      summary: Mint a new signing secret
      description: Deliveries signed with the old secret stop verifying at once.
      responses:
        "200":
          description: The new secret, shown once
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret: { type: string, example: "whsec_…" }
  /partner/webhook/test:
    post:
      summary: Queue a `webhook.test` delivery
      responses:
        "200":
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
        "400":
          description: "`WEBHOOK_NOT_CONFIGURED`"
  /partner/webhook/deliveries:
    get:
      summary: Recent deliveries, newest first
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Deliveries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WebhookDelivery"
  /partner/webhook/deliveries/{id}/redeliver:
    post:
      summary: Queue a delivery again at once
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The delivery, now pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDelivery"
        "404":
          description: "`DELIVERY_NOT_FOUND`"
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
  schemas:
    GameId:
      type: string
      description: Case-insensitive.
      enum: [ludo, callbreak, rummy, tambola, chess, teen_patti, carrom, roulette, crash, trivia, wheel, scratch]
    LaunchRequest:
      type: object
      required: [game, externalUserId]
      properties:
        game:
          $ref: "#/components/schemas/GameId"
        externalUserId:
          type: string
          minLength: 1
          maxLength: 128
          description: Your id for the player. The same id always opens the same Playmez account.
        displayName:
          type: string
          minLength: 1
          maxLength: 32
          description: Shown at the table. Updates the account when it changes.
        avatar:
          type: string
          pattern: "^[0-9]{1,2}$"
          description: One of Playmez's avatars, "1" to "12".
        tableCode:
          type: string
          description: Seat the player at a table created with POST /partner/tables.
          example: K7Q2MZ
        theme:
          $ref: "#/components/schemas/Theme"
        lang:
          type: string
          enum: [en, hi]
          description: Interface language for this launch; a BCP 47 tag such as hi-IN is accepted. Overrides the partner default.
    LaunchResponse:
      type: object
      properties:
        launchUrl:
          type: string
          format: uri
          description: Frame this in an iframe or WebView. One use, five minutes.
        launchToken:
          type: string
        expiresAt:
          type: string
          format: date-time
        game:
          type: string
          example: LUDO
        lang:
          type: string
          enum: [en, hi]
          description: The language the embed will render in.
        player:
          type: object
          properties:
            id: { type: string }
            externalUserId: { type: string }
            displayName: { type: string }
            balance: { type: integer, description: Virtual points, no cash value. }
    Partner:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        slug: { type: string }
        allowedOrigins:
          type: array
          items: { type: string }
        features:
          type: object
          description: Switches for your embeds, set by Playmez on request.
          properties:
            chat: { type: boolean }
            voice: { type: boolean }
        webhook:
          type: object
          properties:
            url: { type: string, nullable: true }
        theme:
          $ref: "#/components/schemas/Theme"
        lang:
          type: string
          enum: [en, hi]
          description: Default interface language for your embeds.
    Theme:
      type: object
      nullable: true
      description: |
        Embed appearance. Every field is optional; unknown fields are dropped; a bad value is refused
        with 400 INVALID_THEME and a `problems` list. On a launch it is merged over the partner theme.
      properties:
        accent: { type: string, pattern: "^#?[0-9a-fA-F]{6}$", example: "#1e90ff" }
        background: { type: string, pattern: "^#?[0-9a-fA-F]{6}$", description: Must be dark (relative luminance ≤ 0.25). }
        text: { type: string, pattern: "^#?[0-9a-fA-F]{6}$", description: Must be light. }
        radius: { type: string, enum: [none, small, medium, large] }
        font: { type: string, enum: [spinline, system, rounded, serif, mono] }
        googleFont: { type: string, maxLength: 40, description: A fonts.google.com family name, loaded by the embed. }
    Tournament:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        status: { type: string, enum: [SCHEDULED, RUNNING, FINISHED, CANCELLED] }
        game: { type: string, nullable: true, example: CARROM }
        rulesetId: { type: string, nullable: true }
        startsAt: { type: string, format: date-time }
        endsAt: { type: string, format: date-time }
        prizes:
          type: array
          items: { type: integer }
          description: Virtual points for 1st, 2nd, 3rd.
        autoEnrol: { type: boolean }
        entrants: { type: integer }
    Standing:
      type: object
      properties:
        rank: { type: integer }
        player:
          type: object
          properties:
            id: { type: string }
            externalUserId: { type: string, nullable: true }
            displayName: { type: string }
        netPoints: { type: integer }
        wins: { type: integer }
        roundsPlayed: { type: integer }
        prize: { type: integer }
    WebhookConfig:
      type: object
      properties:
        url: { type: string, nullable: true }
        secret:
          type: string
          nullable: true
          description: Only when a secret was just created. Store it; it is not shown again.
    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        event: { type: string, example: player.result }
        status: { type: string, enum: [PENDING, DELIVERED, FAILED] }
        attempts: { type: integer }
        nextAttemptAt: { type: string, format: date-time, nullable: true }
        lastAttemptAt: { type: string, format: date-time, nullable: true }
        lastStatusCode: { type: integer, nullable: true }
        lastError: { type: string, nullable: true }
        deliveredAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
    WebhookEnvelope:
      type: object
      description: |
        The body of every delivery. Headers: Playmez-Event, Playmez-Delivery (same on retries),
        Playmez-Timestamp (unix seconds), Playmez-Signature (v1=hex HMAC-SHA256 of "timestamp.body"), Playmez-Attempt.
      properties:
        id: { type: string }
        type: { type: string, enum: [player.created, session.opened, player.result, table.closed, tournament.started, tournament.finished, webhook.test] }
        createdAt: { type: string, format: date-time }
        partner:
          type: object
          properties:
            id: { type: string }
            slug: { type: string }
        data:
          type: object
          additionalProperties: true
    Error:
      type: object
      properties:
        statusCode: { type: integer }
        code: { type: string }
        message: { type: string }
