Repository navigation
The current rules of Max, mini app backends, tools for writing bots, and production operations:
health, tracing, a WebFlux webhook, native image and non-blocking deduplication.
Added
-
Spring Boot starter: the health contributor
maxwhen Spring Boot Actuator is present
(MaxHealthAutoConfiguration). Its partbot(MaxBotHealthIndicator) isDOWNwhen long polling
ended (with the error inpollingError), when webhook mode has no endpoint or the subscription to
max.bot.webhook.urlis missing, and when the API does not answer or rejects the token; the request
to the API (getSubscriptionsorgetMyInfo) is made at most once permax.bot.health.cache-ttl
(30 seconds) and waits up tomax.bot.health.timeout(5 seconds). Its partstoragechecks the
database or Redis ofmax.bot.storage=jdbc|redis.management.health.max.enabled=falseturns it off. -
New module
maxapi-opentelemetry:MaxTracingrecords API requests as client spans
(GET /chats/{id}withhttp.request.method,url.template,http.response.status_code,
server.address,server.port,http.request.resend_count,error.type), dispatched updates as
consumer spans (process message_createdwithmessaging.system=max,max.update.type,
max.chat.id,max.update.handled,error.type) and renewed webhook subscriptions as
max.webhook.resubscribe. The span of an update is current in the coroutines of its handler, so
the requests the handler makes are its children; a webhook update handled synchronously continues
the trace of the server span. The starter createsMaxTracingwhen the application has an
OpenTelemetrybean (MaxTracingAutoConfiguration). -
MaxEventListener.updateContext(update): coroutine context elements the bot adds while it
dispatches an update, e.g. an OpenTelemetry context or an SLF4JMDCContext;compositecombines
the contexts of its listeners. TheonUpdateevent of the update is called within that context. -
Spring Boot starter: the webhook endpoint in reactive web applications (Spring WebFlux), with the
same answers as the servlet one: 200, 401, 400 and 503. Handlers run onDispatchers.Default, and
withmax.bot.webhook.async=falsethe response waits for them without blocking a thread. -
GraalVM native image:
maxapihas reflection metadata for all model classes, the bundled root CA
and the Kotlin built-ins inMETA-INF/native-image/io.github.securitytrip/maxapi; the starter
registers the same hints with Spring AOT (MaxRuntimeHintsinMETA-INF/spring/aot.factories). -
SuspendUpdateDeduplicatorwithsuspend fun markSeen(update), a non-blocking variant of
UpdateDeduplicator, set withBotConfig.suspendDeduplicator(also in the builder). The bot calls
markSeenof a deduplicator that implements both interfaces. The starter passes a
SuspendUpdateDeduplicatorbean to the bot when there is noUpdateDeduplicatorbean. -
MaxBot.isPollingandMaxBot.pollingFailure: whether long polling runs, and the error that ended it. -
Mini app backends, in the new package
ru.securitytrip.maxapi.webappofmaxapiwith no new
dependencies.WebAppData.validate(initData, botToken, maxAge)checkswindow.WebApp.initData
as https://dev.max.ru/docs/webapps/validation describes (values URL-decoded once, exactly one
hash,HMAC_SHA256(HMAC_SHA256("WebAppData", botToken), sorted key=value lines),auth_date
no older thanmaxAge, one hour by default) and returns the user, the chat,query_id,
auth_date,start_param,ipand all other fields; it throwsWebAppDataExceptionwith a
reason:MALFORMED,MISSING_HASH,DUPLICATE_HASH,INVALID_SIGNATUREorEXPIRED.
WebAppData.verifySignatureonly checks the signature and returns aBoolean. -
WebAppPhone.verifychecks the phone number fromrequestContact()of MAX Bridge
(HMAC_SHA256(botToken, "authDate=…\nphone=…\nuserId=…")), andContactAttachment.verify(also on
ContactAttachmentPayload) the contact shared with arequest_contactbutton
(HMAC_SHA256(botToken, vcf_info); checked against the live API). All hashes are compared in
constant time. -
MaxLinksbuilds deep links:bot,botStart(?start=, 1–128 characters),miniApp
(?startapp=, up to 512 characters of[A-Za-z0-9_-]) andshare(:share?text=), and throws
IllegalArgumentExceptionfor a payload Max would drop.MaxLimits.BOT_START_PAYLOAD_LENGTH,
MaxLimits.isValidOpenAppPayloadandMaxLimits.checkOpenAppPayloadare public. -
Spring Boot starter: controller parameters annotated with
@MaxWebAppUserget the checked
WebAppUserorWebAppData, and requests without validinitDataget 401.max.bot.web-app.source
says whereinitDatais (headerby default,parameterorbody), withmax.bot.web-app.header
(X-Max-Init-Data),max.bot.web-app.parameter(initData) andmax.bot.web-app.max-age(1h). -
maxapi-ktor: theMaxWebApproute plugin answers 401 to calls without validinitData(from the
X-Max-Init-Dataheader by default), andcall.maxInitData()andcall.maxWebAppUser()return
the checked data. -
examples-springhas a mini app backend with a page that calls it, anopen_appbutton, a
startapplink and a verifiedrequest_contactcontact. -
MaxLimitsholds the limits of Max, andMaxClientchecks them before sending: text of a message
or a comment up to 4000 characters; keyboards of up to 30 rows, 1 to 210 buttons and 7 buttons in
a row (3 in a row with alink,open_app,request_geo_locationorrequest_contactbutton,
as the live API enforces it); button text
of 1–128 characters,callbackandclipboardpayloads up to 1024,linkURLs up to 2048,
open_apppayloads up to 512 of[A-Za-z0-9_-]; up to 32 bot commands with names of 1–64 and
descriptions of 1–128 characters.sendMessage,editMessage,answerOnCallback,sendComment,
editComment,editMyCommandsandMaxBot.syncCommandsthrowIllegalArgumentExceptionwith
the broken limit instead of making a request. Lengths are counted in UTF-16 code units, as the
server counts them (checked against the live API: an emoji takes 2 characters). -
MaxClient.sendLongMessage(withsendLongMessageToChatandsendLongMessageToUser, in the
blocking and async facades too) andUpdateContext.replyLong(replyLongBlocking) send text
longer than 4000 characters as several messages; the keyboard and other attachments go with the
last one.TextSplitterdoes the splitting: between paragraphs, then lines, then words, never
inside a surrogate pair, and with Markdown and HTML markup closed and opened again at every cut. -
RateLimit.MAX: the limits of Max, 30 requests per second and 2 answers to button presses per
second in a chat.RateLimit.withPerChatAnswerssets the per-chat limit of answers in any
RateLimit;MaxBottells the client the chat of every press it dispatches, because
POST /answerscarries only thecallback_id. -
MaxBot.ensureWebhook(andensureWebhookBlocking) subscribes to a webhook again if Max dropped
the subscription after 8 hours without successful deliveries, and reports it to the new
MaxEventListener.onWebhookResubscribe.maxapi-micrometercounts it in
max.bot.webhook.resubscriptions. -
Spring Boot starter:
max.bot.rate-limit=none|max(defaultnone) setsRateLimit.MAX, and in
webhook mode withmax.bot.webhook.urlthe bot checks its subscription every
max.bot.webhook.check-interval(default 10 minutes,0turns it off) and subscribes again when
it is gone. -
Scenes:
scene("name") { ... }(Scene.builderin Java) describes a wizard on top of the dialog
state, so it works with anyStateStorage. Steps have a prompt, checks (validate, with an error
text or anonInvalidreaction) and an optionalinputthat reads other kinds of answers; answers
are kept inDialogState.dataunder the step names. A scene is entered withenterOnCommand,
enterOnCallbackorScene.enter(ctx, data);/backorbackButtonreturns a step,/cancelor
cancelButtonrunsonCancel, andoption(text, value)buttons answer only the current step.
BaseRouter.include(scene)adds it to a bot;SceneContextgives prompts and callbacks the handler
context withanswers. TheQuestionnaireBotexample is rewritten on a scene, with aBotTester
test. -
Keyboards:
Paginatorshows a long list page by page with a◀ 2/7 ▶row packed byCallbackData
(page and argument), andConfirmationis a "Yes / No" question withyes()andno()filters.
Both keep to the keyboard limits of Max: 7 buttons in a row, 3 with a wide button, 30 rows. -
Translationspicks texts byUpdateContext.localewith a fallback language, from maps or
ResourceBundles (Translations.fromBundles), with{0}and{name}placeholders and plural
forms (one/few/manyfor Russian, Ukrainian and Belarusian,one/otherotherwise). -
Chat registry: Max has no method that lists the chats of a bot, so with
BotConfig.chatStoreset
the bot remembers them frombot_added,bot_removed,bot_started,bot_stopped,
chat_title_changedand the first message from an unknown chat, andMaxBot.chats(vararg types)
(chatsBlocking) lists the active ones asKnownChat. Records keep the time of their event, so a
repeated or late delivery never undoes a newer one. Stores:InMemoryChatStore,
BlockingChatStorefor Java,JdbcChatStoreinmaxapi-jdbcandRedisChatStorein
maxapi-redis. These update types are requested even without handlers for them. -
Broadcasts:
Broadcaster.send(id, targets, message)(sendBlocking) sends a message, the same or
per target, to users and chats (BroadcastTarget) at up to 25 requests per second by default,
waits forRetry-Afteron 429, records 403 and 404 as unavailable without retrying, and returns a
BroadcastReport. The outcome of every target goes to aBroadcastStoreright away, so running the
broadcast again with the same id continues where it stopped. Stores:InMemoryBroadcastStore,
BlockingBroadcastStore,JdbcBroadcastStoreandRedisBroadcastStore(with an optional TTL). -
Spring Boot starter:
max.bot.storage=jdbc|redisalso creates the chat store (<prefix>chat,
<prefix>chats) and the broadcast store (<prefix>broadcast,<prefix>broadcast:); aChatStore
bean goes to the bot, and aBroadcasterbean uses theBroadcastStorebean (in memory without one). -
maxapi-test:TestUpdates.botAdded,botRemoved,botStoppedandchatTitleChanged.
Changed
RedisUpdateDeduplicatorandJdbcUpdateDeduplicatoralso implementSuspendUpdateDeduplicator,
so the bot no longer blocks its workers on them: Redis is asked with the asynchronous API of
Lettuce, JDBC runs onDispatchers.IO.isNewstill works as before.- Spring Boot starter: the storage deduplicators of
max.bot.storageare not created when the
application has its ownUpdateDeduplicatororSuspendUpdateDeduplicatorbean. Buttons.openAppandopenAppof the keyboard DSL check the payload when the button is created,
not only when the message is sent.- Messages, comments, answers and commands that break the limits of Max now fail with
IllegalArgumentExceptionbefore the request instead ofMaxApiException(400) from the server. - With a per-chat limit set (
RateLimit.perChatRequestsPerSecond), answers to button presses
dispatched byMaxBotcount towards the limit of their chat.
Deprecated
MaxClient.addMembersand its blocking and async versions: Max restricted
POST /chats/{chatId}/memberson 2026-09-09 and removed it on 2026-09-30. There is no replacement.