[{"data":1,"prerenderedAt":53},["ShallowReactive",2],{"doc-connect":3},{"html":4,"headings":5},"\u003Cp>Three steps. Nothing in help-bites knows about your app beyond what you put\non the project; your users keep their existing accounts.\u003C\u002Fp>\n\u003Ch2 id=\"1-create-a-project\">1. Create a project\u003Ca class=\"heading-anchor\" href=\"#1-create-a-project\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Admin panel → Projects → New project. You get:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>a \u003Cstrong>public key\u003C\u002Fstrong> \u003Ccode>pk_…\u003C\u002Fcode> — safe to ship to browsers; identifies the project\u003C\u002Fli>\n\u003Cli>a \u003Cstrong>secret\u003C\u002Fstrong> \u003Ccode>sk_…\u003C\u002Fcode> — server only; signs identity tokens\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Set the site URL (used in email links), the origins allowed to host the widget,\nthe name replies are signed with, and the email addresses to notify when a user\nwrites.\u003C\u002Fp>\n\u003Ch2 id=\"2-mint-identity-tokens-on-your-server\">2. Mint identity tokens on your server\u003Ca class=\"heading-anchor\" href=\"#2-mint-identity-tokens-on-your-server\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Add one authenticated route to your backend that returns a short-lived HS256\nJWT for the signed-in user. Claims:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>claim\u003C\u002Fth>\n\u003Cth>value\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>iss\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the project public key\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>sub\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>your user's id (stable, unique per project)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>name\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional display name\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>email\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional; enables email notifications for that user\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>username\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional; the public handle feedback boards show and \u003Ccode>@mention\u003C\u002Fcode> by — never an email address\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>avatar\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional; an http(s) image URL for feedback boards\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>locale\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional; the person's language, a tag such as \u003Ccode>es\u003C\u002Fcode> or \u003Ccode>pt-BR\u003C\u002Fcode>. It fills the contact's language the first time (one set in the panel or over the API is never replaced), and campaigns and sequences then send the matching translation of a template. See \u003Ca href=\"\u002Fdocs\u002Flanguages#a-contacts-language\">Languages\u003C\u002Fa>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>role\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>optional; \u003Ccode>admin\u003C\u002Fcode> lets one of your staff moderate the feedback board\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>iat\u003C\u002Fcode>, \u003Ccode>exp\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>issued-at and expiry (an hour is plenty)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Node, no dependencies:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-kwd\">import\u003C\u002Fspan> { createHmac } \u003Cspan class=\"shj-syn-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'node:crypto'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">mintSupportToken\u003C\u002Fspan>(user) {\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">b64\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> v \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">Buffer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">from\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">stringify\u003C\u002Fspan>(v))\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">toString\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'base64url'\u003C\u002Fspan>)\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> now \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">Math\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">floor\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-class\">Date\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">now\u003C\u002Fspan>() \u003Cspan class=\"shj-syn-oper\">\u002F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">1000\u003C\u002Fspan>)\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> head \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">b64\u003C\u002Fspan>({ alg\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'HS256'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> typ\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'JWT'\u003C\u002Fspan> })\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> body \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">b64\u003C\u002Fspan>({ iss\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> process\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>env\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">HELP_BITES_PROJECT_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> sub\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> name\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>name\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> email\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>email\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> iat\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> now\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> exp\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> now \u003Cspan class=\"shj-syn-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">3600\u003C\u002Fspan> })\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> sig \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">createHmac\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'sha256'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> process\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>env\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">HELP_BITES_SECRET\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">update\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>head\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>body\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">digest\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'base64url'\u003C\u002Fspan>)\n  \u003Cspan class=\"shj-syn-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>head\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>body\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>sig\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Any JWT library works too (\u003Ccode>jose\u003C\u002Fcode>, \u003Ccode>jsonwebtoken\u003C\u002Fcode>, PyJWT, …): algorithm HS256,\nsecret = the project secret.\u003C\u002Fp>\n\u003Cp>Never send the secret to a browser. Rotate it from the project page if it leaks;\ntokens minted with the old one stop verifying immediately.\u003C\u002Fp>\n\u003Ch2 id=\"3-show-the-chat\">3. Show the chat\u003Ca class=\"heading-anchor\" href=\"#3-show-the-chat\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Ch3 id=\"drop-in-widget\">Drop-in widget\u003Ca class=\"heading-anchor\" href=\"#drop-in-widget\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fwidget.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">defer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n  window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">||\u003C\u002Fspan> []\n  \u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">push\u003C\u002Fspan>([\u003Cspan class=\"shj-syn-str\">'init'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {\n    getToken\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> () \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">fetch\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'\u002Fapi\u002Fsupport\u002Ftoken'\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">json\u003C\u002Fspan>())\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>token)\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    context\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> { type\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'bookmark'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> id\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'abc'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> label\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'The thing they are looking at'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> url\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> location\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>href } \u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F optional\n\u003C\u002Fspan>  }])\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cul>\n\u003Cli>\u003Ccode>getToken\u003C\u002Fcode> is called when the chat opens and again whenever a token expires.\u003C\u002Fli>\n\u003Cli>\u003Ccode>context\u003C\u002Fcode> is attached to new conversations so you know what the user was\nlooking at. Update it with \u003Ccode>HelpBites.push(['setContext', {...}])\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>HelpBites.push(['open'])\u003C\u002Fcode> \u002F \u003Ccode>(['close'])\u003C\u002Fcode> control the panel.\u003C\u002Fli>\n\u003Cli>\u003Ccode>data-position=&quot;left&quot;\u003C\u002Fcode> and \u003Ccode>data-accent=&quot;#hex&quot;\u003C\u002Fcode> on the script tag tweak the launcher.\u003C\u002Fli>\n\u003Cli>A link with \u003Ccode>?sb=&lt;conversationId&gt;\u003C\u002Fcode> (what notification emails use) opens that\nconversation on load.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Only origins listed on the project can host the widget; anything else sees an\nerror inside the frame and never gets a token.\u003C\u002Fp>\n\u003Ch3 id=\"one-tag-for-everything\">One tag for everything\u003Ca class=\"heading-anchor\" href=\"#one-tag-for-everything\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Instead of a script per feature, \u003Ccode>helpbites.js\u003C\u002Fcode> asks the project what it has and\nadds the chat, the feedback board, the changelog and onboarding that are set up:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fhelpbites.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">async\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>It adds the chat by default; name more with \u003Ccode>data-features=&quot;chat,feedback&quot;\u003C\u002Fcode> or\ngive one its own setting (\u003Ccode>data-changelog-target=&quot;#changelog&quot;\u003C\u002Fcode>, \u003Ccode>data-flow=&quot;welcome&quot;\u003C\u002Fcode>).\n\u003Ccode>data-position\u003C\u002Fcode>, \u003Ccode>data-accent\u003C\u002Fcode>, \u003Ccode>data-mode\u003C\u002Fcode>, \u003Ccode>data-token\u003C\u002Fcode> and \u003Ccode>data-locale\u003C\u002Fcode> pass\nthrough to the scripts, \u003Ccode>HelpBites.push([...])\u003C\u002Fcode> keeps working, and a script\nalready on the page is never added twice. The project can also turn on a Home\ntab in the chat (what's new, top feedback, onboarding) and a public page at\n\u003Ccode>\u002Fhub\u002F&lt;pk&gt;\u003C\u002Fcode>. All of it is in \u003Ca href=\"\u002Fdocs\u002Fhub\">Public hub and the one-tag loader\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch3 id=\"your-own-ui\">Your own UI\u003Ca class=\"heading-anchor\" href=\"#your-own-ui\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Everything the widget does is on the API: \u003Ccode>GET \u002F_docs\u002Fscalar\u003C\u002Fcode>. Send\n\u003Ccode>Authorization: Bearer &lt;identity token&gt;\u003C\u002Fcode> and use:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>GET \u002Fv1\u002Fconversations\u003C\u002Fcode>, \u003Ccode>POST \u002Fv1\u002Fconversations\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>GET \u002Fv1\u002Fconversations\u002F:id\u002Fmessages?before=\u003C\u002Fcode>, \u003Ccode>POST \u002Fv1\u002Fconversations\u002F:id\u002Fmessages\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>POST \u002Fv1\u002Fconversations\u002F:id\u002Fread\u003C\u002Fcode>, \u003Ccode>DELETE \u002Fv1\u002Fmessages\u002F:id\u003C\u002Fcode>, \u003Ccode>GET \u002Fv1\u002Funread\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>POST \u002Fv1\u002Fattachments\u003C\u002Fcode> (multipart \u003Ccode>file\u003C\u002Fcode>)\u003C\u002Fli>\n\u003Cli>\u003Ccode>GET \u002Fv1\u002Fevents\u003C\u002Fcode> — server-sent events; use \u003Ccode>fetch\u003C\u002Fcode> + a stream reader so the token stays in a header\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Retries are safe: pass a \u003Ccode>clientId\u003C\u002Fcode> with each message and a repeated POST\nreturns the message that already landed.\u003C\u002Fp>\n\u003Ch2 id=\"email-only-users-no-backend\">Email-only users (no backend)\u003Ca class=\"heading-anchor\" href=\"#email-only-users-no-backend\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>An app with no server of its own (a native app, a static site) cannot sign\nidentity tokens, and must never hold the project secret. Turn on \u003Cstrong>Allow\nemail-only users\u003C\u002Fstrong> in Project &gt; Settings and the app can use chat and feedback\nwith just an email address:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>POST \u002Fv1\u002Fanonymous\u002Fsession   { \u003Cspan class=\"shj-syn-str\">\"pk\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"email\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"ann@example.com\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"name\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"Ann\"\u003C\u002Fspan> }\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cul>\n\u003Cli>A \u003Cstrong>new address\u003C\u002Fstrong> gets \u003Ccode>200 { status: &quot;token&quot;, token: &quot;eu_…&quot;, externalId, verified: false }\u003C\u002Fcode>.\nSend \u003Ccode>Authorization: Bearer eu_…\u003C\u002Fcode> on every end-user route (conversations,\nmessages, attachments, \u003Ccode>\u002Fv1\u002Fevents\u003C\u002Fcode>, feedback, changelog comments). Keep the\ntoken (the Keychain on iOS); the person does not enter their email again.\u003C\u002Fli>\n\u003Cli>An address \u003Cstrong>already known\u003C\u002Fstrong> (someone used it before, or it is a contact) never\ngets a token from this call. It gets \u003Ccode>202 { status: &quot;code_sent&quot; }\u003C\u002Fcode> and a\n6-digit code by email, valid 10 minutes. Trade it with\n\u003Ccode>POST \u002Fv1\u002Fanonymous\u002Fverify\u002Fconfirm { pk, email, code }\u003C\u002Fcode> for a verified token;\n\u003Ccode>POST \u002Fv1\u002Fanonymous\u002Fverify { pk, email }\u003C\u002Fcode> sends a new code. Five wrong guesses\nlock the code. Confirming ends every earlier session of that address.\u003C\u002Fli>\n\u003Cli>After \u003Cstrong>5 writes\u003C\u002Fstrong> (messages, posts, comments, votes, ...) an unverified person\ngets \u003Ccode>403\u003C\u002Fcode> with \u003Ccode>data.code = &quot;email_verification_required&quot;\u003C\u002Fcode> on the next\nwrite. Show a code field and call the two verify routes above. Verified people\nare never asked again.\u003C\u002Fli>\n\u003Cli>Until verified, the address is a claim only: the session has no \u003Ccode>email\u003C\u002Fcode>, so it\ncannot trigger reply mail, flag allow-lists or changelog subscriptions, and\nunverified people cannot upload images.\u003C\u002Fli>\n\u003Cli>\u003Ccode>DELETE \u002Fv1\u002Fanonymous\u002Fme\u003C\u002Fcode> with the token erases everything the person left.\u003C\u002Fli>\n\u003Cli>Turning the setting off ends all email-only access at once.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>An email-only user is an ordinary contact whose \u003Ccode>externalId\u003C\u002Fcode> starts with\n\u003Ccode>anon_\u003C\u002Fcode>, so conversations, feedback and the CRM work as for any other user.\u003C\u002Fp>\n\u003Ch2 id=\"guest-access-no-account-no-email\">Guest access (no account, no email)\u003Ca class=\"heading-anchor\" href=\"#guest-access-no-account-no-email\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Email-only users still hand over an address. For a visitor who gives nothing, an\nadmin can open \u003Cstrong>guest access\u003C\u002Fstrong> per surface: Project → Settings → Chat widget →\n\u003Cem>Guest access\u003C\u002Fem>, or \u003Ccode>PATCH \u002Fv1\u002Fadmin\u002Fprojects\u002F:id { &quot;guestSurfaces&quot;: [&quot;chat&quot;, &quot;feedback&quot;, &quot;comments&quot;] }\u003C\u002Fcode>\n(any of the three; \u003Ccode>[]\u003C\u002Fcode> turns guests off). It is off by default.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Surface\u003C\u002Fth>\n\u003Cth>A guest can\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>chat\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Start conversations and message you (replies show in the chat; there is no address to email)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>feedback\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Post, vote, react and comment on the feedback board\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>comments\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Comment and react on blog posts and changelog entries\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Lead forms and sign-up forms are public already and need no guest.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>POST \u002Fv1\u002Fanonymous\u002Fguest   { \u003Cspan class=\"shj-syn-str\">\"pk\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> }\n→ 200 { \u003Cspan class=\"shj-syn-str\">\"token\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"eu_…\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"externalId\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"guest_…\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"name\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"Guest 7f3a\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"surfaces\"\u003C\u002Fspan>: [\u003Cspan class=\"shj-syn-str\">\"chat\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"feedback\"\u003C\u002Fspan>] }\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Send \u003Ccode>Authorization: Bearer eu_…\u003C\u002Fcode> on the end-user routes of the opened surfaces,\nand keep the token: every call makes a \u003Cstrong>new\u003C\u002Fstrong> guest, so a returning visitor\nshould reuse theirs. A guest is an ordinary contact whose \u003Ccode>externalId\u003C\u002Fcode> starts with\n\u003Ccode>guest_\u003C\u002Fcode> and whose name is \u003Ccode>Guest 7f3a\u003C\u002Fcode>, so conversations, feedback and the CRM\nwork as for anyone. \u003Ccode>DELETE \u002Fv1\u002Fanonymous\u002Fme\u003C\u002Fcode> erases a guest's data.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Built-in protections\u003C\u002Fstrong>, all on by default and not switchable:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cem>Only where the admin opened it.\u003C\u002Fem> A write on any other surface is \u003Ccode>403\u003C\u002Fcode> with\n\u003Ccode>data.code = &quot;guest_not_allowed&quot;\u003C\u002Fcode>. Closing every surface ends all guest sessions\nat once (\u003Ccode>401\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cem>Rate limited.\u003C\u002Fem> Making guests: 10 an hour per IP and 500 an hour per project (\u003Ccode>429\u003C\u002Fcode>).\nWriting: 20 per ten minutes and 100 per day per guest, and 600 an hour across the\nproject, on top of the limits every user has (messages, posts, comments, votes).\u003C\u002Fli>\n\u003Cli>\u003Cem>No files.\u003C\u002Fem> Guests cannot upload images (\u003Ccode>403\u003C\u002Fcode>, \u003Ccode>data.code = &quot;guest_upload_not_allowed&quot;\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cem>No identity to borrow.\u003C\u002Fem> A guest session carries no email, so it cannot trigger\nreply mail, feature-flag allow-lists, changelog subscriptions or the host's\n\u003Ccode>role: &quot;admin&quot;\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cem>Sessions expire.\u003C\u002Fem> 30 days without use ends a guest session; the token is random,\nopaque and stored only as a hash. Expired sessions, and guests who never wrote\nanything, are deleted every six hours.\u003C\u002Fli>\n\u003Cli>\u003Cem>Your own sign-in wins.\u003C\u002Fem> The built-in chat only makes a guest when the page gave it\nno \u003Ccode>getToken\u003C\u002Fcode>; the board when it has no \u003Ccode>onSignIn\u003C\u002Fcode>; blog comments when the page has\nneither.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"web\">Web\u003Ca class=\"heading-anchor\" href=\"#web\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>No code beyond the embed. With guest access open for chat:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fwidget.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">defer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n  window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">||\u003C\u002Fspan> []\n  \u003Cspan class=\"shj-syn-class\">HelpBites\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">push\u003C\u002Fspan>([\u003Cspan class=\"shj-syn-str\">'init'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {}])\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>The feedback board (\u003Ccode>feedback.js\u003C\u002Fcode>) and blog comments (\u003Ccode>blog.js\u003C\u002Fcode>) do the same: with\nno \u003Ccode>onSignIn\u003C\u002Fcode> (and, for blog comments, no \u003Ccode>getToken\u003C\u002Fcode>), a visitor becomes a guest on their first action\n(reading never makes one), and the browser keeps that guest in its own storage. If\nstorage is blocked (a third-party frame in some browsers), the guest lasts until the\npage closes. Colours, position and labels are the embed's usual options and the\nproject's Brand kit; nothing guest-specific needs styling.\u003C\u002Fp>\n\u003Cp>For your own UI, call the route above and use the token like any other (the API\nreference is \u003Ccode>\u002F_docs\u002Fscalar\u003C\u002Fcode>, tag \u003Cem>End user\u003C\u002Fem>).\u003C\u002Fp>\n\u003Ch3 id=\"swift\">Swift\u003Ca class=\"heading-anchor\" href=\"#swift\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>\u002F\u002F GuestChat.swift. MIT. Gets a guest session once, keeps it in the Keychain, and lists the guest's conversations.\nimport SwiftUI\nimport Security\n\nstruct GuestChat: View {\n  let host: URL                  \u002F\u002F https:\u002F\u002Fsupport.example.com\n  let projectKey: String         \u002F\u002F pk_…\n  var accent: Color = .accentColor\n  var cornerRadius: CGFloat = 10\n\n  struct Conversation: Decodable, Identifiable { let id: String; let subject: String }\n\n  @State private var conversations: [Conversation] = []\n  @State private var draft = \u003Cspan class=\"shj-syn-str\">\"\"\u003C\u002Fspan>\n  @State private var message = \u003Cspan class=\"shj-syn-str\">\"\"\u003C\u002Fspan>\n\n  var body: some View {\n    VStack(alignment: .leading, spacing: 12) {\n      ForEach(conversations) { Text($0.subject.isEmpty ? \u003Cspan class=\"shj-syn-str\">\"Conversation\"\u003C\u002Fspan> : $0.subject) }\n      TextField(\u003Cspan class=\"shj-syn-str\">\"How can we help?\"\u003C\u002Fspan>, text: $draft, axis: .vertical)\n        .padding(10).background(.quaternary, in: RoundedRectangle(cornerRadius: cornerRadius))\n      Button(\u003Cspan class=\"shj-syn-str\">\"Send\"\u003C\u002Fspan>) { Task { await send() } }\n        .padding(.horizontal, 14).padding(.vertical, 8)\n        .background(accent, in: RoundedRectangle(cornerRadius: cornerRadius)).foregroundStyle(.white)\n        .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty)\n      if !message.isEmpty { Text(message).font(.footnote).foregroundStyle(.secondary) }\n    }\n    .task { await load() }\n  }\n\n  private func request(_ path: String, body: [String: Any]? = nil) async throws -&gt; Data {\n    var request = URLRequest(url: host.appending(path: \u003Cspan class=\"shj-syn-str\">\"v1\u002F\"\u003C\u002Fspan> + path))\n    request.setValue(\u003Cspan class=\"shj-syn-str\">\"Bearer \"\u003C\u002Fspan> + (try await token()), forHTTPHeaderField: \u003Cspan class=\"shj-syn-str\">\"Authorization\"\u003C\u002Fspan>)\n    if let body {\n      request.httpMethod = \u003Cspan class=\"shj-syn-str\">\"POST\"\u003C\u002Fspan>\n      request.setValue(\u003Cspan class=\"shj-syn-str\">\"application\u002Fjson\"\u003C\u002Fspan>, forHTTPHeaderField: \u003Cspan class=\"shj-syn-str\">\"Content-Type\"\u003C\u002Fspan>)\n      request.httpBody = try JSONSerialization.data(withJSONObject: body)\n    }\n    let (data, response) = try await URLSession.shared.data(for: request)\n    if (response as? HTTPURLResponse)?.statusCode == 401 { Keychain.delete(); throw URLError(.userAuthenticationRequired) }\n    return data\n  }\n\n  private func load() async {\n    struct List: Decodable { let conversations: [Conversation] }\n    if let data = try? await request(\u003Cspan class=\"shj-syn-str\">\"conversations\"\u003C\u002Fspan>), let list = try? JSONDecoder().decode(List.self, from: data) { conversations = list.conversations }\n  }\n\n  private func send() async {\n    do {\n      _ = try await request(\u003Cspan class=\"shj-syn-str\">\"conversations\"\u003C\u002Fspan>, body: [\u003Cspan class=\"shj-syn-str\">\"body\"\u003C\u002Fspan>: draft, \u003Cspan class=\"shj-syn-str\">\"clientId\"\u003C\u002Fspan>: UUID().uuidString])\n      draft = \u003Cspan class=\"shj-syn-str\">\"\"\u003C\u002Fspan>\n      await load()\n    } catch { message = \u003Cspan class=\"shj-syn-str\">\"Could not send. Try again in a moment.\"\u003C\u002Fspan> }\n  }\n\n  \u002F\u002F\u002F The saved guest token, or a new guest (one call, rate limited by the server).\n  private func token() async throws -&gt; String {\n    if let saved = Keychain.read() { return saved }\n    var request = URLRequest(url: host.appending(path: \u003Cspan class=\"shj-syn-str\">\"v1\u002Fanonymous\u002Fguest\"\u003C\u002Fspan>))\n    request.httpMethod = \u003Cspan class=\"shj-syn-str\">\"POST\"\u003C\u002Fspan>\n    request.setValue(\u003Cspan class=\"shj-syn-str\">\"application\u002Fjson\"\u003C\u002Fspan>, forHTTPHeaderField: \u003Cspan class=\"shj-syn-str\">\"Content-Type\"\u003C\u002Fspan>)\n    request.httpBody = try JSONSerialization.data(withJSONObject: [\u003Cspan class=\"shj-syn-str\">\"pk\"\u003C\u002Fspan>: projectKey])\n    let (data, _) = try await URLSession.shared.data(for: request)\n    struct Guest: Decodable { let token: String }\n    let guest = try JSONDecoder().decode(Guest.self, from: data)\n    Keychain.save(guest.token)\n    return guest.token\n  }\n}\n\nprivate enum Keychain {\n  static let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: \u003Cspan class=\"shj-syn-str\">\"helpbites.guest\"\u003C\u002Fspan>]\n  static func read() -&gt; String? {\n    var found: AnyObject?\n    guard SecItemCopyMatching(query.merging([kSecReturnData as String: true]) { $1 } as CFDictionary, &#38;found) == errSecSuccess, let data = found as? Data else { return nil }\n    return String(data: data, encoding: .utf8)\n  }\n  static func save(_ token: String) { delete(); SecItemAdd(query.merging([kSecValueData as String: Data(token.utf8)]) { $1 } as CFDictionary, nil) }\n  static func delete() { SecItemDelete(query as CFDictionary) }\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Colours and corner radius are parameters. Feedback and comments work the same way\nwith \u003Ccode>\u002Fv1\u002Ffeedback\u003C\u002Fcode> and \u003Ccode>\u002Fv1\u002Fblog\u002F…\u002Fcomments\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"feedback-board\">Feedback board\u003Ca class=\"heading-anchor\" href=\"#feedback-board\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The same token also signs users into the project's public feedback board.\nTurn it on under Project → Feedback, then either drop in the board:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"feedback\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Ffeedback.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-target\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"#feedback\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">defer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n  window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBitesFeedback\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBitesFeedback\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">||\u003C\u002Fspan> []\n  \u003Cspan class=\"shj-syn-class\">HelpBitesFeedback\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">push\u003C\u002Fspan>([\u003Cspan class=\"shj-syn-str\">'init'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {\n    getToken\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> () \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">fetch\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'\u002Fapi\u002Fsupport\u002Ftoken'\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>ok \u003Cspan class=\"shj-syn-oper\">?\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">json\u003C\u002Fspan>() \u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">null\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r \u003Cspan class=\"shj-syn-oper\">&#38;&#38;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>token)\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    onSignIn\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> () \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">openLoginModal\u003C\u002Fspan>()\n  }])\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>or build your own on \u003Ccode>\u002Fv1\u002Ffeedback\u003C\u002Fcode> (public reads with \u003Ccode>?pk=\u003C\u002Fcode>). Add\n\u003Ccode>role: &quot;admin&quot;\u003C\u002Fcode> to the token claims of your own staff to let them moderate.\nEverything — options, the API, importing an existing board, account deletion —\nis in \u003Ca href=\"\u002Fdocs\u002Ffeedback\">FEEDBACK.md\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"feature-flags\">Feature flags\u003Ca class=\"heading-anchor\" href=\"#feature-flags\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Switch features of your app from Project → Flags: each flag is \u003Ccode>off\u003C\u002Fcode>, \u003Ccode>on\u003C\u002Fcode>, or\n\u003Ccode>admins\u003C\u002Fcode> (your admins plus a per-flag allow list), with an optional fallback for\nwhen HelpBites can't be reached. Read them with \u003Ccode>GET \u002Fv1\u002Fflags\u003C\u002Fcode> — from your\nserver with the secret, or from a browser with \u003Ccode>?pk=\u003C\u002Fcode> or the identity token.\nDetails in \u003Ca href=\"\u002Fdocs\u002Fflags\">FLAGS.md\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"error-tracking\">Error tracking\u003Ca class=\"heading-anchor\" href=\"#error-tracking\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>If the operator enables Project → Install &amp; API → Error tracking, copy its DSN into any\nSentry-compatible SDK and call \u003Ccode>Sentry.setUser({ id: user.id })\u003C\u002Fcode>, where \u003Ccode>user.id\u003C\u002Fcode>\nis the same stable value used as this document's token \u003Ccode>sub\u003C\u002Fcode>. That associates a\ncustomer's recent errors with their HelpBites conversations. See \u003Ca href=\"\u002Fdocs\u002Ferrors\">ERRORS.md\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"typed-tickets-and-host-actions\">Typed tickets and host actions\u003Ca class=\"heading-anchor\" href=\"#typed-tickets-and-host-actions\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>support\u003C\u002Fcode> is available on every project. Create additional types under\n\u003Ccode>\u002Fv1\u002Fadmin\u002Fprojects\u002F:id\u002Ftypes\u003C\u002Fcode> to define their statuses, intake fields, and\noperator actions. New conversations accept \u003Ccode>type\u003C\u002Fcode>, \u003Ccode>fields\u003C\u002Fcode>, and \u003Ccode>subjectRef\u003C\u002Fcode>\n(\u003Ccode>{ type, id, label, url?, thumbnailUrl? }\u003C\u002Fcode>); URL values are restricted to\nhttp(s).\u003C\u002Fp>\n\u003Cp>For a host backend to file a ticket for one of its users, call \u003Ccode>POST \u002Fv1\u002Ftickets\u003C\u002Fcode>\nwith the project secret—not an identity token:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">fetch\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'https:\u002F\u002Fsupport.example.com\u002Fv1\u002Ftickets'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {\n  method\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'POST'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n  headers\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> { authorization\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">`Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>process\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>env\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">HELP_BITES_SECRET\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> 'content-type'\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'application\u002Fjson'\u003C\u002Fspan> }\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n  body\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">stringify\u003C\u002Fspan>({\n    externalId\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> name\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>name\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> email\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>email\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    type\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'report'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> subject\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Report an icon'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> body\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> details\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    fields\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> { reason\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'copyright'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> sourceUrl }\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    subjectRef\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> { type\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'icon'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> id\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> icon\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> label\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> icon\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>name\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> url\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> icon\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>url }\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    idempotencyKey\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">`report:\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>user\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">:\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>icon\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\n  })\n})\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Set \u003Ccode>actionWebhookUrl\u003C\u002Fcode> and \u003Ccode>actionWebhookSecret\u003C\u002Fcode> on the project. Operator\nactions POST JSON to that URL with \u003Ccode>X-HelpBites-Signature: sha256=&lt;HMAC-SHA256(body)&gt;\u003C\u002Fcode>; verify it before applying the host-side effect.\nThe payload includes \u003Ccode>actionKey\u003C\u002Fcode>, the ticket snapshot, and an idempotency key.\u003C\u002Fp>\n\u003Ch2 id=\"work-with-an-ai-assistant\">Work with an AI assistant\u003Ca class=\"heading-anchor\" href=\"#work-with-an-ai-assistant\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The operator side has an \u003Ca href=\"\u002Fdocs\u002Fmcp\">MCP server\u003C\u002Fa> too: connect Claude, ChatGPT or\nCursor to \u003Ccode>https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fmcp\u003C\u002Fcode> and it can read the inbox, reply,\ntriage feedback and write changelog entries as you.\u003C\u002Fp>\n",[6,10,13,16,20,23,26,29,32,35,38,41,44,47,50],{"id":7,"text":8,"level":9},"1-create-a-project","1. Create a project",2,{"id":11,"text":12,"level":9},"2-mint-identity-tokens-on-your-server","2. Mint identity tokens on your server",{"id":14,"text":15,"level":9},"3-show-the-chat","3. Show the chat",{"id":17,"text":18,"level":19},"drop-in-widget","Drop-in widget",3,{"id":21,"text":22,"level":19},"one-tag-for-everything","One tag for everything",{"id":24,"text":25,"level":19},"your-own-ui","Your own UI",{"id":27,"text":28,"level":9},"email-only-users-no-backend","Email-only users (no backend)",{"id":30,"text":31,"level":9},"guest-access-no-account-no-email","Guest access (no account, no email)",{"id":33,"text":34,"level":19},"web","Web",{"id":36,"text":37,"level":19},"swift","Swift",{"id":39,"text":40,"level":9},"feedback-board","Feedback board",{"id":42,"text":43,"level":9},"feature-flags","Feature flags",{"id":45,"text":46,"level":9},"error-tracking","Error tracking",{"id":48,"text":49,"level":9},"typed-tickets-and-host-actions","Typed tickets and host actions",{"id":51,"text":52,"level":9},"work-with-an-ai-assistant","Work with an AI assistant",1792677543075]