# 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 `