Widget reference for developers
Mount the widget in your app shell. Reuse your app's selected wallet and load the widget when Help opens.
The support widget In testing
Get the full module
The prompt includes the full support module once and uses current hosts and release flags. Follow Install for setup and Fix install problems for errors.
Connect the app
Read the workspace ID from the app's public config. A configured ID can skip Connect. Without one, the coding tool starts the flow. It never asks the user for a workspace ID.
Connect your app In testing
Check support.connectVerified at https://usetempl.com/api/status before starting.
If it is false, prepare the integration with support off and report that Connect is unavailable.
- Detect the app's exact HTTPS origins. Use its config, and leave out paths, queries, fragments and trailing slashes.
- Call
POST https://community.usetempl.com/connect/startwith JSON{ origins: [...] }. AddappNameonly when the app has a name. - Read
code,connectUrl,pollToken,expiresAtandintervalfrom the response. - Show only
connectUrlandcodeto the user. The user opens that link and signs in with their wallet. - The user confirms the app and origins. They create a workspace or choose one they manage.
- Call
POST https://community.usetempl.com/connect/pollwith JSON{ pollToken }until approval or expiry. - For
approved, checkworkspaceIdis a UUID. Save it in the app's public config and continue installing.
Keep pollToken only in memory. Never print it, log it, save it to a file or commit it.
Wait at least interval seconds between polls. Honor Retry-After and each larger interval.
For pending, keep polling until expiresAt. For denied or expired, stop and report the result.
If Connect fails, keep support off and report the error. Never invent an ID or ask for one.
Connect returns only the public workspace ID after approval. It grants the coding tool no account or staff access.
The link expires after 15 minutes. The code can approve only one request.
Creating a workspace uses no free resolved case.
Configure your site
Only the manager approves the exact HTTPS origin in Widget.
Connect uses the same approval rules. It saves origins only when the signed-in manager confirms them.
An origin includes the scheme, hostname and port. It has no path or trailing slash.
The manager API uses GET and POST /rooms/:workspaceId/widget.
Writes carry origins, minimumLevel, expectedPrincipalId and confirmed: true as needed.
The manager must have signed in recently. Each field saves on its own.
Mount the widget
The install prompt includes the full support module.
For TypeScript, save support-widget.d.ts beside the module.
Add it to your tsconfig include.
The widget loads from a hosted ES module. There is no npm package.
The returned widget has these methods:
open(), close(), setQuestion(text), setContext(context), captureFailure(input),
setWallet(wallet), setAppearance(partial), and destroy().
setQuestion(text) fills the message box and does not send it.
Pass the wallet
Call setSupportWallet whenever the wallet connects, changes account or network, or disconnects.
The helper calls the widget's setWallet method with the same value.
- EVM: pass
{ evm: provider }. Use the selected wallet's EIP-1193 provider. - With wagmi, use
await connector.getProvider()for the connector fromuseAccount(). - Without a wallet: pass
null. The widget can still accept guests.
Solana widget sign-in In testing
For Solana, pass { solana: wallet }.
Use useWallet() from @solana/wallet-adapter-react or the selected Wallet Standard wallet.
The wallet must support signing a message.
Pass one wallet family at a time. Never pass both evm and solana.
Changing wallets ends a signed-in session for the other wallet.
Call closeSupport() when the user signs out or the app shell unmounts.
On wallet disconnect, call setSupportWallet(null) and keep the widget open.
Keep it open across route changes.
Open support
Make your Help button call openSupport(workspaceId).
If it rejects, show "Support couldn't load. Try again."
Let the user press Help again to retry.
Without your own Help button, set SHOW_LAUNCHER to true.
Call loadSupport(workspaceId) when the app shell mounts.
Catch its rejection and show the same error with Retry.
Make Retry call loadSupport(workspaceId) again. Hide the error after a successful mount.
Clear the error and retry handler on sign-out or shell unmount. Ignore late failures after cleanup.
This example uses the workspace ID from your app's public config:
<div id="support-load-error" role="status" hidden>
<p>Support couldn't load. Try again.</p>
<button id="support-load-retry" type="button">Retry</button>
</div>
import { closeSupport, loadSupport } from "./templ-support.js";
function mountSupportLauncher() {
const error = document.querySelector("#support-load-error");
const retry = document.querySelector("#support-load-retry");
let active = true;
async function start() {
error.hidden = true;
try {
await loadSupport(workspaceId);
} catch {
if (active) error.hidden = false;
}
}
retry.addEventListener("click", start);
void start();
return () => {
active = false;
retry.removeEventListener("click", start);
error.hidden = true;
closeSupport();
};
}
const stopSupport = mountSupportLauncher();
Call stopSupport() on sign-out or shell unmount.
Keep the error state and cleanup in your framework's app shell.
A Help link beside a pending transaction can pass its network and hash:
depositHelp.addEventListener("click", () =>
openSupport(workspaceId, { network: "Base", transactionHash: deposit.hash }),
);
The user must tick it under Details from this app to share it.
An error screen can offer its current details with openSupport(workspaceId, failure).
The failure object uses the fields below.
This helper calls setContext(failure) and opens the widget.
Use captureSupportFailure in the failure handler to save the wallet and network at that moment.
Set appearance
The widget keeps your app's font. Leave out default options when you mount it.
| Option | Values | Default |
|---|---|---|
theme | "auto", "light", "dark" | "auto" |
accent | "#rrggbb" or { light: "#rrggbb", dark: "#rrggbb" } | Templ green |
radius | "square", "soft", "round" | "soft" |
position | "right", "left" | "right" |
Auto follows your app before the user's system setting. It checks these sources in order:
- Computed
color-schemeonhtmlandbody, and<meta name="color-scheme">. - Classes,
data-theme, anddata-modecontaininglightordarkonhtmlandbody. - The page's background brightness. Transparent backgrounds use the background above them, up to
html. - The user's
prefers-color-schemesetting.
Auto updates when those root attributes or the system setting change. Appearance checks read only styles, these attributes, and the meta tag. They make no network requests.
Use one accent for both themes, or choose one for each theme:
const widget = mountSupportWidget({
workspace: workspaceId,
appearance: {
accent: { light: "#176451", dark: "#9945FF" },
radius: "round",
position: "left",
},
});
Filled buttons and the launcher keep your exact accent. Their text uses dark or light ink for contrast. Hover and pressed colors give feedback. The widget adjusts accent lightness for readable text, links, and focus rings. It keeps the accent's hue and chroma when adjusting lightness. Invalid accents use Templ green. Radius changes the panel, message bubbles, inputs, buttons, and launcher.
Call setAppearance(partial) when your app changes its theme or colors:
widget.setAppearance({ theme: "dark", accent: "#2563EB" });
widget.setAppearance({ theme: "auto", radius: "square", position: "right" });
Each call changes only the options you pass.
It keeps the open case and draft.
Set the logo when you mount the widget.
appearance.logo accepts an HTTPS URL or a path on your site.
The header shows a chat icon when no logo loads.
prompts adds suggested questions before the user sends a message.
Clicking a question opens the send summary. The user presses Send to share it.
For workspaces that require wallet sign-in, questions appear after sign-in.
showLauncher: false hides the floating button.
Closing the widget returns focus to the button that opened it, when that button still exists.
Allow widget files
If your app has a content security policy, allow these hosts:
| Directive | Allowed host | Purpose |
|---|---|---|
script-src | https://usetempl.com | Widget module and chunks |
connect-src | https://community.usetempl.com wss://community.usetempl.com | Widget API and live replies |
img-src | https://usetempl.com | Widget art |
Use the hosts in your install prompt for your environment.
Allow both API and WebSocket hosts in connect-src.
Widget code and art share the same app host.
Allow your custom logo's host in img-src too.
With strict-dynamic, give the script that bundles templ-support.ts the page's nonce.
The widget uses an inline stylesheet inside its Shadow DOM.
If your policy limits style-src, make styleNonce() return the page's style nonce.
The module passes it as styleNonce.
For your own interface and stylesheet, use the exported createSupportClient.
Capture a failed wallet action
Find each handler where a wallet action fails.
This includes wallet rejection, failed simulation, failed submission, and failure after a transaction was sent.
Call captureSupportFailure(workspaceId, failure) there right away.
Do not wait for it before showing the app's error. Ignore its rejection.
The helper calls captureFailure.
It saves the failure with the wallet and network at that moment.
A wallet or network change leaves those saved details unchanged.
Use only fields your app has. Keep secrets out of error messages. This example uses a fixed error message:
try {
await vault.deposit(amount);
} catch {
showDepositError("Deposit failed.");
void captureSupportFailure(workspaceId, {
network: "eip155:8453",
app: {
page: "/vault",
action: "deposit",
product: { label: "USDC vault" },
shown: {
value: "25.5",
metric: { label: "Deposit amount" },
asOf: new Date().toISOString(),
},
error: { code: "DEPOSIT_FAILED", message: "Deposit failed." },
appVersion: "1.0.0",
device: "Chrome on macOS",
failure: {
kind: "other",
amount: "25.5",
asset: { symbol: "USDC" },
},
},
}).catch(() => {});
}
getHelp.addEventListener("click", () => openSupport(workspaceId));
Show the app's error with a Get help button.
That button opens the saved failure.
Add transactionHash beside network when the app has a transaction hash.
Do not make up a hash for a rejected wallet action.
Each field has these rules:
network: use"eip155:<chainId>", such as"eip155:8453"for Base. A known name such as"Base"also works.transactionHash: an EVM transaction hash has0xfollowed by 64 hex characters. A supported explorer link also works.page: starts with"/", with at most 200 characters. Use the path only, with no query, fragment or whitespace.action:deposit,withdraw,claim,migrate,swap,stake,unstake,approve,bridge, orother.product: an object with a label of at most 80 bytes, or an address with its network.product.key: optional lowercase key. Start with a letter or digit. Use letters, digits, dots, underscores, colons, slashes, or hyphens, up to 80 characters.shown.value: the value the app showed, with at most 40 characters.shown.metric: optional{ label }or{ key }. Its label and key follow the product rules.shown.period: optional plain text, at most 40 characters.shown.asOf: optional ISO 8601 time string, at most 40 characters.error.code: at most 64 characters, using only letters, digits and_ . : -.error.message: at most 300 bytes. Exclude other people's wallet addresses, tokens, cookies, emails, and keys.appVersion: a short string, at most 60 bytes.device: a short string, at most 60 bytes.connector: an optional wallet app name, at most 60 bytes.failure.kindis required:rejected,simulation,submission,broadcastorother.rejectedmeans the wallet rejected the action.broadcastmeans a sent transaction failed. Without it,captureSupportFailurerejects and nothing is saved.failure.amount: an optional decimal string, at most 40 characters. Use no sign, exponent or zeros before another digit.failure.asset: the asset's symbol (at most 60 bytes), or its address with its network.failure.at: an optional ISO 8601 time string, at most 40 characters. The widget records the capture time when absent.
network and transactionHash inputs each accept at most 300 characters.
Byte limits count UTF-8 bytes. Most text fields must stay on one line.
error.message can include line breaks.
For Solana, use network: "solana".
Use the transaction's base58 signature as transactionHash, or a supported Solana explorer link.
The signature must decode to 64 bytes.
A Solana product address must decode to 32 bytes.
Templ leaves out any other field it can't read and logs a console warning. The other fields are still saved. The helper saves the failure, time and wallet before loading the widget. A wallet change during loading keeps the saved wallet. A failed load or capture keeps the saved details. Get help retries capture before opening the chat. The widget adds its own build and API version. Each detail shows unticked under Details from this app until the user ticks it. The user still chooses what to share. Nothing is sent before that choice.
Do not attach portfolio data, private team notes, or secrets from URL query strings. To add your AI agent, follow Connect your own AI agent.
Keep sign-in scoped
The EVM widget asks for a fresh SIWE signature through its sign-in card. It cannot reuse your app's sign-in message or session cookie. The server checks the approved origin, workspace, one-time challenge, message, and signature. Chat cannot sign or send transactions. Never ask for a private key or recovery phrase.
Before sending, the widget shows the message, wallet status, and each detail the user ticked. When the workspace builds de-identified copies, it also shows that notice with a link to Privacy. The user presses Send to share or Edit to go back. A guest's app-reported wallet starts unticked and remains unverified when shared. At the Attached wallet level, the wallet row stays ticked. The user can go back without sending.
Guest sessions share the origin and workspace limits. They hold only the member permission role.
Opening the widget alone stores no guest session.
Sending the opening message creates it. The browser stores a return credential for that user's case history.
Host-site scripts can read that storage. Include it in your privacy notice.
Only your team can ask a user to verify their wallet. Signing in from that guest session moves its case history to the wallet. The guest's own credential must redeem the proof. That guest credential then stops working. If the wallet already has case history, both stay linked.
The widget token is bound to the exact origin and workspace. It uses no third-party cookies and gains no staff permissions. A guest can return with the stored credential. A signed-in user signs in again unless they chose remembered sign-in. The client checks the selected wallet before and after authenticated requests. A changed or unreadable wallet ends the session.
Check EVM sign-in networks
| Network | Chain ID |
|---|---|
| Ethereum In testing | 1 |
| OP Mainnet In testing | 10 |
| BNB Chain In testing | 56 |
| Polygon In testing | 137 |
| Sonic In testing | 146 |
| zkSync Era In testing | 324 |
| Base In testing | 8453 |
| Arbitrum One In testing | 42161 |
| Avalanche In testing | 43114 |
| Linea In testing | 59144 |
| Blast In testing | 81457 |
| Scroll In testing | 534352 |
| Katana In testing | 747474 |
| Robinhood Chain In testing | 4663 |
These rows name wallet sign-in networks. Transaction checks have their own enabled network list. Contract-wallet signatures fail closed unless their separate sign-in checks pass.
Keep drafts private
The widget keeps unsent drafts and retry details in the host site's local storage by default. It separates them by origin, workspace, and account. Closing the widget or signing out keeps the saved draft. The same account can restore it. A confirmed send or Discard removes it. Clearing the text box removes the draft too.
Draft storage includes text, transaction hashes, retry details, and the context approved for that message.
It excludes wallet proofs, session tokens, and sent history.
Guest credentials have their own entry.
Set persistDrafts: false for memory-only drafts.
When browser storage fails, the draft stays in memory and the widget says so.
A failed send shows Not sent., Retry, and Discard. Retry sends the same message and approved details again. If only the details failed, it sends only those details. A restored draft sends only when the user chooses Retry. Discard removes the browser's copy. It does not remove a message already delivered.
Check the integration
- Send a case from the approved site. Reply from the team inbox.
- Return with the same account. Check another account cannot read it.
- Check users cannot see internal notes.
- Ask as a guest, then choose Ask to verify wallet in the inbox.
- Sign in through the widget. Check that the case keeps its history.
- Test a rejected signature, expired session, changed wallet and unapproved origin.
A paused or suspended workspace shows its fixed message when it cannot take a case. Never ask for a private key or recovery phrase.
In testing means the feature is built and not yet open to every workspace.