SMeet Mini Apps
SDK
The SDK is one small script without dependencies. It connects the page to the SMeet window, checks that the window really is SMeet, and gives the page login codes, the environment and a way to send results to the bot.
Files and addresses#
Version 1.0.0 comes in two builds of the same code:
| File | What it is |
|---|---|
smeet-mini-apps-1.0.0.js | a classic script; defines the global object SMeetMiniApp |
smeet-mini-apps-1.0.0.esm.js | an ES module with the same exports: connect, NotInSMeetError, SMeetBridgeError, version |
smeet-mini-apps-1.js, smeet-mini-apps-1.esm.js | aliases: the newest 1.x of each build |
SMeet serves them at https://messenger.scrile.com/docs/mini-apps/sdk/<file>, for example smeet-mini-apps-1.0.0.js. A version never changes and is cached for a year; an alias moves to each new 1.x and is cached for a few minutes. Both are served to any site (Access-Control-Allow-Origin: *), so the module build loads from another origin too.
For a production app, put the SDK into your own build or serve the file from your origin: your page then does not depend on another server, and your script-src names only your own origin. Each example archive has the files of the version its example uses in sdk/, with their TypeScript declarations (*.d.ts).
Loading#
Load the SDK synchronously on every page of the app, before the page's load event: a <script src> in <head>, or a <script type="module">. Never load it lazily, after load, or from a button.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/smeet-mini-apps-1.0.0.js"></script>
<script src="/static/page.js" defer></script>
</head>import * as SMeetMiniApp from "/static/smeet-mini-apps-1.0.0.esm.js";Why: SMeet waits 15 seconds for the first page of the window to connect, and shows "The app is not responding or does not allow embedding" otherwise. When the person moves to the next page of your app, SMeet covers the window until that page's SDK answers, because only a live SDK proves that the page is still on your domain. A page without the SDK, or with the SDK loaded late, stays covered.
Load one SDK per page.
connect()#
let app;
try {
app = await SMeetMiniApp.connect();
} catch (error) {
if (error instanceof SMeetMiniApp.NotInSMeetError) return showOpenInSMeet();
if (error instanceof SMeetMiniApp.SMeetBridgeError) return showError(error.code); // NOT_READY after 10 s
throw error; // TypeError: a wrong containerOrigins
}
drawFirstScreen(app.environment);
await app.ready();- Outside SMeet (the page is not in a frame and has no SMeet handler on iOS) it fails at once with
NotInSMeetErrorand listens to nothing. A page opened withwindow.openhas no bridge. - Without an answer within 10 seconds it fails with
SMeetBridgeErrorNOT_READY. - A wrong
containerOriginsfails at once withTypeError, before anything is sent, and keeps nothing: the nextconnect()starts afresh. - Once the handshake has started,
connect()runs once per document: a second call returns the same promise, and a failure is final until the page loads again; the person can press Retry or Reload in the window. - The object
appexists only afterconnect()has finished; before that the page has nothing to call.
connect({containerOrigins}) replaces the list of SMeet origins the SDK accepts, ["https://messenger.scrile.com"]. It is for a local SMeet stand only: an http origin in it counts only while the page itself is open on localhost or 127.0.0.1; on a production page a list of only http origins fails with TypeError, so a page that kept the stand's list connects to nothing.
The environment#
app.environment is what SMeet says about this launch. The SDK keeps it up to date when the theme, the size or the language changes; app.getEnvironment() asks SMeet again.
| Field | Meaning |
|---|---|
bridgeVersion | 1 |
platform | web, ios or macos |
appId | the id of the Mini App, a decimal string |
deployment | production or test |
startParam | the parameter of the button or the link, up to 64 characters of A-Z a-z 0-9 _ -, or null |
language | ru or en, the language of the person's SMeet |
colorScheme | light or dark |
theme | colors: bg, text, hint, link, button, buttonText, secondaryBg (Theme) |
viewport | {width, height, isExpanded}: the visible area under the SMeet bar and above the on-screen keyboard |
safeArea | {top, bottom, left, right}: the cut-outs of the device that cross the visible area |
methods | the methods this SMeet client serves |
Use startParam to open the right screen, never as a permission: anyone can put any parameter into a link. Your server gets the same value as start_param of the exchange.
Methods#
| Call | Result | What it does |
|---|---|---|
app.supports(method) | boolean | whether environment.methods lists the method |
app.getEnvironment() | the environment | asks SMeet for the environment again |
app.ready() | the first screen is drawn: SMeet removes its loading screen. Call it within 15 seconds of connecting | |
app.authorize() | {code, codeVerifier, expiresAt} | a new login code (Sign-in) |
app.submitToBot(data, {requestId}) | {submissionId, status} | sends data to the bot after the person confirms it (Sending to the bot) |
app.getSubmissionStatus(submissionId) | {status} | where a submission stands |
app.openExternalLink(url) | {opened} | opens an https address in the browser after the person agrees; opened is false when they did not. In the web app, after three presses of Cancel in a row in one launch, the next calls get RATE_LIMITED until the person opens the app again |
app.setBackHandler(handler) | handler is called on SMeet's Back instead of SMeet acting itself; null gives Back back to SMeet | |
app.setClosingConfirmation(enabled) | while true, SMeet asks once before it closes the window: the page has unsaved changes | |
app.close() | closes the window |
Every method returns a promise. A method that is not in environment.methods fails with UNSUPPORTED_METHOD without reaching SMeet; check with app.supports() when a feature is optional.
Events#
| Subscribe | Data | When |
|---|---|---|
app.onThemeChanged(callback) | {colorScheme, theme} | the person switched the light or dark look of SMeet |
app.onViewportChanged(callback) | {viewport, safeArea} | the keyboard opened or closed, the device turned, the window was resized, expanded or collapsed |
app.onVisibilityChanged(callback) | {visible} | SMeet hid the window, for example during a call, and showed it again |
app.onLanguageChanged(callback) | {language} | web only: the person changed the language; on iOS and macOS that closes the window |
Each on... returns a function that removes the callback. When a callback runs, app.environment already holds the new values. Back comes to the handler of setBackHandler(), not as a subscription.
Sending to the bot#
const draft = loadDraft() || { requestId: crypto.randomUUID(), fields: {} };
try {
const { submissionId } = await app.submitToBot(draft.fields, { requestId: draft.requestId });
clearDraft();
showReceipt(submissionId);
} catch (error) {
if (error.code === "USER_DECLINED") return; // the person pressed Cancel
if (error.code === "SUBMISSION_CONFLICT") draft.requestId = crypto.randomUUID();
saveDraft(draft); // try again later with the same requestId
}- SMeet shows the person the app, the bot, the size and the fields, and sends only after Send. One confirmation at a time: a second call meanwhile gets
BUSY. In the web app, after three presses of Cancel in a row in one launch,submitToBot()answersRATE_LIMITEDuntil the person opens the app again: neither waiting nor Reload in the window resets it. Keep the draft and itsrequestId; the person sends it from a new window. datagoes in its JSON form, which must be an object, not an array, of up to 16 KiB in UTF-8. ADate, aMap, aSetor an object whosetoJSONgives no object is refused withINVALID_PARAMS.requestIdis 1 to 64 characters ofA-Z a-z 0-9 . _ : -. Make it together with the draft and keep it with the draft. Sending again with the samerequestIdand the same data, after an error, a reload or in a new window, gives the samesubmissionIdand no second update for the bot. The samerequestIdwith other data fails withSUBMISSION_CONFLICT: make a new one.- The bot receives a
mini_app_submissionupdate withdataexactly as sent,client_request_id=requestId, and the person and chat fixed by SMeet (The update).
app.getSubmissionStatus(submissionId) answers for submissions of the same person, app, environment and space, also those of an earlier launch:
| Status | Means |
|---|---|
pending | in the bot's queue, not confirmed yet |
delivered | the bot confirmed it |
failed | it is in the owner's list of failed updates |
cancelled | cancelled before delivery, or skipped by the owner |
expired | not confirmed within 7 days |
The answer of submitToBot() itself has the status accepted. An unknown, foreign or deleted submission gets INVALID_PARAMS.
Errors#
Every refusal of SMeet comes as SMeetBridgeError with one of these codes in error.code. Any other code from SMeet comes as INTERNAL, with the original code in error.message.
| Code | Means | What to do |
|---|---|---|
UNSUPPORTED_METHOD | this SMeet client does not serve the method | check app.supports() and do without it |
INVALID_PARAMS | the parameters are wrong, or the submission is unknown | fix the call; do not repeat it as it is |
NOT_READY | the window shows an error screen, or SMeet did not answer connect() in time | nothing; the person retries in the window |
USER_DECLINED | the person pressed Cancel on the confirmation | keep the draft |
BUSY | a confirmation is open already | wait for the first call |
LAUNCH_CLOSED | the launch is over; SMeet tells the person why | stop; keep the draft |
CODE_EXPIRED | the code of this request expired before it reached the page | call authorize() again |
RATE_LIMITED | too many calls, or, in the web app, three presses of Cancel in a row in this launch | wait and repeat; after three Cancel keep the draft: the method stays refused until the person opens the app again |
PAYLOAD_TOO_LARGE | a message over 64 KiB, or data over 16 KiB | send less |
SUBMISSION_CONFLICT | this requestId was used with other data | make a new requestId |
QUEUE_FULL | the bot's queue is full | keep the draft, repeat later with the same requestId |
NOT_ALLOWED | the configuration of the app does not allow it (for example sending to the bot) | ask for the capability in the settings |
MAINTENANCE | Mini Apps are paused | say so, keep the draft, repeat later |
NETWORK | SMeet could not reach its server, or the page was replaced while the call was open | call the method again |
INTERNAL | a fault of SMeet | repeat once, then show an error |
CODE_EXPIRED and NETWORK are meant to be repeated: call the method again, which is a new request. A repeated submitToBot() with the same requestId asks the person again.
The bridge#
The SDK talks to the SMeet window over the bridge, version 1. You do not need to know it to use the SDK; it is described for those who check messages or test their pages.
- In web the page and the window meet with a
hellofrom the page and aconnectfrom SMeet, which hands over aMessagePort. The SDK takes a port only fromwindow.parent, only from an origin of its list (https://messenger.scrile.com), only with the bridge's name and version and exactly one port. Then requests, replies and events travel over the port; SMeet sendspingevery second, the SDK answerspong, and onpagehideit sendsleaving. - The domain in the bar of the window stays confirmed only while the page answers. When the page goes to another site or stops answering, SMeet covers it within 2 seconds and says "The page left <domain> or is not responding".
- On iOS and macOS the page sends requests to the
smeetmessage handler of the web view and gets each reply as the handler's answer; events arrive throughwindow.__smeetMiniAppEvent. - A message is at most 64 KiB; a page sends at most 20 requests a second.
Every message, both ways, is described by the JSON Schema bridge.schema.json; the SDK's tests check the messages it sends and accepts against it. The schema at this address describes the current bridge exactly. Within version 1 each new release of it only adds optional fields, methods, events, status values and error codes, so what was valid stays valid. Check what your page sends, and your test doubles of SMeet, against the current schema; at run time skip what a message of SMeet carries beyond what you know instead of rejecting the message (Changelog).
What the SDK does not do#
- It stores nothing: no browser storage, no cookies, nothing between documents. Unfinished requests live in the page's memory only to be sent again over a new port.
- It checks no rights. SMeet checks them on its server for every call.
- It has no switches for tests. To try your page outside SMeet, handle
NotInSMeetError; to test against a local SMeet stand, passcontainerOrigins.
TypeScript#
The declarations come with the SDK in every example archive: smeet-mini-apps-1.0.0.d.ts describes the global namespace SMeetMiniApp of the classic script, smeet-mini-apps-1.0.0.esm.d.ts the module. The types Environment, MiniApp, BridgeErrorCode, SubmitResult and the others have the names used on this page.