{
  "openapi": "3.1.0",
  "info": {
    "title": "Pikoman HTTP API",
    "version": "2.0",
    "description": "Publish events to WebSocket channels, see who is in a channel, and disconnect clients. Every request carries Authorization: Bearer <token>, an EdDSA JWT signed with one of your app's API keys and valid for at most 10 minutes. The dashboard (https://pikoman.se/dashboard/) makes 10-minute tokens to paste here. Each operation says which right its key needs. Docs: https://pikoman.se/docs/http-api/ and https://pikoman.se/docs/keys/"
  },
  "servers": [{ "url": "https://next-api.pikoman.se" }],
  "security": [{ "apiKeyToken": [] }],
  "tags": [
    { "name": "Publish", "description": "Send events to the connections in a channel." },
    { "name": "Channels", "description": "Look inside a channel." },
    { "name": "Connections", "description": "Disconnect clients." }
  ],
  "paths": {
    "/{channel}/{event}": {
      "post": {
        "tags": ["Publish"],
        "summary": "Publish an event",
        "description": "Delivers the body to every connection in the channel as this event. A body that is valid JSON arrives as JSON; any other body arrives as a string. Events are not stored: a client that connects later does not get it. Right: publish.",
        "operationId": "publish",
        "parameters": [
          { "$ref": "#/components/parameters/channel" },
          {
            "name": "event",
            "in": "path",
            "required": true,
            "description": "The event name the client listens for with on(event, fn). At most 128 bytes.",
            "schema": { "type": "string", "maxLength": 128 },
            "example": "shipped"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {}, "example": { "id": 1042, "status": "shipped" } },
            "text/plain": { "schema": { "type": "string" } }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Delivered" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/mass_events": {
      "post": {
        "tags": ["Publish"],
        "summary": "Publish many events at once",
        "description": "Each entry sends one payload to one or more channels. At most 1,000 channel deliveries and 1 MiB of data, counted once per channel, per request. Right: publish, for every channel named.",
        "operationId": "publishMany",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["channels", "data"],
                  "properties": {
                    "channels": { "type": "array", "items": { "type": "string" } },
                    "event": { "type": "string", "maxLength": 128, "description": "Leave out to send the payload without an event name." },
                    "data": {}
                  }
                }
              },
              "example": [
                { "channels": ["private-user-7", "private-user-9"], "event": "invite", "data": { "team": "Design" } },
                { "channels": ["dashboard"], "event": "tick", "data": { "online": 31 } }
              ]
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queued. count is the number of channel deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "count": { "type": "integer" }, "message": { "type": "string" } }
                },
                "example": { "count": 3, "message": "success" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "413": { "description": "Over 1,000 channel deliveries or 1 MiB of data; nothing was sent." },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/{channel}/stats": {
      "get": {
        "tags": ["Channels"],
        "summary": "Count the connections in a channel",
        "description": "Right: stats.",
        "operationId": "channelStats",
        "parameters": [{ "$ref": "#/components/parameters/channel" }],
        "responses": {
          "200": {
            "description": "How many connections are in the channel.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "clients": { "type": "integer" } } },
                "example": { "clients": 12 }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/{channel}/clients": {
      "get": {
        "tags": ["Channels"],
        "summary": "List who is in a channel",
        "description": "The connections in the channel, in the order they joined, each with its member data. Outside presence channels the data is null. Member data is returned exactly as the join token had it and never says who is a bot; bots, the connections that joined with an API key, are listed in bots with the key id, which is left out when there are none. Right: clients.",
        "operationId": "channelClients",
        "parameters": [{ "$ref": "#/components/parameters/channel" }],
        "responses": {
          "200": {
            "description": "Connection id to member data, and connection id to API key id for the bots.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["clients"],
                  "properties": {
                    "clients": {
                      "type": "object",
                      "description": "Connection id to member data, as the join token had it.",
                      "additionalProperties": {}
                    },
                    "bots": {
                      "type": "object",
                      "description": "The connections that joined with an API key: connection id to key id (k_…). Left out when there are none.",
                      "additionalProperties": { "type": "string" }
                    }
                  }
                },
                "example": {
                  "clients": {
                    "c_8f3a1c9e2b7d4a6051e0c3b9": { "name": "Ada" },
                    "c_b07e5d2a9c4f1e8b3d6a0c57": { "name": "Order bot" }
                  },
                  "bots": { "c_b07e5d2a9c4f1e8b3d6a0c57": "k_3f9a1c0e5b7d2468" }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/connection/{connectionId}": {
      "delete": {
        "tags": ["Connections"],
        "summary": "Disconnect one client",
        "description": "Closes the connection with close code 1008. The client reconnects with a new id, so also make your token endpoint refuse that user. Right: disconnect.",
        "operationId": "disconnect",
        "parameters": [
          {
            "name": "connectionId",
            "in": "path",
            "required": true,
            "description": "The id from pikoman.getClientId(), the one your token endpoint receives.",
            "schema": { "type": "string" },
            "example": "c_8f3a1c9e2b7d4a6051e0c3b9"
          }
        ],
        "responses": {
          "200": {
            "description": "false means your app has no connection with that id.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "kicked": { "type": "boolean" } } },
                "example": { "kicked": true }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/disconnect-all": {
      "delete": {
        "tags": ["Connections"],
        "summary": "Disconnect every client of your app",
        "description": "Formerly /kick-all, which is gone. Right: disconnect_all.",
        "operationId": "disconnectAll",
        "responses": {
          "200": {
            "description": "How many connections were closed.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "kicked": { "type": "integer" } } },
                "example": { "kicked": 31 }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "An EdDSA (Ed25519) JWT signed with one of your app's API keys. Header {\"alg\": \"EdDSA\", \"typ\": \"JWT\", \"kid\": \"<key id>\"}, payload {\"iss\": \"<app id>\", \"iat\": <now>, \"exp\": <at most iat + 600>}. The dashboard makes 10-minute tokens to paste here; in code, sign one per request (https://pikoman.se/docs/http-api/#auth)."
      }
    },
    "parameters": {
      "channel": {
        "name": "channel",
        "in": "path",
        "required": true,
        "description": "The channel name, without ':'.",
        "schema": { "type": "string" },
        "example": "orders"
      }
    },
    "schemas": {
      "Error": { "type": "object", "properties": { "error": { "type": "string" } } }
    },
    "responses": {
      "Delivered": {
        "description": "How many connections the event was delivered to. Zero means nobody was listening.",
        "content": {
          "application/json": {
            "schema": { "type": "object", "properties": { "clients": { "type": "integer" } } },
            "example": { "clients": 12 }
          }
        }
      },
      "Unauthorized": {
        "description": "No Bearer token, or one that is not accepted: not a JWT with kid, iss, iat and exp; not EdDSA; a signature that does not match; expired, made in the future or valid for more than 10 minutes; or an unknown app or key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "The token has expired" }
          }
        }
      },
      "Forbidden": {
        "description": "The token is good, but the key may not do this: the key has expired, is used from an address it may not be used from, lacks the operation's right, or the channel is outside its channel kinds or prefixes.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "right": { "value": { "error": "This API key lacks the publish right" } },
              "channel": { "value": { "error": "This API key may not be used for this channel" } },
              "address": { "value": { "error": "This API key may not be used from this address" } },
              "expired": { "value": { "error": "This API key has expired" } }
            }
          }
        }
      },
      "TooManyRequests": { "description": "Over the app's request rate: 100 a second with bursts to 200 on the standard plan; higher plans allow more. The response says to slow down; retry after a moment." }
    }
  }
}
