{
  "openapi": "3.1.0",
  "info": {
    "title": "SMeet Mini Apps author API",
    "version": "1.0.0",
    "summary": "The API the server of a Mini App calls to turn a login code into a session and to check and revoke sessions.",
    "description": "The server of a Mini App (the author's server) signs a person in through SMeet: the page gets a\none-time login code from the SMeet window, hands it to its own server with the PKCE verifier, and\nthe server exchanges it here for a Mini App session. Before every protected operation the server\nchecks that session again.\n\n## Conventions\n\n* Every method is `POST https://messenger.scrile.com/mini-app-api/v1/<method>` with a JSON body\n  in UTF-8 and `Content-Type: application/json`. There is no CORS: only a server calls this API,\n  never a page.\n* Authentication: `Authorization: Bearer <key>` with the key of one environment of one Mini App\n  (`smac1_<key id>_<secret>`). The key decides the application and the environment; a code or a\n  session of another one is refused or reported as not found.\n* Every response is JSON. Success: `{\"ok\": true, \"result\": ...}`. Failure:\n  `{\"ok\": false, \"error\": {\"code\": \"...\", \"message\": \"...\", \"retry_after\": 1}, \"request_id\": \"...\"}`.\n  `retry_after` (seconds) is present only when retrying makes sense.\n* All identifiers are decimal strings; times are RFC 3339 in UTC.\n* Unknown JSON fields may appear in any object at any time, and new values of `reason` may appear.\n* `exchangeLaunchCode` needs an `Idempotency-Key`. A repeat with the same key, the same key of the\n  author and the same body within 60 seconds of the exchange returns the same session.\n* A request body is at most 64 KiB.\n",
    "contact": {
      "name": "SMeet",
      "url": "https://messenger.scrile.com/docs/mini-apps/en/"
    }
  },
  "servers": [
    {
      "url": "https://messenger.scrile.com/mini-app-api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "authorKey": []
    }
  ],
  "tags": [
    {
      "name": "Sessions",
      "description": "Signing a person in and checking that they may still use the Mini App."
    }
  ],
  "paths": {
    "/exchangeLaunchCode": {
      "post": {
        "operationId": "exchangeLaunchCode",
        "tags": [
          "Sessions"
        ],
        "summary": "Exchange a login code and its PKCE verifier for a Mini App session.",
        "description": "The code comes from `authorize()` in the page and lives 60 seconds; the verifier is the one the\nSDK made for it. Exchange it once: the same `Idempotency-Key` with the same body within 60 seconds\nof the exchange repeats the answer (a lost response), anything else with a used code fails with\n`CODE_ALREADY_USED`. A wrong verifier revokes the code. When the person may not use the Mini App\nany more, the answer is `ACCESS_DENIED` with the reason. The session lives 15 minutes at most;\nthe page renews it with a new `authorize()` and a new exchange.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExchangeLaunchCodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session and who signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExchangeLaunchCodeResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/checkSession": {
      "post": {
        "operationId": "checkSession",
        "tags": [
          "Sessions"
        ],
        "summary": "Check a Mini App session now, before a protected operation.",
        "description": "Call it without a cache before every operation that acts for the person: creating or changing\ntheir data, uploading or downloading a file, reading what belongs to them. Every state of the\nsession is an answer 200: `active: true` with the current details, or `active: false` with the\nreason. An unknown token, a wrong secret and a session of another application or environment\nall give `reason: not_found`. During a pause of the platform the answer is\n`active: false, reason: maintenance`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckSessionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The state of the session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckSessionResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/revokeSession": {
      "post": {
        "operationId": "revokeSession",
        "tags": [
          "Sessions"
        ],
        "summary": "End a Mini App session, for example when the person signs out of your app.",
        "description": "Revokes only a session of the key's application and environment. The answer is the same for an\nunknown token, a wrong secret, a session of another application or environment and a session that\nis already revoked, and nothing changes then. Works during a pause of the platform too.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RevokeSessionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always the same answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeSessionResponse"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "authorKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "smac1_<key id>_<secret>",
        "description": "The key of one environment of the Mini App, shown once in \"My bots\" when it is issued. Keep it on\nthe server only, never in the page, a link or a log. Each environment has up to two active keys,\nso a key can be replaced without a pause. A revoked or unknown key fails with\n`401 INVALID_CREDENTIALS`.\n"
      }
    },
    "parameters": {
      "IdempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "One key per sign-in, for example bound to your login attempt (`login:<attempt id>`). Send the same\nkey with the same body when you repeat the request after a timeout, 429 or 503.\n",
        "schema": {
          "$ref": "#/components/schemas/IdempotencyKey"
        }
      },
      "IdempotencyKeyOptional": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Accepted and not needed; the method changes nothing or is idempotent by itself.",
        "schema": {
          "$ref": "#/components/schemas/IdempotencyKey"
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Any failure. See `ErrorCode` for the meaning of each code and the HTTP status it comes with. The\n`Retry-After` header is set whenever the body carries `retry_after`. A 502, 503 or 504 without\nthis JSON body comes from a proxy in front of SMeet: treat it as `TEMPORARILY_UNAVAILABLE`.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          },
          "X-Request-Id": {
            "schema": {
              "type": "string"
            }
          },
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "Id": {
        "type": "string",
        "pattern": "^[0-9]{1,20}$",
        "description": "A decimal identifier passed as a string.",
        "example": "1000"
      },
      "LoginCode": {
        "type": "string",
        "pattern": "^smlc_[A-Za-z0-9_-]{43}$",
        "description": "A one-time login code from `authorize()`; it lives 60 seconds."
      },
      "CodeVerifier": {
        "type": "string",
        "pattern": "^[A-Za-z0-9._~-]{43,128}$",
        "description": "The PKCE verifier (RFC 7636) that the SDK made for the code; SMeet checks it with S256."
      },
      "SessionToken": {
        "type": "string",
        "pattern": "^smas1_([0-9a-f]{16})_([A-Za-z0-9_-]{43})$",
        "description": "A Mini App session. Your server keeps it; it never reaches the page."
      },
      "AuthorKey": {
        "type": "string",
        "pattern": "^smac1_([0-9a-f]{16})_([A-Za-z0-9_-]{43})$",
        "description": "The key of one environment of the Mini App, sent as `Authorization: Bearer`."
      },
      "IdempotencyKey": {
        "type": "string",
        "pattern": "^[A-Za-z0-9._:-]{1,128}$",
        "description": "1 to 128 characters."
      },
      "Timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "RFC 3339, UTC.",
        "example": "2026-10-10T12:15:00Z"
      },
      "Revision": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64,
        "description": "The revision of the person's conversation with the bot. Pass it to the Bot API as\n`expected_access_revision`; compare it only for equality.\n",
        "example": "1088"
      },
      "Environment": {
        "type": "string",
        "enum": [
          "production",
          "test"
        ]
      },
      "SpaceKind": {
        "type": "string",
        "enum": [
          "public",
          "organization"
        ],
        "description": "SMeet's public space or an organisation."
      },
      "LanguageCode": {
        "type": "string",
        "enum": [
          "en",
          "ru"
        ],
        "description": "The language of the person's SMeet interface."
      },
      "Scope": {
        "type": "string",
        "enum": [
          "profile"
        ],
        "description": "What the person agreed to share beyond the id and the language; `profile` is the display name."
      },
      "SessionReason": {
        "type": "string",
        "enum": [
          "not_found",
          "maintenance",
          "expired",
          "revoked",
          "launch_closed",
          "parent_session_revoked",
          "operator_session",
          "account_disabled",
          "chat_stopped",
          "membership_revoked",
          "app_unavailable",
          "space_disabled",
          "bot_unavailable",
          "test_access_removed",
          "user_revoked",
          "config_changed"
        ],
        "description": "Why a session is not active. Consumers must accept unknown values."
      },
      "AccessDeniedReason": {
        "type": "string",
        "enum": [
          "launch_closed",
          "parent_session_revoked",
          "operator_session",
          "account_disabled",
          "chat_stopped",
          "membership_revoked",
          "app_unavailable",
          "space_disabled",
          "bot_unavailable",
          "test_access_removed",
          "user_revoked",
          "config_changed"
        ],
        "description": "Why an exchange is refused with `ACCESS_DENIED`. Consumers must accept unknown values."
      },
      "ErrorCode": {
        "type": "string",
        "description": "| Code | HTTP | Meaning |\n|---|---|---|\n| INVALID_CREDENTIALS | 401 | The key is missing, unknown or revoked. Do not retry |\n| INVALID_REQUEST | 400 | A field is missing or malformed; `field` names it |\n| PAYLOAD_TOO_LARGE | 413 | The body is larger than 64 KiB; `limit` gives the size in bytes |\n| RATE_LIMITED | 429 | Too many requests; wait `retry_after` and repeat with the same key |\n| MINI_APPS_MAINTENANCE | 503 | Mini Apps are paused; exchangeLaunchCode only, `retry_after` 30 |\n| TEMPORARILY_UNAVAILABLE | 503 | Nothing was changed; repeat the same request with the same key after `retry_after` |\n| IDEMPOTENCY_KEY_REQUIRED | 400 | exchangeLaunchCode needs an Idempotency-Key header |\n| IDEMPOTENCY_CONFLICT | 409 | The key was used for this code with another `code_verifier` |\n| APP_MISMATCH | 403 | The code belongs to another Mini App than the key |\n| ENVIRONMENT_MISMATCH | 403 | The code belongs to the other environment of the key's Mini App |\n| CODE_NOT_FOUND | 400 | No such code. The page signs in again |\n| CODE_EXPIRED | 400 | The code lived its 60 seconds. The page signs in again |\n| CODE_REVOKED | 400 | The code was revoked: the window closed, access ended, or a wrong verifier was sent. The page signs in again |\n| CODE_ALREADY_USED | 400 | The code was exchanged already; with `reason` `replay_unavailable` a repeat cannot be answered any more. The page signs in again |\n| CODE_VERIFIER_MISMATCH | 400 | The verifier does not match the code, and the code is revoked now. The page signs in again |\n| ACCESS_DENIED | 403 | The person may not use the Mini App now; `reason` says why |\n",
        "enum": [
          "INVALID_CREDENTIALS",
          "INVALID_REQUEST",
          "PAYLOAD_TOO_LARGE",
          "RATE_LIMITED",
          "MINI_APPS_MAINTENANCE",
          "TEMPORARILY_UNAVAILABLE",
          "IDEMPOTENCY_KEY_REQUIRED",
          "IDEMPOTENCY_CONFLICT",
          "APP_MISMATCH",
          "ENVIRONMENT_MISMATCH",
          "CODE_NOT_FOUND",
          "CODE_EXPIRED",
          "CODE_REVOKED",
          "CODE_ALREADY_USED",
          "CODE_VERIFIER_MISMATCH",
          "ACCESS_DENIED"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string",
            "description": "Human-readable, English, free of secrets. Do not parse it."
          },
          "retry_after": {
            "type": "integer",
            "minimum": 1,
            "description": "Seconds to wait before retrying. Present only when a retry makes sense."
          },
          "reason": {
            "type": "string",
            "description": "With ACCESS_DENIED, a value of `AccessDeniedReason`; with CODE_ALREADY_USED,\n`replay_unavailable` when a repeat of the exchange cannot be answered any more.\n"
          },
          "field": {
            "type": "string",
            "description": "Present with INVALID_REQUEST, the field at fault (`code`, `code_verifier`, `session_token`, `idempotency_key`, `body`, or `method` for a path that is no method)."
          },
          "limit": {
            "type": "integer",
            "description": "Present with PAYLOAD_TOO_LARGE, the largest accepted body in bytes."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "ok",
          "error",
          "request_id"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "$ref": "#/components/schemas/Error"
          },
          "request_id": {
            "type": "string",
            "description": "Quote it when you write to support. Empty when the proxy in front of SMeet answered."
          }
        }
      },
      "App": {
        "type": "object",
        "required": [
          "id",
          "bot_id",
          "title"
        ],
        "description": "The Mini App the key belongs to.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "bot_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The bot's id, the same as `id` of getMe in the Bot API."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The title of the configuration the person opened."
          }
        }
      },
      "Space": {
        "type": "object",
        "required": [
          "id",
          "kind"
        ],
        "description": "Where the person opened the Mini App.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "kind": {
            "$ref": "#/components/schemas/SpaceKind"
          }
        }
      },
      "User": {
        "type": "object",
        "required": [
          "id",
          "language_code"
        ],
        "description": "The person who signed in.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id",
            "description": "The same id the bot sees as `from.id` in the Bot API."
          },
          "display_name": {
            "type": "string",
            "description": "Present only when the Mini App asks for `profile` and the person agreed."
          },
          "language_code": {
            "$ref": "#/components/schemas/LanguageCode"
          }
        }
      },
      "Chat": {
        "type": "object",
        "required": [
          "id",
          "type",
          "access_revision"
        ],
        "description": "The person's private chat with the bot.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id",
            "description": "Use it as `chat_id` in the Bot API."
          },
          "type": {
            "const": "private"
          },
          "access_revision": {
            "$ref": "#/components/schemas/Revision"
          }
        }
      },
      "Session": {
        "type": "object",
        "required": [
          "session_token",
          "expires_at",
          "environment",
          "app",
          "space",
          "user",
          "chat",
          "scopes",
          "start_param"
        ],
        "description": "A new Mini App session and who it is for.",
        "properties": {
          "session_token": {
            "$ref": "#/components/schemas/SessionToken"
          },
          "expires_at": {
            "$ref": "#/components/schemas/Timestamp",
            "description": "15 minutes after the exchange at most, and never after the person's SMeet sign-in ends."
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "app": {
            "$ref": "#/components/schemas/App"
          },
          "space": {
            "$ref": "#/components/schemas/Space"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "chat": {
            "$ref": "#/components/schemas/Chat"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            },
            "description": "What the person agreed to share; empty when the Mini App asks only for the id and the language."
          },
          "start_param": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9_-]{0,64}$",
            "description": "The parameter of the button or the link that opened the Mini App; null when there was none."
          }
        }
      },
      "SessionCheck": {
        "type": "object",
        "required": [
          "active"
        ],
        "description": "The state of a session. `active: false` comes with `reason` only; `active: true` with the session's\ndetails, read now: `chat.access_revision` is the revision to put into what you send the person after\nthis check.\n",
        "properties": {
          "active": {
            "type": "boolean"
          },
          "reason": {
            "$ref": "#/components/schemas/SessionReason",
            "description": "Present when `active` is false."
          },
          "expires_at": {
            "$ref": "#/components/schemas/Timestamp",
            "description": "Present when `active` is true."
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "app": {
            "$ref": "#/components/schemas/App"
          },
          "space": {
            "$ref": "#/components/schemas/Space"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "chat": {
            "$ref": "#/components/schemas/Chat"
          }
        }
      },
      "ExchangeLaunchCodeRequest": {
        "type": "object",
        "required": [
          "code",
          "code_verifier"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/LoginCode"
          },
          "code_verifier": {
            "$ref": "#/components/schemas/CodeVerifier"
          }
        }
      },
      "ExchangeLaunchCodeResponse": {
        "type": "object",
        "required": [
          "ok",
          "result"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "result": {
            "$ref": "#/components/schemas/Session"
          }
        }
      },
      "CheckSessionRequest": {
        "type": "object",
        "required": [
          "session_token"
        ],
        "properties": {
          "session_token": {
            "$ref": "#/components/schemas/SessionToken"
          }
        }
      },
      "CheckSessionResponse": {
        "type": "object",
        "required": [
          "ok",
          "result"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "result": {
            "$ref": "#/components/schemas/SessionCheck"
          }
        }
      },
      "RevokeSessionRequest": {
        "type": "object",
        "required": [
          "session_token"
        ],
        "properties": {
          "session_token": {
            "$ref": "#/components/schemas/SessionToken"
          }
        }
      },
      "RevokeSessionResponse": {
        "type": "object",
        "required": [
          "ok",
          "result"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "result": {
            "type": "object",
            "required": [
              "revoked"
            ],
            "properties": {
              "revoked": {
                "const": true
              }
            }
          }
        }
      }
    }
  }
}
