# Milo Source: https://feddy.app/docs/concepts/assistant Milo, the AI that answers users from your answers and your docs, and the Copilot that drafts replies for you in the inbox. Milo is the AI in Feddy. It does two jobs. It answers users on its own, within limits you set, and it helps you write replies in the inbox. It never makes up facts about your product: everything it says to a user comes from your answers, which you write or it learns from your team's replies, or from the docs and text you added as sources. You'll use it to: * Answer the questions you get over and over, without touching them. * Let users know they've been heard while they wait for you. * Draft, shorten, translate, or soften a reply before you send it. Its settings live under **Settings → Milo AI**. ## Answers An answer is the reply you'd give to a common question, saved once so it can be reused. Each one has a title for your own reference, and optionally keywords, a topic, and a platform that narrow when it applies. Write them under **Settings → Milo AI → Answers**, or import a batch from the same page. ## Sources A source is material Milo reads to answer questions your answers don't cover. Add one under **Settings → Milo AI → Sources**, where each kind has its own add button: * **Website**: the address of your docs or help center. Milo reads that page and the pages under it on the same site, and reads them again every week. Click **Refresh** on its row to read them again now. * **App Store**: the link to your app on the App Store. Milo reads the description, the release notes of the latest version, and which devices, system versions, and languages the app supports. Prices are left out, and lines in the description that state a price are skipped. The listing is read from the country in the link and again every week, or now with **Refresh**. * **Text**: anything you paste, or a `.md` or `.txt` file you drop in. Only the text is kept, not the file. Milo writes replies from your answers and sources in its own words and never sends a passage as is. When a source and one of your answers disagree, the answer wins. If nothing covers a question, Milo hands the conversation to you. Each source has a **Used by** setting: * **Milo and team**: Milo can use it in replies to users. Anything in it may end up in a reply, so add only what any user may read. * **Team only**: only the Copilot reads it, for drafts you review before sending. Use this for internal notes. Open a source to see every passage Milo reads from it. If a site draws its pages with JavaScript, there's no text to read and the source shows why. Add the text instead. ## About your app **Settings → Milo AI → Settings** has an **About your app** box: a short description Milo reads before every reply. Fill it in. It's how Milo knows what your product is and what it isn't. For a first draft, click **Draft from a link** and paste your app's App Store link or your website's address. Milo reads that one page and writes a short paragraph in English, without prices, into the box. Nothing is saved until you click **Save**, so edit it first: add what the page leaves out, and remove anything you don't want Milo to say. **Also read this page for answers**, ticked by default, adds the same link as a [source](#sources) too, so Milo can answer from it. If it's already a source, nothing changes. ## Modes Choose how Milo replies to users under **Settings → Milo AI → Settings**: | Mode | What users get | | - | - | | Keyword answers only | An answer whose keyword appears in their message. Anything else gets the fallback message. | | Answer with AI | A reply written from your answers and sources, using what's known about their device and app version and any screenshot they attached. A message too short to answer gets one question back. Follow-ups get replies too, up to three per conversation. If part of a message isn't covered, Milo answers the rest and leaves that part to you. | | Suggest replies to me | Nothing automatic beyond keyword answers. Instead, Milo drafts a reply for you in the inbox. | New projects on a paid plan start in **Answer with AI**; on the Free plan, in **Keyword answers only**. Turn **Enabled** off to send nothing automatically. Whatever the mode, Milo never changes a conversation's status and never marks it resolved. Only you, or the user, can close a conversation. Users see replies under the name you give Milo in **Name**. New projects start with "Milo". ## Style **Style** sets how replies sound: **Tone** (friendly, neutral, or playful), **Length** (short or detailed), **Formality** (casual or formal), and **Emoji** (never or sometimes). Each starts at **Auto**, which follows the usual way support writes in the user's language. Changes apply to the next reply. ## Rules **Rules** decide what Milo does in three common cases. Each is a switch: | Rule | On | Off | | - | - | - | | Complaints: acknowledge only | A complaint about price or content gets one sentence that names it, and nothing more. This is the default. | Milo also says what your answers and sources say about it, such as what's free, without prices. | | Bugs: ask one question first | When your answers don't explain the problem, Milo asks one question, such as what they tapped. This is the default. | The conversation goes straight to you. | | Refunds and account problems: always hand off | Refunds, disputed charges, and account problems always go to you. | Milo explains how refunds work from your answers. This is the default. | For anything else, use **Other instructions**. ## Other instructions Free text Milo follows on every reply: what to call your product, things it should never say, anything **Style** doesn't cover. Where they differ from **Style**, these win. Keep facts in your answers and use instructions for behavior. ## Handing over to you Milo steps back and leaves the conversation to you when the user asks for a person, when nothing in your answers or sources covers the question, after answering only part of a message, or after its third reply. If it hasn't replied yet, the user gets the fallback message. If it has, it tells them in its own words that you'll take it from here. After that it stays quiet in the conversation, even if a later message is one it could answer. When it hands over, Milo leaves you an internal note in the conversation: what the user wants, what's already known (their device, version, plan, what their screenshots show), what Milo told them, and what's still missing. The user never sees it. The note is written in the language your team replies in. ## Fallback message The fallback message goes out when a new conversation gets no answer, so the user knows they've been heard and roughly when to expect you. The default is a thank-you with a 24 to 48 hour expectation; change it to match your real response time. Leave it empty to send nothing. ## What Milo did Under each of Milo's replies in a conversation, a one-line card says what it did and why: which answer it used, or what was missing that made it ask a question or hand the conversation to you. Open the card to see the answers and pages it drew on (**Based on** links the page), how it read the message, and whether the user found it helpful. The **Milo AI** page lists the past week or month: how many times Milo replied, what users said about the replies, the conversations it handed to you grouped by reason with an example of each, any time it could not reach the AI service, what it learned from your replies, and new [requests](/docs/concepts/requests) from users. Every Monday, everyone on the project also gets the same list by email. Turn it off under **Account → Notifications → Weekly Milo summary**. If the AI service doesn't respond, the user gets your fallback message, the conversation is left to you with a note saying why, and the week's list counts it. ## Was this helpful? After one of Milo's replies, the user sees a **Was this helpful?** prompt. Yes closes the conversation as resolved. No leaves it open and flags the reply in your inbox so you can step in. ## Learn from your replies When this is on, which it is by default, Milo reads what your team replies to users and keeps what the next user asking the same thing should hear. It saves it to your answers and uses it right away. There is nothing to review. * It learns only what holds for everyone. A refund you granted or "it works now" stays with that one user. * It keeps your steps and conditions as you wrote them, and doesn't turn them into a yes or a no. * When your team's answer changes, it updates the answer it already has instead of adding a second one. Your newest reply wins, also over an answer you wrote by hand, unless you edited that answer after the reply. * Something temporary, like an outage or a workaround until your next release, stops being used after two weeks, or from the app version you said fixes it. Learned answers are marked **Learned** in your answers list. Each answer page has a history of every change, and any earlier version can be restored. In a conversation, Milo's card shows which answers a reply used; next to one it learned, **Undo** takes back its latest change. Editing a learned answer makes it yours. Questions Milo couldn't answer, and hasn't learned an answer for since, are listed under **Unanswered**, each with a button to write the answer. ## Copilot Next to every conversation, the **Copilot** tab has already read the thread, its screenshots, your notes, your answers, and your sources, including the ones for your team only. Ask it to draft a reply, summarize, translate into the user's language, or soften the tone. What it writes goes into your reply box, where you edit it and send it yourself. Nothing reaches the user until you press send. Copilot answers up to 30 times per conversation, counting everyone on your team. ## Try it **Settings → Milo AI → Settings** has a phone on the right where you can talk to Milo the way a user of your app would: several messages in a row, in any language, with a screenshot if you like. It uses your settings as saved, so a change on the left applies to your next message. Under each reply, a line says what Milo did; click it to see the answers and pages it used and its note for you. Once Milo hands the conversation over, click **Clear** to start again. Nothing is sent to anyone, and it doesn't count toward your plan's AI replies. ## Availability AI answers, sources, and the Copilot are available on paid plans. Pro includes 5 sources and 500 pages per project, Business 20 sources and 3,000 pages; pasted text and an App Store listing each count as one page. Keyword answers and the fallback message work on every plan. ## Related guides > [card] [Conversations](/docs/concepts/conversations) > > What "Was this helpful?" does to a conversation's status. > [card] [Projects](/docs/concepts/projects) > > Where Milo's settings live inside a project. > [card] [Team](/docs/concepts/team) > > Saved replies for the answers you'd rather send by hand. > [card] [Troubleshooting](/docs/troubleshooting) > > What to do when Milo answered something it shouldn't have. # Contacts Source: https://feddy.app/docs/concepts/contacts Who's writing in, from an anonymous visitor to a user you've identified, and what Feddy knows about them. A contact is one person, as far as Feddy can tell. Every conversation belongs to exactly one contact, and everything Feddy knows about that person, from the device they're on to the plan they're paying for, is gathered in one place. Contacts let you: * See who's on the other end of a conversation before you read it. * Answer without asking "which plan are you on?" or "what version are you running?" * Follow one user's history across every device they've used. * Reach users by email when they've left the app. ## Everyone starts anonymous The first time the SDK runs on a device, it creates an anonymous ID and stores it on that device. That ID is the contact. On iOS it survives reinstalls. In a browser it lasts until the visitor clears the site's data, which is why the widget can ask for an email after a first message: it's the only way to reach a visitor once that ID is gone. Anonymous contacts show as **Anonymous** in the inbox, alongside the device details the SDK collected. ## Identified users When someone logs in to your app, call `identify` with your own user ID for them. From then on, the inbox shows their name or email instead of **Anonymous**, and conversations from every device they've used are merged into one history. [Identify users](/docs/guides/identify-users) covers how to call it and what happens when two records merge. ## What you know about a contact The **Details** panel next to a conversation shows two kinds of facts: * **Collected by the SDK**: device, operating system, app version, language, time zone, and when they were first and last seen. You don't send any of this. * **Sent by you with `identify`**: whatever you chose to include, such as their plan or membership status. Feddy calls these attributes. **Settings → Data attributes** lists every attribute you've ever sent. Feddy works out whether each one is text, a number, or a date. From there you can give an attribute a friendlier label, show it as a column in the inbox, or make it something you can filter by. A new attribute appears in the list on its own the first time the SDK sends it. ## Email A contact's email comes from `identify`, or from the user typing it into the widget. Feddy uses it for one thing: emailing them your replies, and only when your project has a verified email domain. See [Team](/docs/concepts/team) for how to set one up. ## The Contacts page **Contacts** in the dashboard lists everyone who has written in, with their conversations and when they were last seen. Filter to anonymous contacts to see how many people you could be identifying. ## Related guides > [card] [Identify users](/docs/guides/identify-users) > > Turn anonymous visitors into named users with a plan and a history. > [card] [Conversations](/docs/concepts/conversations) > > What each conversation carries and what happens to it. > [card] [Team](/docs/concepts/team) > > Verify an email domain so replies can reach contacts by email. > [card] [Troubleshooting](/docs/troubleshooting) > > What to check when attributes are missing from the inbox. # Conversations Source: https://feddy.app/docs/concepts/conversations What a thread between you and a user contains, what its status means, and what happens on its own. A conversation is one thread between you and one user. It starts the moment a user sends a message and holds everything that follows: their messages, your replies, notes your team leaves for each other, and every change of status, in the order they happened. Each conversation comes with context you didn't have to ask for: the topic the user picked, what device and app version they're on, and anything you've told Feddy about them. You'll find all of it in the panel next to the thread. From the inbox you can: * Reply to the user, or leave a note only your team can see. * Change the status, or let Feddy change it for you as the conversation moves along. * See the user's device, app version, and details, without asking them. * Insert a saved reply by typing `/`. * Read and reply in your own language when a user writes in another. * Mark a conversation unread so a teammate takes a look. ## Status A conversation is always in one of three states. | Status | What it means | | - | - | | Open | No one on your team has replied yet. New conversations start here, and a user replying to something you sent brings it back here. Milo may already have answered it. | | Pending | Someone on your team replied. Sending a reply moves the conversation here on its own. | | Closed | Done. Closed conversations never show as unread. | Milo's replies don't change the status, so an open conversation isn't always waiting on you. **Needs reply**, at the top of the inbox, lists the ones that are: the user wrote last, or Milo handed the conversation to your team. The number beside it counts them. You can set any status yourself from the conversation header. Two things also happen without you: * A conversation that sits in **Pending** for 14 days closes on its own. The user never answered, so there's nothing left to do. * When a user answers "Was this helpful?" with yes after one of Milo's replies, the conversation closes as resolved. If a user writes again within 7 days of a close, the same conversation reopens. After 7 days their message starts a fresh conversation, with a link back to the old one so you still have the history. ## Topics When a user starts a conversation, they pick what it's about. This sorts your inbox before you've read a word. Five topics are built in: **Bug**, **Feature**, **Question**, **Billing**, and **Other**. Users see them in their own language. You can turn any of them off in **Settings → Topics**, but you can't rename or delete them. Each topic has a color, shown as a dot next to its name in your inbox so you can tell them apart at a glance. The built-in topics start with suggested colors. Click a dot in **Settings → Topics** to pick another one, or any color of your own. Users never see topic colors. Add your own for anything specific to your product, such as **Sync** or **Account**. A custom topic has a name, translations for other languages, and a short code the SDK uses to refer to it. The code can't be changed once the topic exists, so pick it with care. On paid plans, Milo reads each new conversation and moves it to the right topic when the user picked the wrong one. The details panel then shows which topic the user picked. A topic you set by hand stays as it is. See [Requests](/docs/concepts/requests#topics). ## What comes with each conversation The SDK sends along what it can see about the user's setup, and the inbox shows it beside the thread: * **iOS**: device model, iOS version, app version and build, the device language, and the time zone. * **Web**: the page they were on, browser, operating system, window size, where they came from, the browser language, and the time zone. From the time zone, the inbox shows what time it is for the user right now, so you know when it's the middle of the night for them. It needs iOS SDK 0.8.2 or later. If you've told Feddy who the user is, their name and details appear here too. See [Contacts](/docs/concepts/contacts). ## Replies and notes The box at the bottom of a conversation has two modes: * **Reply** goes to the user. Sending one moves the conversation to **Pending**. * **Note** stays inside your inbox. Use it to leave context for a teammate. The user never sees notes and nothing about the conversation changes. > [!TIP] > > Type `/` in the reply box to insert a saved reply. Set them up under **Settings → Saved replies**. ## Translation When a user writes in a language you don't, you don't have to switch. Hover over a message and click **Translate into English** (or your language) on its corner. It's marked **Translated from Spanish** (or whichever language it was), and **Show original** brings back what they actually wrote. To read the whole conversation in your language, click the translate icon in the header. It turns green while the conversation is translated; click it again to switch back. Messages that arrive while it's on are translated too. This works for the user's messages, Milo's replies, and replies from teammates who write in another language. Write your reply in your own language and press **Send in Spanish**. The user receives it in Spanish, in the app and by email, with nothing marking it as translated. In the inbox you keep seeing what you wrote. **Show what they got** shows the translation they received. If the translation fails, nothing is sent and your reply stays in the box. Press **Send reply** to send it untranslated. Replies go out in the language of the user's latest message. If they switch languages mid-conversation, your next reply switches too. Conversations already in your language look exactly as before. Your language comes from your browser. To change it, go to **Account → Profile → Language**. Turn on **Translate automatically** there to open every conversation already translated; you can still switch any one back to the original. Teammates each read in their own language. Notes are never translated. Milo's weekly email uses the same language, or English until you pick one. Translation is available on paid plans and doesn't count toward your plan's AI replies. ## Attachments Users can attach up to 5 images to a message: PNG, JPEG, or WebP, up to 10 MB each. The SDK shrinks large images before uploading, so a photo from a phone camera is fine. Other kinds of files aren't accepted. On the web, users can also attach a screenshot of the page they're on with one click, so you see exactly what they see. Attachments are available on paid plans. ## Unread A conversation is unread when nobody on your team has seen its latest message yet. Opening it marks it read for everyone. If you want it to stand out again, use **Mark unread** in the header. Closed conversations never count as unread. ## Related guides > [card] [Contacts](/docs/concepts/contacts) > > Who's on the other end of a conversation, and what you know about them. > [card] [Milo](/docs/concepts/assistant) > > Answer repeat questions automatically, from your answers. > [card] [Team](/docs/concepts/team) > > Get notified about new messages and send replies by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show users a count when a reply is waiting for them. # Projects Source: https://feddy.app/docs/concepts/projects What a project holds, how to organize apps and sites into projects, and why the project ID is safe to share. A project is the home for one app or website in Feddy. Everything that comes from that app lives inside it: the conversations, the people who wrote in, the topics users pick from, the answers Milo can give, and the settings that shape how Feddy looks and behaves. Each project has its own inbox. You'll use projects to: * Keep each product's conversations in their own inbox. * Decide who on your team can see which inbox. * Give each product its own branding, topics, and automatic answers. ## One project per product The usual setup is one project for each app or site you support. If you have an iOS app and a website for the same product, they can share a project as long as the same people answer both. Prefer two projects when you want separate inboxes. ## The project ID Every project has an ID that starts with `fd_` followed by 16 characters. Copy it from **Settings → Install**. The SDK sends it with every message so Feddy knows which project, and which inbox, the message belongs to. **The project ID isn't a secret.** It's built into your app and visible in your page source, so anyone can find it, and that's fine: it only identifies the project. Commit it, hard-code it, put it in a public repository. There's no way to rotate it and no need to. If someone tries to flood your inbox, Feddy limits how many requests one device can send. ## What's inside a project Everything below lives under **Settings** in the dashboard: * **Install**: the project ID, and copy-paste snippets for iOS, the web, and coding agents. * **General**: the project's name and the short name used in its dashboard links, plus what users see: your brand name, accent color, and the response-time promise shown before and after they send a message. Pick the color from a short set or choose your own, and the response time from a list that already reads in every language users see. A preview beside the settings shows the result as you change it; your app picks it up once you save. * **Topics**: the choices users pick from when they start a conversation. * **Data attributes**: the facts you send about users, such as their plan, and how they appear in the inbox. * **Milo AI**: automatic replies, and the answers they're allowed to draw on. * **Saved replies**: snippets you can drop into the reply box by typing `/`. * **Email domain**: the domain your replies are emailed from. * **Members**: who can see this project's inbox. * **Danger zone**: archive the project, or delete it and every conversation in it. ## Development and production You don't need a separate project for testing. The SDK has an `apiUrl` option for pointing a development build at a local server, and the project ID stays the same either way. Create a second project only if you'd rather keep test conversations out of your real inbox. ## Related guides > [card] [Create a project](/docs/getting-started/create-a-project) > > Sign up, create your first project, and copy its ID. > [card] [Conversations](/docs/concepts/conversations) > > What arrives in a project's inbox and what happens to it. > [card] [Milo](/docs/concepts/assistant) > > Set up automatic answers for the questions you get most. > [card] [Team](/docs/concepts/team) > > Invite teammates to a project and connect an email domain. # Requests Source: https://feddy.app/docs/concepts/requests What your users ask for, grouped by Milo, with how many asked and who to tell when it ships. Users ask for the same things in different words, spread over weeks of conversations. **Requests** puts them in one list: each feature they want, each complaint, each bug they run into, with the number of people behind it. ## How the list fills Milo reads every new conversation once, a couple of minutes after it starts, and decides whether the user is asking for something other users could want too: * **Feature**: add or change something, like a setting, a kind of content, or support for a device. * **Complaint**: unhappy with the price, how much is free, or how much content there is. * **Bug**: something in your app doesn't work or looks wrong. If one of your requests already covers it, the conversation is added there. Otherwise Milo starts a new one with a short name. Questions about how to do something, cancelling a subscription, refunds, and one person's account problems are not requests. When Requests first turns on, Milo also reads your conversations from the last 90 days, so the list isn't empty on day one. ## The list **Requests** in the sidebar shows them with the most people first. For each one you see its kind, how many people asked, how many of them pay, and when someone last asked. The paying count comes from what your app reports with each message, such as `is_pro` or `plan` (see [Identify users](/docs/guides/identify-users)). If your app reports nothing like that, the column doesn't appear. Open a request to see everyone who asked, each with the sentence they wrote and a link to the conversation. From there you can: * **Rename** it or change its kind. * **Merge** it into another request, when Milo filed the same thing twice. * Remove one conversation from it with **Not this request**. * **Delete** it, when it isn't something users want. The conversations stay in your inbox, and Milo doesn't file them again. In a conversation, the details panel shows the request it belongs to. ## When it ships Click **Mark as shipped** on the request. A box appears for one message to everyone who asked. Each of them gets it as a new conversation in your app, plus an email if they have an address and your project has a verified sending domain. People who use your app in another language get it in theirs. Everyone is told once. If more people ask after you sent it, the box comes back for them. ## Topics While it reads a conversation, Milo also checks the topic the user picked. Users often pick **Bug** for a question about their subscription. If the conversation belongs under another of your topics, Milo moves it there, and the details panel shows **User picked Bug** under the topic. A topic you changed by hand is never touched. ## Weekly summary The Monday email lists new requests and the older ones people asked for again that week, with links. Projects where Milo only sends keyword answers get the email too when there are requests to report. ## Availability Requests and topic sorting are available on paid plans. Milo's reading of your conversations doesn't count toward your plan's AI replies. ## Related guides > [card] [Conversations](/docs/concepts/conversations) > > Topics, statuses, and what comes with each conversation. > [card] [Milo](/docs/concepts/assistant) > > How Milo answers users and learns from your replies. # Team Source: https://feddy.app/docs/concepts/team Inviting teammates, how you find out about new messages, and how replies reach users by email. Feddy is built for a small team answering together: one inbox, everyone sees the same conversations, and a reply from any of you reads as a reply from the product. This page covers who can see a project, how you're told about new messages, and how your replies travel. ## Members A project's **Members** page lists who can see its inbox. There are two roles: * **Owner**: every project, plus members and billing. * **Member**: this project's inbox only. Invite people by email. An invitation is good for 7 days. Owners can disable a member without removing them, which keeps their history in place, and restore them later. ## Notifications Everyone chooses their own under **Account → Notifications**. * **Email**: a message for each new conversation and reply, either right away or bundled every 30 minutes, 3 hours, or 12 hours. * **Dashboard**: a sound when a message arrives, the unread count in the browser tab's title, and desktop notifications while you're away from the tab. Desktop notifications are a per-browser setting, so switch them on in each browser you use. ## Replies by email Replies always appear inside your app or site. They also go out by email when two things are true: Feddy has an email for the contact, and your project has a verified email domain. To set one up, go to **Settings → Email domain**, add a subdomain of your own such as `support.yourapp.com`, and publish the DNS records shown there. Once the domain is verified, replies go out from your own address, with the full reply in the body so the user can read it without opening the app. Until then, replies are shown only in the app, and the SDK doesn't ask users for an email, since there'd be nothing to send. > [!NOTE] > > Feddy never emails your users from a Feddy address. Mail to you and your team, such as notifications and invitations, comes from Feddy's own domain; mail to your users only ever comes from yours. ## Saved replies **Settings → Saved replies** holds the answers you send often. Give one a shortcut, then type `/shortcut` in the reply box to drop it in. ## Related guides > [card] [Identify users](/docs/guides/identify-users) > > Pass a user's email so replies can reach them once your domain is verified. > [card] [Contacts](/docs/concepts/contacts) > > Where a contact's email comes from and how it's used. > [card] [Conversations](/docs/concepts/conversations) > > Replies, notes, and what counts as unread for your team. > [card] [Milo](/docs/concepts/assistant) > > Let Milo handle repeat questions so your team sees fewer of them. # Set up with an AI agent Source: https://feddy.app/docs/getting-started/ai-agent Hand the install to Claude Code, Cursor, or any coding agent that can read these docs. Every install in these docs comes down to a few lines of code in a known place. If you work with a coding agent, it can do those steps for you. All it needs is your project ID and a way to read these docs. ## Copy the prompt from your dashboard Every project has a prompt written for coding agents under **Settings → Install**. It already contains your project ID and the steps for your platform, so you can paste it into the agent as is. When the agent is done, read through what it changed before you ship. The quickstart for your platform shows what a correct install looks like, and the two should match. ## How agents can read these docs * **Context7.** These docs are indexed from the public [FeddyLab/feddy-docs](https://github.com/FeddyLab/feddy-docs) repository. If your agent has the Context7 tool, it can look up `feddy` directly. * **llms.txt.** [feddy.app/docs/llms.txt](https://feddy.app/docs/llms.txt) lists every page with a one-line summary, and [feddy.app/docs/llms-full.txt](https://feddy.app/docs/llms-full.txt) is the full text of every page in one file. Paste either link into the agent's context. * **The SDK source.** The iOS package at [FeddyLab/feddy-ios](https://github.com/FeddyLab/feddy-ios) is public, so an agent can read the code behind every call these docs mention. ## What to check afterwards Three things tell you the install is right: * Feddy is set up once, with your project ID, when the app or page starts and before anything else can open it. * The Support button is somewhere users already look for help, usually Settings. * A message sent from a debug build shows up in your inbox, and your reply from the inbox shows up in the app. > [!TIP] > > If all three hold, you're done. Everything else, from identifying users to the unread badge, is optional. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show a count on your Support button so users notice when you've answered. # Create a project Source: https://feddy.app/docs/getting-started/create-a-project Sign up, create a project for your app or site, and copy its project ID. Everything in Feddy is organized around projects. A project is the home for one app or website: its conversations, the people who wrote in, its settings, and the inbox your team answers from. You need one before you install anything, and creating it takes about a minute. **1. Sign up** Create an account at [dash.feddy.app](https://dash.feddy.app/signup). The free plan doesn't expire and covers everything in these docs. **2. Create a project** Name it after your app or site. One project per product is the usual setup. If you have an iOS app and a website for the same product, they can share a project as long as the same people answer both. Prefer two projects when you want separate inboxes. **3. Copy the project ID** Open **Settings → Install**. The project ID starts with `fd_` followed by 16 characters. It's the only thing you need from the dashboard to get set up, and every quickstart asks for it. > [!TIP] > > You have a project and its ID. Pick your platform on the [introduction](/docs/introduction) and follow its quickstart. > [!NOTE] > > **The project ID isn't a secret.** It's built into your app or visible in your page source, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it, hard-code it, put it in a public repository. There's no way to rotate it and no need to. If someone tries to flood your inbox, Feddy limits how many requests one device can send. ## One project for development and production You don't need a separate project for testing. The SDK has an `apiUrl` option for pointing a development build at a local server, and the project ID stays the same either way. Create a second project only if you'd rather keep test conversations out of your real inbox. ## Next steps > [card] [Add Feddy to your product](/docs/introduction) > > Pick your platform and follow its five-minute quickstart. > [card] [Projects](/docs/concepts/projects) > > Everything a project holds, and the settings you'll find inside it. # Identify users Source: https://feddy.app/docs/guides/identify-users Tell Feddy who's logged in, so you see their name in the inbox, their history follows them across devices, and replies can reach them by email. By the end of this page, conversations from a logged-in user will show their name and details in your inbox, and their history will follow them onto every device they sign in on. You don't have to do this. Feddy works from the first install: until you identify someone, each device is treated as its own anonymous visitor, with an ID Feddy stores on the device. Identifying users is what turns "an iPhone running 2.3.1" into "Ada, on the Pro plan". ## When to call it Call `identify` right after the user logs in, and again whenever something you send about them changes. It's safe to call as often as you like, even on every launch; each call updates what's stored. ```swift theme={null} Feddy.identify( userId: user.id, email: user.email, attributes: ["plan": user.plan, "is_member": user.isMember, "renews_at": user.renewsAt] ) ``` ```js theme={null} Feddy.identify({ userId: user.id, email: user.email, attributes: { plan: user.plan, is_member: user.isMember }, }) ``` `userId` is your own ID for the user, the one your backend uses. Everything else is optional. ## What happens when you do * The anonymous visitor on this device becomes that user. Their email, name, picture, and details now show in the inbox. * If Feddy already knows this `userId` from another device, the two records merge into one. The conversations from this device move over, and the user sees a single history everywhere they log in. * The details you send replace the previous set as a whole. Send everything you want to keep on every call; leave a key out and it's removed. Email, name, and picture are the exception: they stay if you don't send them. ## Details about the user Alongside the ID, you can send facts that help you answer faster: their plan, whether they're a paying member, when their trial ends, how long they've had an account. Feddy calls these **attributes**. They're simple key-value pairs; values can be text, numbers, true/false, or dates written like `2026-09-22T10:00:00Z`. > [!NOTE] > > Attributes are for facts about the person. Device model, OS version, app version, and language are collected by the SDK and shown with every conversation already, so there's no need to send those. There are limits, but hitting one never causes an error. Anything over a limit is quietly dropped, so a mistake in your integration can't stop a user from writing in. | Limit | Value | | - | - | | Keys per contact | 20 | | Key length | 40 characters | | Value length | 255 characters as text | | Total size | 8 KB | | Value types | text, number, true/false, date as `2026-09-22T10:00:00Z` | Nested objects and lists are dropped too. Keep it flat. Once you've sent a key, it appears under **Settings → Data attributes** in the dashboard, where Feddy has worked out whether it's text, a number, or a date. From there you can give it a friendlier label for the inbox, show it as a column in the conversation list, or make it something you can filter by. ## Subscription status Whether someone is paying is usually the first thing you want to know when they write in. Your subscription tool already knows it, so pass it on as attributes, and call `identify` again whenever it changes: after a purchase, a renewal, a cancellation, or a refund. Each call replaces the whole set of attributes, so keep one function that sends all of them, and call that from everywhere, including your subscription listener: ```swift theme={null} func updateFeddy(isSubscribed: Bool, renewsAt: Date?) { guard let user = currentUser else { return } var attributes: [String: Any] = ["plan": user.plan, "is_subscribed": isSubscribed] if let renewsAt { attributes["renews_at"] = renewsAt } Feddy.identify(userId: user.id, email: user.email, attributes: attributes) } ``` Then call it whenever your subscription tool reports a change. Replace `pro` or `premium` with the name of your own entitlement or access level. ```swift RevenueCat theme={null} Task { for await info in Purchases.shared.customerInfoStream { let pro = info.entitlements["pro"] updateFeddy(isSubscribed: pro?.isActive == true, renewsAt: pro?.expirationDate) } } ``` ```swift Adapty theme={null} final class SubscriptionObserver: AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { let premium = profile.accessLevels["premium"] updateFeddy(isSubscribed: premium?.isActive == true, renewsAt: premium?.expiresAt) } } // After Adapty.activate Adapty.delegate = subscriptionObserver ``` ```swift Superwall theme={null} subscriptionCancellable = Superwall.shared.$subscriptionStatus .receive(on: DispatchQueue.main) .sink { status in switch status { case .active: updateFeddy(isSubscribed: true, renewsAt: nil) case .inactive: updateFeddy(isSubscribed: false, renewsAt: nil) case .unknown: break } } ``` ```swift StoreKit 2 theme={null} func refreshSubscription() async { var active: Transaction? for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result, transaction.productType == .autoRenewable { active = transaction } } updateFeddy(isSubscribed: active != nil, renewsAt: active?.expirationDate) } // At launch, and whenever a transaction changes Task { await refreshSubscription() for await _ in Transaction.updates { await refreshSubscription() } } ``` With StoreKit 2, if your app already listens to `Transaction.updates` to finish purchases, call `refreshSubscription()` from that listener instead of starting a second one. > [!NOTE] > > Feddy doesn't connect to RevenueCat, Adapty, or Superwall, and never needs their keys. Your app passes along what it already knows. ## Email Pass `email` when you have it. If your project has a verified email domain, replies are also emailed to the user, so they hear back even if they never reopen the app. Without a verified domain the email is stored but nothing is sent. If you don't pass an email and your project has a verified domain, the widget may ask the user for one after their first message. It only asks once per device. > [!TIP] > > Log in on a test build, send a message, and open the inbox. The conversation shows the user's name and the attributes you sent, instead of **Anonymous**. ## Next steps > [card] [Contacts](/docs/concepts/contacts) > > What Feddy knows about a person, and where to see it in the dashboard. > [card] [Team](/docs/concepts/team) > > Verify an email domain so replies reach users by email. > [card] [iOS SDK reference](/docs/reference/ios) > > The full `identify` signature for Swift. > [card] [Web SDK reference](/docs/reference/web) > > The full `identify` options for the web widget. # Use Feddy from your AI assistant Source: https://feddy.app/docs/guides/mcp Connect Claude, Cursor, ChatGPT, or Codex to your inbox over MCP, then read, reply, and change how Milo answers by asking. Feddy runs an MCP server. Connect your AI assistant to it and you can work your inbox in plain language: * "What came in today? Reply to the easy ones." * "From now on, politely turn down anyone asking for a student discount." * "Save what I just told #118 as an answer." * "What are people asking for most this month?" ## Connect your assistant Add this address to your assistant: ``` https://core.feddy.app/mcp ``` Your browser then opens a Feddy page asking you to approve the connection. There's nothing to copy and no key to keep safe. > [tab] Claude Code > > ```bash theme={null} > claude mcp add --transport http feddy https://core.feddy.app/mcp > ``` > > Start a session, type `/mcp`, select **feddy**, and choose **Authenticate**. > [tab] Claude > > On claude.ai or in the Claude desktop app, open **Settings → Connectors**, click **Add custom connector**, and paste the address. > > On Team and Enterprise plans, an owner adds the connector for the organization first. Each member then clicks **Connect**. > [tab] Cursor > > Add this to `~/.cursor/mcp.json`: > > ```json theme={null} > { > "mcpServers": { > "feddy": { > "url": "https://core.feddy.app/mcp" > } > } > } > ``` > > Cursor asks you to sign in the first time it connects. > [tab] ChatGPT > > In ChatGPT's connector settings, turn on developer mode, create a connector with the address, and choose OAuth as the authentication. > [tab] Codex > > ```bash theme={null} > codex mcp add feddy --url https://core.feddy.app/mcp > codex mcp login feddy > ``` Any other assistant that can connect to a remote MCP server with OAuth works the same way. ## Choose what it can do The approval page shows which app is asking, which team it will work in, and where it sends you back afterwards. Your assistant can always read your conversations, users, and Milo's settings. Tick what else it can do: | Permission | What it allows | | - | - | | **Reply to users** | Sends messages your users receive. Off unless you tick it. | | **Handle conversations** | Adds notes your team sees, closes and reopens conversations, changes topics. | | **Manage Milo** | Changes Milo's instructions and answers. | Anything you leave unticked doesn't exist for that assistant. If it can't reply, it drafts the reply for you to send from the dashboard instead. To change permissions later, disconnect the app and connect it again. ## See and disconnect apps Open your avatar menu → **My profile** → **MCP**. Every connected app is listed with its permissions and when it was last used. **Disconnect** cuts it off right away. The connection acts as you, in the projects you can see. It stops working if you leave the team or reset your password. ## What it can do | Tool | What it does | | - | - | | `list_projects` | Lists your projects | | `list_conversations` | Lists conversations, filtered by project, status, unread, or text | | `get_conversation` | Reads a whole conversation by number (`#118`) or ID, with who the user is, the device they wrote from, and where a reply will reach them | | `reply` | Sends a reply to the user, optionally translated into their language | | `add_note` | Adds a note only your team sees | | `update_conversation` | Closes or reopens a conversation, or changes its topic | | `find_contacts`, `get_contact` | Finds users by name, email, or the details your app sends | | `list_answers`, `create_answer`, `update_answer` | Reads and edits the answers Milo replies from | | `get_milo`, `update_milo` | Reads and changes Milo's instructions and mode | | `list_requests`, `get_request` | Reads the feature requests and bugs Milo grouped from conversations | | `list_unanswered` | Lists the questions Milo couldn't answer | Your plan applies the same way it does in the dashboard. For example, translated replies need a paid plan. ## Teach Milo by telling your assistant When you say how support should work from now on, your assistant updates Milo's instructions. When you state a fact users ask about, it saves an answer. Both take effect on Milo's next reply. To check what changed, open **Settings → Milo AI** in the dashboard, or ask your assistant to show Milo's settings. # Unread badge Source: https://feddy.app/docs/guides/unread-badge Show users that a reply is waiting, on the button that opens Feddy or anywhere else in your app. By the end of this page, the button that opens Feddy will show how many replies are waiting, and the number will disappear once the user has read them. You don't have to keep the count up to date yourself. The SDK checks for new replies on its own: every 30 seconds on iOS, and every 45 seconds on the web plus whenever the user returns to the tab. ## iOS ### A row in a List or Form ```swift theme={null} Form { Button("Support") { Feddy.present() } .feddyUnreadBadge() } ``` This is the same badge iOS puts on rows in the Settings app. Nothing is drawn when the count is zero. ### A row that draws its own chevron A list badge sits at the trailing edge of the row, which would put it outside your chevron. Place the marker yourself instead: ```swift theme={null} HStack { Text("Support") Spacer() FeddyUnreadDot(showsCount: true) Image(systemName: "chevron.forward") } ``` Pass `showsCount: false` for a plain dot without the number. ### A tab bar item or anything custom Read the count from `FeddyUnread.shared`. It's an observable object, so a SwiftUI view that reads it updates itself whenever the count changes: ```swift theme={null} struct RootTabs: View { @ObservedObject private var unread = FeddyUnread.shared var body: some View { TabView { SupportView() .tabItem { Label("Support", systemImage: "bubble.left") } .badge(unread.count) } } } ``` In a UIKit app, assign a function to `Feddy.onUnreadCountChanged` and set `tabBarItem.badgeValue` from it. ### Keep it fresh when the app comes back While the app is in the background, the SDK stops checking. When the user returns, ask for a fresh count right away so the badge is right before the next scheduled check: ```swift theme={null} .onChange(of: scenePhase) { phase in if phase == .active { Feddy.refresh() } } ``` ## Web If you're using the floating chat button, there's nothing to do: it shows the count itself. If you've turned it off with `launcher: false`, give Feddy a function to call whenever the count changes, and update your own element from it: ```js theme={null} Feddy.init({ projectId: 'fd_XXXXXXXXXXXXXXXX', launcher: false }) const badge = document.querySelector('#help-badge') Feddy.onUnreadCountChanged = (count) => { badge.hidden = count === 0 badge.textContent = String(count) } ``` ## Reply popups A badge only helps if the user looks at where you put it. So when someone comes back to your app with a reply they haven't seen, Feddy also shows a card with the latest message, at the top of the screen on iOS and above the floating button on the web. Tapping it opens the conversation. Each reply pops up at most once, and only replies from the last 14 days do. After that the badge is the only signal. Turn popups off with `Feddy.replyPopupsEnabled = false` on iOS (for example while a paywall is up) or `replyPopups: false` in `Feddy.init` on the web. ## What counts as unread A reply is unread until the user opens the conversation it belongs to. Opening Feddy isn't enough on its own. Replies in closed conversations count too: closing a conversation doesn't mean the user has read your reply. > [!TIP] > > Send a message from a test build, reply from the inbox, and watch the badge show `1`. Open the conversation in the app and it goes away. ## Next steps > [card] [Conversations](/docs/concepts/conversations) > > What makes a conversation unread in the inbox, and how to mark one unread again. > [card] [iOS SDK reference](/docs/reference/ios) > > `feddyUnreadBadge`, `FeddyUnreadDot`, `FeddyUnread`, and `refresh` in full. > [card] [Web SDK reference](/docs/reference/web) > > `onUnreadCountChanged` and the `launcher` option in full. > [card] [Troubleshooting](/docs/troubleshooting) > > What to check when the badge doesn't update. # Introduction Source: https://feddy.app/docs/introduction In-app support for iOS apps and websites. Users write in from inside your product; replies land back where they asked. Feddy is in-app support for iOS apps and websites. Your users write to you from inside the product, without leaving for a website or looking up an email address. Everything they send lands in one inbox. When you reply, the answer shows up right where they asked, and by email too once you've connected your own email domain. Here's how it works: 1. **A user writes in.** They tap Support in your app, or the chat button on your site, and send a message. 2. **You see it in your inbox.** The message arrives with the user's device details and anything you've told Feddy about them, so you can answer without asking follow-up questions. 3. **Your reply reaches them.** It appears in the app or on the site, with a badge so they notice it. If you've connected an email domain, they get it by email as well. Milo, the AI in Feddy, can handle the questions you've already answered once. It only ever uses answers your team wrote, or learned from your team's replies, and passes everything else to you. ## Before you start You'll need a Feddy project and its project ID. Both take a minute, and the free plan covers everything in these docs. > [card] [Create a project](/docs/getting-started/create-a-project) > > Sign up, create a project for your app or site, and copy its project ID. > [card] [Set up with an AI agent](/docs/getting-started/ai-agent) > > Prefer to let Claude Code, Cursor, or another coding agent do the install? Start here. ## Add Feddy to your product Pick your platform. Each page takes about five minutes and ends with a real message in your inbox. ### iOS > [card] [SwiftUI](/docs/quickstart/ios/swiftui) > > A Swift package and a Support row. > [card] [UIKit](/docs/quickstart/ios/uikit) > > The same package, from your app delegate. ### Web > [card] [JavaScript](/docs/quickstart/web/javascript) > > One script tag, nothing to build. > [card] [React](/docs/quickstart/web/react) > > A small component and your own button. > [card] [Vue](/docs/quickstart/web/vue) > > Load on mount, open from a button. > [card] [Svelte](/docs/quickstart/web/svelte) > > Load on mount, open from a button. > [card] [Next.js](/docs/quickstart/web/nextjs) > > Once in the root layout. > [card] [Astro](/docs/quickstart/web/astro) > > Two lines in your shared layout. > [card] [WordPress](/docs/quickstart/web/wordpress) > > From your theme or a small plugin. > [card] [Webflow](/docs/quickstart/web/webflow) > > Paste into Site settings and publish. ## Learn how it works Once messages are coming in, these pages explain what you're looking at in the dashboard and what you can do with it. > [card] [Projects](/docs/concepts/projects) > > What a project holds, and why its ID is safe to share. > [card] [Conversations](/docs/concepts/conversations) > > What a thread contains, what its status means, and what happens on its own. > [card] [Contacts](/docs/concepts/contacts) > > Who's writing in, from an anonymous visitor to a user you've identified. > [card] [Milo](/docs/concepts/assistant) > > Automatic answers to repeat questions, and a Copilot that drafts replies for you. > [card] [Team](/docs/concepts/team) > > Inviting teammates, getting notified, and sending replies by email. ## Go further > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show a count on your Support button so users notice when you've answered. > [card] [iOS SDK reference](/docs/reference/ios) > > Every call in the Swift package, with its signature and what it does. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the web widget. > [card] [Troubleshooting](/docs/troubleshooting) > > What to check when something doesn't behave as expected. # SwiftUI Source: https://feddy.app/docs/quickstart/ios/swiftui Add Feddy to a SwiftUI app with Swift Package Manager and send your first message. By the end of this page your app will have a Support row that opens Feddy, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Xcode 15 or later. The SDK supports iOS 15 and up and doesn't pull in any other packages. **1. Add the package** In Xcode, choose **File → Add Package Dependencies…** and paste this URL: ``` https://github.com/FeddyLab/feddy-ios ``` If you manage dependencies in a `Package.swift` file instead, add: ```swift theme={null} .package(url: "https://github.com/FeddyLab/feddy-ios", from: "0.8.5") ``` **2. Tell Feddy which project this is** Do this once, when the app launches, before anything else can open Feddy. Calling it again later does nothing. ```swift App.swift theme={null} import Feddy @main struct MyApp: App { init() { Feddy.configure(projectId: "fd_XXXXXXXXXXXXXXXX") } var body: some Scene { WindowGroup { ContentView() } } } ``` > [!NOTE] > > **The project ID isn't a secret.** It's built into your app, so anyone who downloads the app can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it to your repository like any other setting. There's nothing to protect, so keep it out of your secrets file. **3. Give users a way in** Add a row wherever users already go looking for help. For most apps that's Settings: ```swift theme={null} Form { Button("Support") { Feddy.present() } .feddyUnreadBadge() } ``` `present()` slides Feddy up over whatever is on screen and opens on its home: a greeting, a button to send a new message, and the user's recent conversations. If you'd rather skip straight to the new-message screen, call `presentNewConversation()` instead. `.feddyUnreadBadge()` puts a count on the row when a reply is waiting, the same way iOS shows counts in Settings. It disappears when there's nothing unread. **4. Send yourself a message** Run the app, tap the row, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the device model and app version alongside. Reply from the inbox. Back in the app, the reply appears in the conversation, and the Support row shows a `1` until the user opens it. > [!TIP] > > That's the whole loop: a user writes in, you answer, and the answer reaches them inside the app. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Put the count on a tab bar item, a custom row, or anywhere else. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [iOS SDK reference](/docs/reference/ios) > > Every call in the package, with its signature and what it does. # UIKit Source: https://feddy.app/docs/quickstart/ios/uikit Add Feddy to a UIKit app with Swift Package Manager and send your first message. By the end of this page your app will have a Support button that opens Feddy, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Xcode 15 or later. The SDK supports iOS 15 and up and doesn't pull in any other packages. **1. Add the package** In Xcode, choose **File → Add Package Dependencies…** and paste this URL: ``` https://github.com/FeddyLab/feddy-ios ``` If you manage dependencies in a `Package.swift` file instead, add: ```swift theme={null} .package(url: "https://github.com/FeddyLab/feddy-ios", from: "0.8.5") ``` **2. Tell Feddy which project this is** Do this once, when the app launches, before anything else can open Feddy. Calling it again later does nothing. ```swift AppDelegate.swift theme={null} import Feddy func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Feddy.configure(projectId: "fd_XXXXXXXXXXXXXXXX") return true } ``` > [!NOTE] > > **The project ID isn't a secret.** It's built into your app, so anyone who downloads the app can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it to your repository like any other setting. There's nothing to protect, so keep it out of your secrets file. **3. Give users a way in** Call `present()` from wherever users go looking for help. For most apps that's a row in Settings or a bar button. Feddy slides up over the current screen and opens on its home: a greeting, a button to send a new message, and the user's recent conversations. ```swift SettingsViewController.swift theme={null} @objc func helpTapped() { Feddy.present() } ``` To show a count on that button when a reply is waiting, give Feddy a function to call whenever the number changes. It runs on the main thread, so you can update the UI directly: ```swift theme={null} Feddy.onUnreadCountChanged = { count in helpItem.badgeValue = count > 0 ? "\(count)" : nil } ``` **4. Send yourself a message** Run the app, tap the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the device model and app version alongside. Reply from the inbox. Back in the app, the reply appears in the conversation, and the button shows a `1` until the user opens it. > [!TIP] > > That's the whole loop: a user writes in, you answer, and the answer reaches them inside the app. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Keep the count fresh when the app comes back to the foreground, and other placements. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [iOS SDK reference](/docs/reference/ios) > > Every call in the package, with its signature and what it does. # Astro Source: https://feddy.app/docs/quickstart/web/astro Add the Feddy widget to an Astro site with two lines in the layout every page shares. By the end of this page your site will have a chat button that opens Feddy on every page, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Nothing to install from npm. The widget is one script loaded from Feddy's servers. **1. Add two lines to your layout** Astro normally rewrites ` ``` Put them just before the closing `` tag of the layout every page uses. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your page source, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it like any other setting. There's nothing to protect, so keep it out of your secrets file. **2. Choose how users open it** By default, a floating chat button appears in the bottom-right corner. If you'd rather open Feddy from your own button, pass `launcher: false` to `init` and call `Feddy.open()` when the button is clicked. If your site uses Astro's `` for page transitions, the widget stays in place as users move between pages. **3. Send yourself a message** Open the page, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the page, the reply appears in the panel. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show the count on your own element when you've turned the floating button off. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # JavaScript Source: https://feddy.app/docs/quickstart/web/javascript Add the Feddy widget to any page with one script tag and send your first message. By the end of this page your site will have a chat button that opens Feddy, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * A page you can edit. There's nothing to install and no build step; the widget is one script loaded from Feddy's servers. **1. Add the script** Put this just before the closing `` tag: ```html index.html theme={null} ``` It's a single file with no dependencies. The widget keeps its own styles, so it never clashes with your page's CSS, and your CSS never leaks into it. **2. Tell Feddy which project this is** ```html theme={null} ``` This runs once per page load; calling it again does nothing. If the script runs before the page has finished loading, the widget waits and appears when it's ready. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your page source, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it like any other setting. There's nothing to protect, so keep it out of your secrets file. **3. Choose how users open it** By default, a floating chat button appears in the bottom-right corner of the page. That's all most sites need. If your site already has a **Support** or **Feedback** link, you can turn the floating button off and open Feddy from your own element instead: ```html theme={null} ``` **4. Send yourself a message** Open the page, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the page, the reply appears in the panel, and the floating button shows a count until the visitor opens it. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show the count on your own element when you've turned the floating button off. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # Next.js Source: https://feddy.app/docs/quickstart/web/nextjs Load the Feddy widget once from your root layout so it follows the user across pages. By the end of this page your site will have a chat button that opens Feddy on every page, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Nothing to install from npm. The widget is one script loaded from Feddy's servers after the page is interactive. **1. Add a client component** Next.js has a built-in `Script` component for loading scripts like this one. It needs to know when the script has finished loading so it can tell Feddy which project this is, and that callback only works in a client component, hence the `'use client'` line: ```tsx app/support.tsx theme={null} 'use client' import Script from 'next/script' export function Support() { return ( ``` On Svelte 4, write the handler as `on:click` instead of `onclick`. Leave out `launcher: false` if you'd rather have the floating button instead of your own. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your bundle, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it like any other setting. There's nothing to protect, so keep it out of your secrets file. **2. Render it once** Put `` somewhere that stays mounted, such as your root layout or settings page. The widget keeps its own styles, so it never clashes with your CSS. **3. Send yourself a message** Open the page, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the page, the reply appears in the panel. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show a count on your Support button when a reply is waiting. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # Vue Source: https://feddy.app/docs/quickstart/web/vue Load the Feddy widget when your component mounts and open it from your own button. By the end of this page your app will have a Support button that opens Feddy, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Nothing to install from npm. The widget is one script loaded from Feddy's servers when your component mounts. **1. Add a component** The component loads the script once, then tells Feddy which project this is. `launcher: false` turns off the floating chat button so your own button is the way in: ```vue Support.vue theme={null} ``` The `open` function lives in the script because a Vue template can't reach the global `Feddy` object directly. Leave out `launcher: false` if you'd rather have the floating button instead of your own. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your bundle, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it like any other setting. There's nothing to protect, so keep it out of your secrets file. **2. Render it once** Put `` somewhere that stays mounted, such as your app shell or settings page. The widget keeps its own styles, so it never clashes with your CSS. **3. Send yourself a message** Open the page, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the page, the reply appears in the panel. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show a count on your Support button when a reply is waiting. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # Webflow Source: https://feddy.app/docs/quickstart/web/webflow Add the Feddy widget to a Webflow site by pasting two lines into Custom code. By the end of this page your site will have a chat button that opens Feddy on every page, and a message sent from it will be sitting in your inbox. It takes about five minutes and no code beyond what you paste. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * A Webflow site you can publish. **1. Paste into Custom code** In Webflow, open **Site settings → Custom code** and paste this into **Footer code**: ```html Site settings → Custom code → Footer code theme={null} ``` Then publish the site. Custom code doesn't run in the Designer preview, so you'll only see the widget on the published site. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your page source, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. There's nothing to protect. **2. Choose how users open it** By default, a floating chat button appears in the bottom-right corner of every page. If you'd rather use a button you designed, give it an ID such as `help` in the Designer, add `launcher: false` to the `init` call, and add this line after it: `document.querySelector('#help').addEventListener('click', () => Feddy.open())`. **3. Send yourself a message** Open the published site, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the site, the reply appears in the panel. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show the count on your own element when you've turned the floating button off. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # WordPress Source: https://feddy.app/docs/quickstart/web/wordpress Add the Feddy widget to a WordPress site from your theme or a small plugin. By the end of this page your site will have a chat button that opens Feddy on every page, and a message sent from it will be sitting in your inbox. It takes about five minutes. ## Before you start * A Feddy project and its project ID. You'll find the ID under **Settings → Install** in the dashboard. No project yet? [Create one](/docs/getting-started/create-a-project) first. * Access to your theme's files, or the ability to add a plugin. Nothing to install from npm; the widget is one script loaded from Feddy's servers. **1. Add the script** Add this to your theme's `functions.php`. If you'd rather not touch the theme, the same lines work in a small plugin file: ```php functions.php theme={null} add_action('wp_enqueue_scripts', function () { wp_enqueue_script('feddy', 'https://core.feddy.app/sdk/feddy.js', [], null, true); wp_add_inline_script('feddy', "Feddy.init({ projectId: 'fd_XXXXXXXXXXXXXXXX' })"); }); ``` Two details worth knowing: the final `true` places the script at the bottom of the page so it never slows down your content, and `null` as the version stops WordPress from adding its own cache-busting suffix, so browsers cache the file the way Feddy intends. > [!NOTE] > > **The project ID isn't a secret.** It's visible in your page source, so anyone can find it, and that's fine: it only tells Feddy which inbox a message belongs to. Commit it like any other setting. There's nothing to protect, so keep it out of your secrets file. **2. Choose how users open it** By default, a floating chat button appears in the bottom-right corner of every page. If you'd rather open Feddy from a menu item or button, pass `launcher: false` to `init` and add a click handler that calls `Feddy.open()`. **3. Send yourself a message** Open the site, click the button, and send a message. Then open your inbox at [dash.feddy.app](https://dash.feddy.app): it's there, with the page URL and browser alongside. Reply from the inbox. Back on the site, the reply appears in the panel. > [!TIP] > > That's the whole loop: a visitor writes in, you answer, and the answer reaches them on your site. ## Next steps > [card] [Identify users](/docs/guides/identify-users) > > Tell Feddy who's logged in, so you see their name in the inbox and replies can reach them by email. > [card] [Unread badge](/docs/guides/unread-badge) > > Show the count on your own element when you've turned the floating button off. > [card] [Conversations](/docs/concepts/conversations) > > What you're looking at in the inbox, and what happens on its own. > [card] [Web SDK reference](/docs/reference/web) > > Every option and call in the widget. # iOS SDK reference Source: https://feddy.app/docs/reference/ios Every public call in the Feddy Swift package, with its signature and what it does. The Swift package gives you one namespace, `Feddy`, plus three SwiftUI helpers for showing unread counts. Everything else is internal, so this page is the complete list of what you can call. Requirements: iOS 15 or later, Swift 5.9 or later, no other packages. Package URL `https://github.com/FeddyLab/feddy-ios`, product `Feddy`. ## `Feddy.configure(projectId:apiURL:)` ```swift theme={null} static func configure(projectId: String, apiURL: URL = URL(string: "https://core.feddy.app")!) ``` Sets Feddy up for this app. Call it once at launch, before anything else can open Feddy; later calls do nothing. It starts loading your project's settings and the unread count, and from then on checks for new replies every 30 seconds while the app is running. * `projectId`: copied from **Settings → Install** in the dashboard. Public by design; see [Create a project](/docs/getting-started/create-a-project). * `apiURL`: leave the default in production. Point it at a local server during development. ## `Feddy.present()` ```swift theme={null} @MainActor static func present() ``` Opens Feddy as a sheet that slides up over whatever's on screen. It opens on the home: a greeting, a button to send a new message, and the user's recent conversations, with a link to all of them. Works from SwiftUI and UIKit alike. Does nothing if the sheet is already open. If called before `configure`, it logs `[Feddy] present() called before configure(projectId:)` and does nothing. ## `Feddy.presentNewConversation()` ```swift theme={null} @MainActor static func presentNewConversation() ``` Same as `present()`, but opens straight onto the new-message screen. ## `Feddy.identify(userId:email:name:avatarUrl:attributes:)` ```swift theme={null} static func identify(userId: String, email: String? = nil, name: String? = nil, avatarUrl: String? = nil, attributes: [String: Any] = [:]) ``` Tells Feddy which of your users is on this device. Call it after login and whenever the details you send change; it's safe to call as often as you like. Does nothing before `configure`. What happens on the server, including how records from different devices merge, is in [Identify users](/docs/guides/identify-users). * `userId`: your own stable ID for the user, 1 to 128 characters. * `email`: optional. Lets replies reach the user by email once your project has a verified email domain. * `name`: optional. Shown in the inbox. * `avatarUrl`: optional. An `http(s)` URL of a picture you already have of the user, shown in the inbox. Anything else is ignored. * `attributes`: optional facts about the user, such as their plan. Values may be `String`, `Bool`, any number, or `Date`; a `Date` is sent as text like `2026-09-22T10:00:00Z`. Other types are dropped on the device. Server-side limits are listed in [Identify users](/docs/guides/identify-users). ## `Feddy.unreadCount(_:)` ```swift theme={null} static func unreadCount(_ completion: @escaping (Int) -> Void) ``` Fetches the current number of unread replies and hands it to `completion` on the main thread. ## `Feddy.refresh()` ```swift theme={null} static func refresh() ``` Asks for a fresh unread count right now instead of waiting for the next scheduled check. Call it when your app returns to the foreground so a badge is up to date immediately. ```swift theme={null} .onChange(of: scenePhase) { phase in if phase == .active { Feddy.refresh() } } ``` ## `Feddy.onUnreadCountChanged` ```swift theme={null} @MainActor static var onUnreadCountChanged: ((Int) -> Void)? ``` A function Feddy calls, on the main thread, whenever the unread count changes. Meant for UIKit badges; SwiftUI apps should use the helpers below instead. ## `Feddy.replyPopupsEnabled` ```swift theme={null} @MainActor static var replyPopupsEnabled: Bool ``` Whether a card slides in at the top of the screen when the user comes back with a reply they haven't seen. On by default. Set it to `false` before a screen that must not be interrupted, such as a paywall or a game level, and back to `true` afterwards; a waiting reply then shows. ```swift theme={null} Feddy.replyPopupsEnabled = false // show the paywall Feddy.replyPopupsEnabled = true ``` ## `View.feddyUnreadBadge()` ```swift theme={null} func feddyUnreadBadge() -> some View ``` Adds the system row badge with the unread count and keeps it current. Nothing is drawn at zero. Badges only render inside `List`, `Form`, and `TabView`; anywhere else the modifier does nothing. ## `FeddyUnreadDot` ```swift theme={null} struct FeddyUnreadDot: View { init(showsCount: Bool = false) } ``` A red marker you place yourself, for rows that draw their own chevron or for spots a list badge can't reach. Renders nothing at zero. Pass `showsCount: true` to draw the number inside the marker. ## `FeddyUnread` ```swift theme={null} @MainActor final class FeddyUnread: ObservableObject { static let shared: FeddyUnread @Published private(set) var count: Int } ``` The unread count as an observable object, for custom SwiftUI badges. A view that reads it updates itself whenever the count changes. ```swift theme={null} @ObservedObject private var unread = FeddyUnread.shared ``` ## What happens on its own * **Identity**: the first time the SDK runs, it creates an anonymous ID and stores it in the Keychain, so a user's history survives reinstalls. * **Language**: the sheet ships in English, Simplified Chinese, Traditional Chinese, Japanese, Korean, German, French, Spanish, Brazilian Portuguese, and Russian, and follows the device language. Other languages fall back to English. Text you write in the dashboard, such as topics, is translated on the server according to your project settings. * **Attachments**: up to 5 images per message, resized so the longest edge is 3000 px before upload. The picker needs iOS 16; on iOS 15 the attachment row isn't shown. * **Reply popups**: when the app becomes active with a reply the user hasn't seen, a card slides in at the top of the screen after about two seconds, saying who replied and showing the first two lines of the latest reply. Tapping it opens that conversation. Swiping up dismisses it, and it goes away on its own after 8 seconds (it stays while VoiceOver is on, with a Close action). If several replies are waiting, only the newest shows. Each reply shows at most once, only replies from the last 14 days, and never while the Feddy sheet is open. The SDK draws it over your screen; it isn't a notification and needs no permission. * **Branding**: the name, accent color, and logo users see come from the dashboard, not from code. * **Privacy manifest**: the package ships a `PrivacyInfo.xcprivacy` that declares what the SDK collects, so there's nothing for you to add for App Store review. # Web SDK reference Source: https://feddy.app/docs/reference/web Every option and call in the Feddy web widget. The widget is one script with no dependencies. Loading it defines a global `Feddy` object with four methods and one callback, and this page is the complete list. ```html theme={null} ``` The script can be loaded from any site; it's served with a one-hour cache and open CORS. The widget draws itself inside a shadow root attached to `div#feddy-widget`, which means it keeps its own styles: your page's CSS never affects it and its CSS never affects your page. Keyboard events inside the widget don't reach your page either. ## `Feddy.init(options)` ```js theme={null} Feddy.init({ projectId: 'fd_XXXXXXXXXXXXXXXX' }) ``` Sets the widget up on the page. It only takes effect once per page load; later calls do nothing. If the page is still loading when you call it, the widget waits and appears once the page is ready. Without a project ID it logs `[Feddy] init requires a projectId` and does nothing. | Option | Type | Default | Description | | - | - | - | - | | `projectId` | `string` | required | Copied from **Settings → Install** in the dashboard. Public by design. | | `apiUrl` | `string` | `https://core.feddy.app` | Point at a local server during development. | | `launcher` | `boolean` | `true` | Show the floating chat button bottom-right. Set `false` to open the panel from your own element instead. | | `sounds` | `boolean` | `true` | Play a chime when a reply arrives. | | `replyPopups` | `boolean` | `true` | Show a card above the floating button when a reply the visitor hasn't seen is waiting. Set `false` to rely on the badge alone. | | `locale` | `string` | `navigator.language` | Force a language with a code such as `'ja'` or `'zh-TW'`. | ## `Feddy.identify(options)` ```js theme={null} Feddy.identify({ userId: 'u_123', email: 'user@example.com', name: 'Ada', avatarUrl: 'https://cdn.example.com/avatars/u_123.png', attributes: { plan: 'pro', credits_left: 42 }, }) ``` Tells Feddy which of your users is on this browser. You can call it before `init`; the call is held and sent once the widget is ready. Does nothing without `userId`. If something goes wrong, it logs a warning with `console.warn` and never throws. What happens on the server, including how records from different devices merge, is in [Identify users](/docs/guides/identify-users). | Field | Type | Description | | - | - | - | | `userId` | `string` | Your own stable ID for the user, 1 to 128 characters. | | `email` | `string` | Optional. Lets replies reach the user by email once your project has a verified email domain. | | `name` | `string` | Optional. Shown in the inbox. | | `avatarUrl` | `string` | Optional. An `http(s)` URL of a picture you already have of the user, shown in the inbox. Anything else is ignored. | | `attributes` | `object` | Optional facts about the user, such as their plan: a flat object of strings, numbers, and booleans. Limits are listed in [Identify users](/docs/guides/identify-users). | ## `Feddy.open()` and `Feddy.close()` ```js theme={null} document.querySelector('#help').addEventListener('click', () => Feddy.open()) ``` Open or close the panel from your own element. `open()` shows the user's conversations and a button to start a new one. Both do nothing until `init` has set the widget up. ## `Feddy.onUnreadCountChanged` ```js theme={null} Feddy.onUnreadCountChanged = (count) => { badge.textContent = count > 0 ? String(count) : '' } ``` A function the widget calls whenever the unread count changes. The widget checks every 45 seconds and again each time the tab becomes visible. Use it to drive your own badge when the floating button is off. ## What happens on its own * **Identity**: the first time the widget runs, it creates an anonymous ID and stores it in `localStorage` under `feddy_anon_id`. Clearing site data resets it, which is why the widget offers to collect an email after a first message when your project has a verified email domain. * **Language**: the widget ships in English, Simplified Chinese, Traditional Chinese, Japanese, Korean, German, French, Spanish, Brazilian Portuguese, and Russian, and follows the browser language. `zh-TW`, `zh-HK`, and `zh-MO` resolve to Traditional Chinese; `pt` resolves to Brazilian Portuguese. Other languages fall back to English. Text you write in the dashboard, such as topics, is translated on the server according to your project settings. * **Attachments**: up to 5 images per message, 10 MB each before resizing. The browser resizes them so the longest edge is 2000 px and converts them to WebP before upload. * **Screenshots**: a camera button in the message box attaches a picture of the page as the visitor sees it, with the widget itself left out. It's made in the browser with no permission prompt. Images from other sites, videos, and embedded frames show as grey boxes, and web fonts fall back to system fonts, which is enough to show which screen and what state. The button is hidden when attachments aren't available on your plan. * **Branding**: the name, accent color, and logo users see come from the dashboard. The accent color is applied as `--fd-accent` inside the shadow root, and the widget picks a readable text color to go with it. * **Reply popups**: when a reply the visitor hasn't seen is waiting, a card appears above the floating button saying who replied, with the first two lines of the latest reply. Clicking it opens that conversation; the close button dismisses it. If several replies are waiting, only the newest shows. Each reply shows at most once per browser, only replies from the last 14 days, and never while the panel is open. It checks shortly after the page loads, when the tab becomes visible again, and when a new reply arrives. * **Floating button**: fixed bottom-right, 56 px, with an unread count badge. Its `z-index` is `2147483000`, so it sits above everything on the page. # Troubleshooting Source: https://feddy.app/docs/troubleshooting What to check when Feddy doesn't behave as expected, on iOS, on the web, and in the inbox. Most problems come down to one of a few things: Feddy hasn't been set up yet when something tries to open it, the project ID doesn't match, or a feature needs a setting that isn't on. Find your symptom below. ## iOS > [accordion] Nothing happens when I call present() > > **Possible cause:** `configure` hasn't run yet. The console shows `[Feddy] present() called before configure(projectId:)`. > > **Solution:** Call `Feddy.configure` in your app's `init` (SwiftUI) or in `application(_:didFinishLaunchingWithOptions:)` (UIKit), so it runs before any screen can call `present()`. > [accordion] The unread badge doesn't update > > **Possible cause:** `.feddyUnreadBadge()` only draws inside a `List`, `Form`, or `TabView`. Anywhere else it does nothing. > > **Solution:** Use `FeddyUnreadDot` for a marker you place yourself, or read the count from `FeddyUnread.shared`. See [Unread badge](/docs/guides/unread-badge). > > **Another possible cause:** the SDK stops checking for replies while the app is in the background, so the badge can be stale for a moment after the user comes back. > > **Solution:** Call `Feddy.refresh()` when the app becomes active. > > Also keep in mind that only opening a conversation clears its count. Opening Feddy doesn't. > [accordion] The attachment button is missing > > **Possible cause:** The image picker needs iOS 16. On iOS 15 the attachment row isn't shown. > > **Another possible cause:** Attachments are a paid-plan feature. > > Only PNG, JPEG, and WebP images are accepted, up to 5 per message. ## Web > [accordion] The widget doesn't appear > > **Possible cause:** `init` was called without a project ID. The console shows `[Feddy] init requires a projectId`. > > **Solution:** Pass `projectId` to `Feddy.init`, copied from **Settings → Install**. > > **Another possible cause:** The script never loaded. In the console, `typeof Feddy` should print `"object"`; if it prints `"undefined"`, check the script tag's URL and that nothing is blocking it. > > **Another possible cause:** You passed `launcher: false`, which turns off the floating button. Nothing is drawn until you call `Feddy.open()` from your own element. > > Note that `init` only takes effect once per page load. To start over, reload the page. > [accordion] The unread count doesn't update on my own badge > > **Possible cause:** `Feddy.onUnreadCountChanged` was assigned after the count changed, so the first update was missed. > > **Solution:** Assign it right after `init`. The widget checks every 45 seconds and whenever the tab becomes visible, and calls your function each time the number changes. > > Also keep in mind that only opening a conversation clears its count. Opening Feddy doesn't. > [accordion] The widget is in the wrong language > > **Possible cause:** The widget follows the browser's language and falls back to English for languages it doesn't ship. > > **Solution:** Pass `locale` to `init` to force one, for example `'ja'` or `'zh-TW'`. Text you wrote in the dashboard, such as topics and the fallback message, uses your translation when one exists for the user's language and your default text otherwise. ## Inbox and email > [accordion] Messages don't arrive in the inbox > > **Possible cause:** The project ID in your code doesn't match the one under **Settings → Install**. A wrong ID fails silently for the user. > > **Another possible cause:** You're looking at a different project. Check the project picker in the dashboard sidebar. > > **Another possible cause:** A development build is still pointed at a local server through `apiUrl` (web) or `apiURL` (iOS). > > **Solution:** Compare the IDs character by character, and make sure production builds use the default server. > [accordion] Attributes are missing from a contact > > **Possible cause:** Anything over the limits is dropped without an error: more than 20 keys, a key over 40 characters, a value over 255 characters, nested objects or lists, or more than 8 KB in total. On iOS, a value that isn't a `String`, `Bool`, a number, or a `Date` is dropped on the device. > > **Another possible cause:** Each `identify` call replaces the whole set. A key you left out of the latest call is gone. > > **Solution:** Keep attributes flat and within the limits, and send every attribute you want to keep on each call. See [Identify users](/docs/guides/identify-users). > [accordion] Users are never asked for an email > > **Possible cause:** The widget only asks when your project has a verified email domain, and only once per device. > > **Solution:** Verify a domain under **Settings → Email domain**, or pass `email` in `identify` so there's nothing to ask for. > [accordion] Replies aren't emailed to users > > **Possible cause:** All three must be true: Feddy has an email for the contact, your project's email domain is verified, and your plan includes email to users. > > **Solution:** Check each one under **Settings → Email domain** and the contact's **Details** panel. Replies still appear in the app either way. > [accordion] Milo answered something it shouldn't have > > **Possible cause:** One of your answers matched more broadly than you meant it to, or a source says something you didn't mean users to read. Milo only ever uses your answers, your sources, and your **About your app** text, so the cause is in there. > > **Solution:** Open the conversation: **Based on** under the reply links the page it used. Fix the answer or the page, or set the source to **Team only**, then ask the same question on the phone under **Settings → Milo AI → Settings** to confirm the change before users see it. ## Still stuck? Use the chat button in the bottom-right corner of your dashboard. It's Feddy's own widget, going to the same kind of inbox you're setting up, and a real person answers.