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

# Mobile push (Android)

> Send push notifications to your Android app through your own Firebase project.

Tranthor sends push to your Android app through Firebase Cloud Messaging (FCM), using a key from your own Firebase project. Your app runs the Tranthor Android SDK, which tells Tranthor which phones belong to which customer and reports what happens to each push.

> [!NOTE]
>
> Mobile push is available per workspace. Ask us at [support@tranthor.com](mailto:support@tranthor.com) to turn it on for yours. iOS is not available yet.

## Before you start

You need:

* A Firebase project with your Android app added to it (the app's package name, for example `com.example.shop`).
* The Tranthor Android SDK in your app, with its push module (below).
* An Admin or Workspace manager role in Tranthor, to add the key.

## 1. Create a Firebase service-account key

1. Open the [Firebase console](https://console.firebase.google.com/) and select the project your app uses.
2. Go to **Project settings**, then **Service accounts**.
3. Select **Generate new private key**. Firebase downloads a JSON file. If Firebase says key creation is not allowed, see [Troubleshooting](#troubleshooting).

Keep the file private. Anyone with it can send push to your app's users. Do not commit it, email it or paste it into a chat.

## 2. Add the key in Tranthor

1. In Tranthor, go to **Settings**, **Mobile**, **Push**.
2. On the **Android (FCM)** card, select **Add key**.
3. Enter your app's application id (its package name) and choose the JSON file, or paste its contents.
4. Select **Save**.

Tranthor checks the key with Google before it saves it. If Google refuses the key, nothing is saved and the dialog says why. The key stays on the Tranthor server and is never shown again; to change it, select **Replace key**.

Once the key is saved, the card shows **Healthy**. Use **Send test** to send a test push to one phone of a customer: choose the customer, then one of their phones. A removed phone is dimmed and can't be picked. A test push is labelled as a test and never counts in your results.

## 3. Install the SDK and bind push

Add the `tranthor-push` module next to `tranthor-sdk`. Your app adds `com.google.firebase:firebase-messaging` itself and keeps its own Firebase configuration (`google-services.json` or manual `FirebaseOptions`). The app never holds the server key.

> [!NOTE]
>
> The Android SDK artifacts are not on Maven yet. Contact us at [support@tranthor.com](mailto:support@tranthor.com) for the build to add to your project.

Bind push to the SDK once, in `Application.onCreate`, after the SDK is configured:

```kotlin theme={null}
val push = TranthorPush.bind(
    context,
    PushConfiguration(applicationId = BuildConfig.APPLICATION_ID),
    sdk,
)
```

Keep `push` for the life of the app. Forward every FCM token to it: the token from `FirebaseMessaging.getInstance().token` at launch and every `onNewToken` callback.

The simplest way is the ready-made messaging service, which forwards tokens, shows each Tranthor push and reports that it arrived:

```kotlin theme={null}
class MyMessagingService : TranthorMessagingService() {
    override suspend fun tranthorPush(): TranthorPush? =
        (application as MyApp).push.await()

    override fun notificationStyle() = TranthorNotificationStyle(
        smallIcon = R.drawable.ic_notification,
        defaultChannelId = "tranthor",
        defaultChannelName = "Updates",
        launchIntent = Intent(this, MainActivity::class.java),
        dismissReceiver = MyDismissReceiver::class.java,
        images = RichImageConfiguration(setOf("images.example.com")),
    )

    override fun onOtherMessage(message: RemoteMessage) = otherSdk.handle(message)
}
```

`images` lets the notification show the template's image. The app downloads it only when the image URL starts with `https://` and its host is exactly one of the hosts you list there, and by default only a JPG, PNG or GIF up to 5 MB. Otherwise the notification shows without the image.

In the push editor, the **Image** card takes the image. **Upload** saves a JPG, PNG or GIF up to 5 MB to your workspace's assets and uses it; a WebP or a bigger file is refused, here and when you upload in **Choose from assets**. **Choose from assets** picks one you already have. Both come from your workspace's asset storage, so add that host to the list too. To read the host, open **···** › **Paste a link** on the card: the field shows the image's full link. **Paste a link** also takes any other image link, with variables. A link without variables that does not start with `https://` is refused and not saved. A link with variables is refused the same way when it starts with a scheme other than `https://`, such as `http://` or `ftp://`; otherwise, as when it starts with a variable, it is checked by what it fills in as for the preview customer. The AI chat refuses the same links.

Messages that are not from Tranthor go to `onOtherMessage`, so another push SDK keeps working. To report opens, call `TranthorNotifications.interactionFrom(context, intent)` in your activity's `onCreate` and `onNewIntent` and pass the result to `push.handle(...)`. To report dismissals, do the same in the dismiss receiver.

On Android 13 and later, posting notifications needs the `POST_NOTIFICATIONS` permission. The SDK never asks for it on its own; ask at a moment that suits your app, then call `push.refreshPermission()`.

Identity and consent follow the SDK: `identify`, `reset`, `optOut` and `optIn` on `TranthorSDK` keep the phone's registration in step.

## Events the SDK sends

The SDK sends these as track events through its queue, under the customer's user id (or anonymous id when there is none). Tranthor reserves these names.

| Event | When | Properties |
| - | - | - |
| `Installation Registered` | The app has a token and an identity. Again when the token, identity, notification permission, app version or SDK version changes, and once per app start. | `platform` (`"android"`), `appId`, `token`, `permissionState` (`"granted"`, `"denied"` or `"unknown"`), `appVersion`, `osVersion`, `sdkVersion` |
| `Installation Removed` | The token was replaced (for the old token), on `reset()`, and on `optOut()`. | `platform`, `appId`, `token` |
| `Push Received` | A Tranthor push reached the app. | `deliveryId` |
| `Push Opened` | The customer tapped the notification. | `deliveryId` |
| `Push Dismissed` | The customer swiped it away. | `deliveryId` |

Each phone of a customer is an Installation. A customer can have several.

## What Tranthor sends to the phone

Tranthor sends data-only FCM messages, with no `notification` block, so your app always builds the notification itself and can report what happens to it:

```json theme={null}
{
  "message": {
    "token": "<fcm-token>",
    "data": {
      "tranthor": "{\"version\":1,\"deliveryId\":\"8c0f2a...\",\"imageUrl\":\"https://images.example.com/a.png\"}",
      "title": "Your order shipped",
      "body": "It arrives Friday.",
      "channelId": "orders",
      "url": "shop://orders/42",
      "orderId": "42"
    },
    "android": { "priority": "HIGH", "ttl": "2419200s" }
  }
}
```

* `tranthor` is a JSON string. It holds `version` (always 1), the `deliveryId` the SDK reports back, the `imageUrl` when the template has one, and `"test": true` for a test push.
* `title`, `body`, `channelId` (the template's **Channel id**, under **Advanced**) and `url` (the template's **When tapped, open** link) are left out when empty.
* The template's data entries are added as their own keys. A data key named `tranthor`, `title`, `body`, `channelId`, `url`, `from` or `message_type`, or starting with `google.` or `gcm.`, is not sent.
* Title, body, image URL, the **When tapped, open** link and data values can use Liquid, like other templates.
* FCM limits a message's data to 4096 bytes. A longer message is not sent, and the send fails with a reason.
* FCM keeps an undelivered message for up to 28 days.

## Which phones receive a push

Tranthor never sends a push only to find out whether a phone is still there. A phone stays reachable until there is real evidence that it is not:

* **Removed at once:** when Google says the token is no longer registered, when the app sends `Installation Removed`, or when the phone has not registered for 90 days. The SDK registers on every app start, so a phone in use never reaches 90 days.
* **Paused after two rejections:** when Google rejects a phone's token twice in a row. A paused phone comes back when the app gets a new token from Firebase.
* **Notification permission:** a phone the SDK reports as `"denied"` is skipped. `"unknown"` still receives push.

Each message goes to at most 10 of a customer's phones, the most recently seen first. A customer with no reachable phone is skipped, not failed.

If Google is unavailable and no phone accepted a message, Tranthor tries again, up to 5 attempts in all over about 50 minutes, then records the message as failed.

If Google refuses your key, Tranthor stops push to that app, marks the key **Unhealthy** on **Settings**, **Mobile**, **Push**, and shows it in **Needs you**. No phone is removed because of a bad key. While the key is Unhealthy, send a test push from the Settings page or a push template's editor: if Google accepts it, the key is **Healthy** again and push resumes. Otherwise replace the key on the Settings page.

## Results

Push results show on each journey step, on the journey's Stats tab and on a broadcast's results:

* **Sent:** Google accepted the message for at least one of the customer's phones.
* **Delivered:** the app on the phone reported `Push Received`. This is app-reported: a phone that was off, or an app that did not run its messaging service, reports nothing, so Delivered can be lower than what reached phones.
* **Opened:** the customer tapped the notification (`Push Opened`).
* **Failed:** no phone accepted the message. The Stats tab and a broadcast's results list failures by reason, in words.
* **Not sent:** a customer with no phone that can get push from the app is skipped, shown as **No active Installation** under **Push not sent**. A skip is neither sent nor failed.

Each message counts once, however many phones it reached. Dismissals are stored but not shown as a result.

The SDK's reports count only when their `deliveryId` matches a push Tranthor sent to that same customer. Anything else is ignored. Test pushes never count.

## Troubleshooting

### Firebase says "Key creation is not allowed on this service account"

Your Google Cloud organization blocks new service-account keys. It does this with the organization policy `iam.disableServiceAccountKeyCreation`. Google turns this policy on by default for organizations created after mid-2024.

You can turn it off for your Firebase project only. The rest of your organization keeps it.

1. Make sure you have the **Organization Policy Administrator** role. If you do not, ask your Google Cloud admin.
2. In the [Google Cloud console](https://console.cloud.google.com/), go to **IAM & Admin**, then **Organization Policies**.
3. In the project picker at the top, select your Firebase project. Do not select the organization.
4. Open **Disable service account key creation**. There are two entries with this name, a legacy one and a managed one. Open the one that shows **Active**.
5. Select **Manage policy**, then **Override parent's policy**.
6. Add a rule and set **Enforcement** to **Off**. Then select **Set policy**.
7. Wait up to 15 minutes for the change to take effect.
8. In Firebase, select **Generate new private key** again.

## Current limits

* Android only. iOS (APNs) is not available yet. The editor already lets you prepare iOS: under **Settings › Message**, **Different for each** gives the iOS version its own title, subtitle, body and image, and **Advanced** holds the iOS **Sound** and **Time sensitive** options. These are saved with the template and shown in the iOS preview, but nothing is sent to iPhones, and Android phones get the same push as before.
* Turned on per workspace. Ask us to turn it on for yours.
* The Android SDK artifacts are not on Maven yet.
* Push is sent by people in the dashboard. In the push template editor, the AI chat can write and change the template's draft. It writes the shared (Android) message only, and does not see or edit the iOS version. It cannot publish the template, start a journey or send push.
* The Supervisor and connected assistants such as Claude and ChatGPT can read push templates, journeys and results, but cannot create, start or send push yet.
* Reachable audience and Blast radius counts do not include push yet.


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