{
  "openapi": "3.1.0",
  "info": {
    "title": "Lia server API",
    "version": "1",
    "summary": "The API a Lia app's own server calls with its secret key.",
    "description": "Verify sessions, check and deliver charges paid on Lia's pay sheet, pay rewards from the app's pool, send notifications, check watched ads and list public tables. A key only sees its own app and its own mode: the test key (lia_test_...) test data, the live key (lia_live_...) live data. Amounts are in $LIA to 6 decimal places; times are milliseconds since 1970. Every POST takes an Idempotency-Key header: a retry with the same key and body answers as the first request did. Docs: https://docs.lialabs.org/reference/server-api"
  },
  "externalDocs": {
    "description": "Lia developer docs",
    "url": "https://docs.lialabs.org/reference/server-api"
  },
  "servers": [
    {
      "url": "https://api.lialabs.org/api/apps/v1"
    }
  ],
  "security": [
    {
      "key": []
    }
  ],
  "tags": [
    {
      "name": "Sessions",
      "description": "Who is asking: check a session token from the page, and take the player id from Lia's answer."
    },
    {
      "name": "Products and charges",
      "description": "What the app sells, and the charges people paid on Lia's pay sheet: check, deliver once, or refund."
    },
    {
      "name": "Rewards",
      "description": "Pay players in $LIA from the app's reward pool, once per key, within the pool and the daily limit per player."
    },
    {
      "name": "Notifications",
      "description": "Opt-in notifications to players, within the limits Lia sets."
    },
    {
      "name": "Ads",
      "description": "Check that a rewarded video was really watched, and by whom, before giving anything."
    },
    {
      "name": "Lobby",
      "description": "Public tables anyone on Lia can find and sit at. Lia keeps the listing; your server runs the game."
    }
  ],
  "paths": {
    "/session/verify": {
      "post": {
        "tags": [
          "Sessions"
        ],
        "operationId": "verifySession",
        "summary": "Check a session token from the page, and whose it is",
        "description": "Always answers 200: { valid: false } for a token that is not Lia's, is for another app or mode, or has ended (15 minutes).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session": {
                    "type": "string",
                    "description": "From Lia.init() or Lia.session(), lias_..."
                  }
                },
                "required": [
                  "session"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "valid": {
                          "const": true
                        },
                        "playerId": {
                          "type": "string"
                        },
                        "mode": {
                          "$ref": "#/components/schemas/Mode"
                        },
                        "exp": {
                          "type": "integer",
                          "description": "When the session ends (milliseconds since 1970)"
                        }
                      },
                      "required": [
                        "valid",
                        "playerId",
                        "mode",
                        "exp"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "valid": {
                          "const": false
                        }
                      },
                      "required": [
                        "valid"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          }
        }
      }
    },
    "/products": {
      "get": {
        "tags": [
          "Products and charges"
        ],
        "operationId": "listProducts",
        "summary": "What the app sells, at the prices in Lia's records (active only, cheapest first)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "products": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    }
                  },
                  "required": [
                    "products"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          }
        }
      }
    },
    "/charges/{id}": {
      "get": {
        "tags": [
          "Products and charges"
        ],
        "operationId": "getCharge",
        "summary": "Is this charge real, paid, and whose?",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The chargeId from Lia.pay()",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  },
                  "required": [
                    "charge"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "No such charge in this key's mode",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/charges/{id}/deliver": {
      "post": {
        "tags": [
          "Products and charges"
        ],
        "operationId": "deliverCharge",
        "summary": "Mark a charge delivered (safe to repeat)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The charge",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  },
                  "required": [
                    "charge"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "Refunded: do not deliver it",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "const": "refunded"
                        },
                        "charge": {
                          "$ref": "#/components/schemas/Charge"
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/charges/{id}/refund": {
      "post": {
        "tags": [
          "Products and charges"
        ],
        "operationId": "refundCharge",
        "summary": "Give the $LIA back to the person (paid or delivered charges; safe to repeat)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The charge",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  },
                  "required": [
                    "charge"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/rewards": {
      "post": {
        "tags": [
          "Rewards"
        ],
        "operationId": "payReward",
        "summary": "Pay a player from the app's reward pool, once per key",
        "description": "Test keys pay pretend $LIA: the pool and the daily limit are not used. The same key again answers status \"duplicate\" with what the first paid.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "playerId": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 10000,
                    "description": "In $LIA, to 6 decimal places"
                  },
                  "key": {
                    "type": "string",
                    "pattern": "^[\\w.:-]{1,128}$",
                    "description": "Pays once, ever. Without it, the Idempotency-Key header is used."
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Shown in the player's activity"
                  }
                },
                "required": [
                  "playerId",
                  "amount"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "enum": [
                        "paid",
                        "duplicate"
                      ]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "pool": {
                      "type": "number",
                      "description": "What's left in the pool",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "status",
                    "amount",
                    "pool"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "bad_amount",
                            "bad_key_field",
                            "bad_idempotency_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unknown_player"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "The pool does not hold the amount",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "const": "pool_empty"
                        },
                        "pool": {
                          "type": "number"
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "The player's daily limit from the pool (UTC day)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "const": "daily_limit"
                        },
                        "dailyCapPerPlayer": {
                          "type": "number"
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pool": {
      "get": {
        "tags": [
          "Rewards"
        ],
        "operationId": "getPool",
        "summary": "The reward pool's balance and the daily limit per player (0: none)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance": {
                      "type": "number"
                    },
                    "dailyCapPerPlayer": {
                      "type": "number"
                    },
                    "mode": {
                      "$ref": "#/components/schemas/Mode"
                    }
                  },
                  "required": [
                    "balance",
                    "dailyCapPerPlayer",
                    "mode"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          }
        }
      }
    },
    "/notifications": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "operationId": "sendNotification",
        "summary": "Notify a player who allowed it; it opens the app with startParam",
        "description": "At most 3 a day per person from one app (unless changed), an hour apart, and 6 a day from all apps together. Test keys reach testers only, every 30 seconds.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "playerId": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 180,
                    "description": "No links, phone numbers or scam words"
                  },
                  "startParam": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,64}$"
                  }
                },
                "required": [
                  "playerId",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "const": true
                    },
                    "pushed": {
                      "type": "boolean",
                      "description": "false: it reached Lia's bell but not the phone"
                    }
                  },
                  "required": [
                    "sent",
                    "pushed"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "enum": [
                            "bad_text",
                            "bad_start_param"
                          ]
                        },
                        "problem": {
                          "enum": [
                            "link",
                            "phone",
                            "scam"
                          ]
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "403": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "notifications_off",
                            "not_allowed"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unknown_player"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "const": "rate_limited"
                        },
                        "retryAt": {
                          "type": "integer",
                          "description": "When to try again, for the app's own limits (milliseconds since 1970)"
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ads/verify": {
      "post": {
        "tags": [
          "Ads"
        ],
        "operationId": "verifyAd",
        "summary": "Was this ad watched, and by whom? (Lia does not mark it used: pay each adId once)",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "adId": {
                    "type": "string"
                  }
                },
                "required": [
                  "adId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "adId": {
                      "type": "string"
                    },
                    "playerId": {
                      "type": "string"
                    },
                    "watched": {
                      "type": "boolean"
                    },
                    "watchedAt": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "adId",
                    "playerId",
                    "watched",
                    "watchedAt"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/lobby/tables": {
      "get": {
        "tags": [
          "Lobby"
        ],
        "operationId": "listTables",
        "summary": "The app's open public tables in this key's mode, newest first (up to 200)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tables": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Table"
                      }
                    }
                  },
                  "required": [
                    "tables"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          }
        }
      },
      "post": {
        "tags": [
          "Lobby"
        ],
        "operationId": "listTable",
        "summary": "List a table waiting for players, or update its listing",
        "description": "A host has at most 3 open tables in an app. When taken reaches seats, the table is taken out instead ({ listed: false }).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tableId": {
                    "type": "string",
                    "pattern": "^[\\w-]{1,57}$"
                  },
                  "hostPlayerId": {
                    "type": "string"
                  },
                  "seats": {
                    "type": "integer",
                    "minimum": 2,
                    "maximum": 10,
                    "default": 2
                  },
                  "taken": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                  },
                  "title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 40
                  },
                  "stake": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Shown only: charge stakes with your own product"
                  },
                  "pot": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Shown only"
                  },
                  "expiresIn": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600,
                    "default": 1800,
                    "description": "Seconds"
                  }
                },
                "required": [
                  "tableId",
                  "hostPlayerId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "listed": {
                          "const": true
                        },
                        "tableId": {
                          "type": "string"
                        },
                        "seats": {
                          "type": "integer"
                        },
                        "taken": {
                          "type": "integer"
                        },
                        "expiresAt": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "listed",
                        "tableId",
                        "seats",
                        "taken",
                        "expiresAt"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "listed": {
                          "const": false
                        }
                      },
                      "required": [
                        "listed"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "bad_table_id",
                            "bad_seats",
                            "bad_taken",
                            "bad_title",
                            "bad_amount",
                            "bad_expires_in"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unknown_player"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "const": "too_many_tables"
                        },
                        "max": {
                          "type": "integer"
                        }
                      },
                      "required": []
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/lobby/tables/{id}/sit": {
      "post": {
        "tags": [
          "Lobby"
        ],
        "operationId": "sitAtTable",
        "summary": "Seat a player at a listed table (refuses blocked players and full tables)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The table",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "playerId": {
                    "type": "string"
                  }
                },
                "required": [
                  "playerId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "seated": {
                      "const": true
                    },
                    "taken": {
                      "type": "integer"
                    },
                    "seats": {
                      "type": "integer"
                    },
                    "full": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "seated",
                    "taken",
                    "seats",
                    "full"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          },
          "404": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unknown_player",
                            "not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "full",
                            "own_table"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/lobby/tables/{id}": {
      "delete": {
        "tags": [
          "Lobby"
        ],
        "operationId": "unlistTable",
        "summary": "Take a table out of the list (safe to repeat)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The table",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "removed"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BadKey"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "key": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your secret key: lia_test_... or lia_live_..."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional, 1 to 128 of A-Z a-z 0-9 _ - . : . A retry with the same key and body answers as the first (kept 24 hours, Idempotent-Replayed: true).",
        "schema": {
          "type": "string",
          "pattern": "^[\\w.:-]{1,128}$"
        }
      }
    },
    "responses": {
      "BadKey": {
        "description": "No key, an unknown key, a live key while the app is not live, or the app is paused",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "properties": {
                    "error": {
                      "enum": [
                        "bad_key"
                      ]
                    }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "schemas": {
      "Mode": {
        "enum": [
          "test",
          "live"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A code from the docs' error codes"
          }
        },
        "required": [
          "error"
        ]
      },
      "Product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number"
          },
          "active": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "price",
          "active"
        ]
      },
      "Charge": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "playerId": {
            "type": "string"
          },
          "productId": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "status": {
            "enum": [
              "paid",
              "delivered",
              "refunded"
            ]
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "createdAt": {
            "type": "integer"
          },
          "deliveredAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "refundedAt": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "playerId",
          "productId",
          "amount",
          "status",
          "mode",
          "createdAt",
          "deliveredAt",
          "refundedAt"
        ]
      },
      "Table": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "game": {
            "type": "string"
          },
          "host": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              }
            },
            "required": [
              "name"
            ]
          },
          "seats": {
            "type": "integer"
          },
          "taken": {
            "type": "integer"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "stake": {
            "type": "number"
          },
          "pot": {
            "type": "number"
          },
          "createdAt": {
            "type": "integer"
          },
          "expiresAt": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "game",
          "host",
          "seats",
          "taken",
          "title",
          "stake",
          "pot",
          "createdAt",
          "expiresAt"
        ]
      }
    }
  }
}