Bridge V1 API#
getHost() returns the injected window.host or throws HOST_UNAVAILABLE outside
Tabs. Every method returns a promise. Serialize calls: the native broker handles
one privileged operation at a time. The browser bridge allows up to eight pending
requests, 256 calls per visit and 64 KiB per transport envelope, with a five-minute
response timeout. On host 0.0.62+, the native host renews a thirty-minute server lease every five
minutes while foregrounded, and revalidates it on resume. Active visits have no
fixed time limit. Closing disables the local bridge and revokes the lease;
abandoned leases expire. Renewal preserves consent, replay history and budgets. Backgrounding and navigation may invalidate late results.
| Method | Input | Result | Permission |
|---|---|---|---|
auth.getAssertion(input) |
{audience, nonce} |
{token, issuer, expiresAt} |
identity.authenticate |
identity.getSelf() |
None | {did, handle, displayName, avatar, appId} |
identity.basic |
identity.resolveHandle(handle) |
Handle string | {did, handle} |
social.public.read |
records.create(input) |
{collection, value} |
{uri, cid} |
records.write |
records.update(input) |
{collection, rkey, value} |
{uri, cid} |
records.write |
records.delete(input) |
{collection, rkey} |
Empty completion (current host returns null) |
records.write |
publicData.query(input) |
{collection, query?} |
{records, nextCursor} |
records.read |
payments.resolve(handle) |
Handle string | {did, handle, displayName, avatar, canReceive} |
payments.request |
payments.request(input) |
{recipientDid, asset, amount, memo?} |
Payment result below | payments.request |
chat.openConversation(input) |
{recipientDid} |
Empty completion (current host returns null) |
chat.openConversation |
Identity supplies the public profile display name and avatar URL, falling back to
the handle when no display name is set. Avatars are public PDS blobs. DIDs use
did:plc: followed by 24 base32 characters. Public identity is not a signed login
proof for your backend; see backends.
Public records#
The collection must be declared in collections for writes or readCollections
for SDK reads. A record's $type must equal the collection ID; the body must match
the approved Lexicon. Records are bounded to 16 KiB JSON, 2,048 nodes and depth 16;
unknown fields, NUL strings and unsafe numbers are rejected. Blob/media upload is
not available through this API.
Create assigns an app-owned key. Use recordKey(result.uri, collection) to extract
it. Update sends the complete replacement record, not a patch. Update/delete may
target only records created by this app for this account. Your own AppView cannot
infer this private ownership from a public record alone. Keep returned references
for convenient editing, and plan for cache loss or a new device. There is no bridge
ownership-history enumeration API yet.
const saved = await host.records.create({
collection: "org.example.recipes.recipe",
value: {
$type: "org.example.recipes.recipe",
title: "Soup",
ingredients: [{ name: "Carrot", quantity: 2 }],
},
});
await host.records.update({
collection: "org.example.recipes.recipe",
rkey: recordKey(saved.uri, "org.example.recipes.recipe"),
value: {
$type: "org.example.recipes.recipe",
title: "New soup",
ingredients: [],
},
});
publicData.query uses Tabs' shared AppView. Optional query keys are author
(DID), limit and cursor (decimal strings, default "30" and "0"). Limit
is 1–100, cursor is 0–100000. Each result is {uri, cid, author: {did, handle}, record, publishedAt}. publishedAt is the index's first-seen time for generic
records. nextCursor is a string or null. Public index refresh is asynchronous;
offset pagination can shift during writes. Your own API can use a different query
contract. See shared AppView.
Native actions#
Only USDC is currently supported. Amount is a positive decimal string with at most six fractional digits and a maximum of 18446744073709.551615 USDC (the native u64 unit bound). Never calculate currency with floating-point arithmetic. Memo, if supplied, is nonempty and at most 280 UTF-8 bytes. Resolve the recipient first and retain the DID rather than substituting an unverified handle/address.
const person = await host.payments.resolve("bob.tabschat.com");
const result = await host.payments.request({
recipientDid: person.did,
asset: "USDC",
amount: "0.01",
});
Results are {status: 'sent', digest}, {status: 'cancelled'}, or
{status: 'pending' | 'failed' | 'expired'}. Every new transaction gets native
amount/recipient confirmation and a fresh passkey, regardless of saved mini-app
consent or chat login. A timeout or pending/failed/expired result is not proof of
payment. Direct the user to Tabs to check the transaction; do not automatically
issue another request. A fulfilment backend must verify settlement independently;
JavaScript responses are not a backend payment proof. The page receives no wallet
address, signing key, transaction bytes or Tabs account token.
chat.openConversation({recipientDid}) opens native UI. It provides no message
history, message-send API, voice/camera access or conversation keys.
Errors and uncertain writes#
Rejected promises have a fixed error.code. Use describeError(error) for safe
copy; unknown codes need a generic fallback.
| Code | Suggested handling |
|---|---|
HOST_UNAVAILABLE |
Open in Tabs, or use the explicitly marked browser preview |
PERMISSION_DENIED, FORBIDDEN |
Show a disabled feature; point to App permissions; never loop prompts |
UNAUTHORIZED |
Reopen the app; the visit/account may no longer be active |
INVALID_INPUT |
Correct the data or manifest scope |
NOT_FOUND |
Handle missing/deleted records or unavailable recipients |
BUSY |
Wait for the current native action |
RATE_LIMITED |
Back off; reopening may be needed for visit limits |
CONFLICT, REPLAY |
Refresh/reconcile; do not change and replay an uncertain write |
TIMEOUT, PAYMENT_PENDING |
Check Tabs and reconcile before another payment/write |
UNAVAILABLE or unknown |
Offer a later retry; do not expose raw error bodies |
Each new SDK invocation receives a new operation ID. The host can retry the same
transport operation idempotently, but a developer calling records.create again
creates a new operation and can create a duplicate. Persist a pending UI state,
refresh public data and reconcile ambiguous outcomes instead of blindly retrying.
Developer login and files#
auth.getAssertion requires host 0.0.51+, an exact allowed HTTPS backend origin,
and a server-generated base64url nonce of 22–128 characters. Your backend must
verify the proof and consume its own challenge once. Do not treat the public
identity API as authentication. The SDK's signInWithTabs helper implements the
example /auth/challenge and /auth/session protocol; the session it returns is
your app's own credential. Server verification helpers live under
@tabschat/miniapp-sdk/server/auth and must run in the developer backend.
files.select enables ordinary <input type="file">; it has no bridge method
for arbitrary paths. SDK selectFiles({accept, multiple}) opens a normal input
and returns File[] (or [] on cancellation). Invoke it directly in a click
handler, then upload through fetch/FormData. The native picker has its own
foreground-return validation; ordinary late bridge results still follow the
background/navigation cancellation rules above. See the complete flow and
limits and the runnable example.
PDS blobs (host 0.0.52+)#
host.blobs.upload(fileOrBlob, {collection}) returns a standard ATProto blob reference.
Declare blobs.upload and records.write, and use a declared write collection.
Binary data uses a single-use HTTPS permit, outside the JSON bridge.
See media limits and protocol.
Payment fulfilment identity (host 0.0.62+)#
An app with both identity.authenticate and payments.request can request
auth.getAssertion({audience, nonce, paymentAccount: true}), or use
signInWithTabs({backendOrigin, paymentAccount: true}). The signed assertion
includes payment_account: {network: "mainnet", senderCommitment}. The commitment
is SHA-256 of tabs-miniapp-payment-v1\n<appId>\n<audience>\nmainnet\n<lowercase 0x sender address>.
The host obtains the address from the active account binding; the app receives
only the scoped commitment. A developer backend independently verifies settlement,
amount, coin type, recipient and the chain sender against this signed commitment.
The commitment is not settlement proof. Retain unique digests and idempotent orders
and reconcile pending transactions before charging again. Missing wallet bindings
return NOT_FOUND. Older hosts reject the optional input.