> ## Documentation Index
> Fetch the complete documentation index at: https://feddy.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](https://feddy.app/docs/concepts/contacts.md)
>
> What Feddy knows about a person, and where to see it in the dashboard.

> [card] [Team](https://feddy.app/docs/concepts/team.md)
>
> Verify an email domain so replies reach users by email.

> [card] [iOS SDK reference](https://feddy.app/docs/reference/ios.md)
>
> The full `identify` signature for Swift.

> [card] [Web SDK reference](https://feddy.app/docs/reference/web.md)
>
> The full `identify` options for the web widget.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.