iMessage API in Kotlin — Send Blue Bubbles from Android Apps (2026)
Android apps cannot send iMessages natively — iMessage is Apple-only. But your Kotlin app can call Sendblue's REST API and deliver genuine blue bubble messages to any iPhone user. This guide covers OkHttp and Retrofit implementations, delivery tracking, and receiving replies via webhook.
How Android can trigger iMessage
iMessage is a closed Apple protocol. There is no Android SDK, no XMPP gateway, no open standard. The only way to send a genuine blue bubble from Android is to proxy through a server that runs macOS and has an Apple ID authenticated — which is exactly what Sendblue operates at scale.
Your Kotlin app makes a single HTTPS POST to https://api.sendblue.co/api/send-message. Sendblue's infrastructure handles Apple authentication, message routing, and delivery confirmation. You never touch Apple's internals.
Benefits for cross-platform teams:
- One API for both iOS and Android codebases
- iMessage for iPhone users, automatic RCS/SMS fallback for Android-to-Android
- 30–45% higher response rates than SMS for iPhone recipients
- SOC 2 Type II certified — safe for enterprise and regulated industries
Add dependencies
Add OkHttp and Gson to your build.gradle.kts:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.code.gson:gson:2.10.1")
// Optional: Retrofit for a typed interface
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}Also add the Internet permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />Send an iMessage with OkHttp
Use Kotlin coroutines with OkHttp's enqueue wrapped in a suspendCancellableCoroutine:
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object SendblueClient {
private val client = OkHttpClient()
private const val API_URL = "https://api.sendblue.co/api/send-message"
private const val API_KEY_ID = BuildConfig.SENDBLUE_API_KEY_ID
private const val API_SECRET_KEY = BuildConfig.SENDBLUE_API_SECRET_KEY
suspend fun sendMessage(
toNumber: String,
content: String,
sendStyle: String? = null,
mediaUrl: String? = null
): JSONObject = suspendCancellableCoroutine { cont ->
val json = JSONObject().apply {
put("number", toNumber)
put("content", content)
sendStyle?.let { put("send_style", it) }
mediaUrl?.let { put("media_url", it) }
}
val body = json.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(API_URL)
.post(body)
.addHeader("sb-api-key-id", API_KEY_ID)
.addHeader("sb-api-secret-key", API_SECRET_KEY)
.build()
val call = client.newCall(request)
cont.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string() ?: "{}"
cont.resume(JSONObject(body))
}
})
}
}
// Call from a ViewModel or coroutine scope:
viewModelScope.launch {
try {
val result = SendblueClient.sendMessage(
toNumber = "+14155551234",
content = "Hello from Android via iMessage!",
sendStyle = "invisible"
)
Log.d("Sendblue", "Status: ${result.getString("status")}")
} catch (e: Exception) {
Log.e("Sendblue", "Failed", e)
}
}Typed Retrofit interface
For larger apps, Retrofit gives you a cleaner, testable interface with Gson serialization:
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
// --- Data classes ---
data class SendMessageRequest(
val number: String,
val content: String,
val send_style: String? = null,
val media_url: String? = null
)
data class SendMessageResponse(
val accountEmail: String?,
val content: String,
val isOutbound: Boolean,
val status: String,
val messageHandle: String,
val dateCreated: String,
val dateSent: String?,
val fromNumber: String,
val number: String,
val error: String?,
val errorCode: Int?
)
// --- Retrofit service ---
interface SendblueService {
@Headers(
"sb-api-key-id: ${BuildConfig.SENDBLUE_API_KEY_ID}",
"sb-api-secret-key: ${BuildConfig.SENDBLUE_API_SECRET_KEY}"
)
@POST("api/send-message")
suspend fun sendMessage(@Body request: SendMessageRequest): SendMessageResponse
}
// --- Singleton ---
object SendblueRetrofit {
val service: SendblueService by lazy {
Retrofit.Builder()
.baseUrl("https://api.sendblue.co/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(SendblueService::class.java)
}
}
// --- Usage ---
viewModelScope.launch {
val response = SendblueRetrofit.service.sendMessage(
SendMessageRequest(
number = "+14155551234",
content = "Your order shipped!",
media_url = "https://yourserver.com/order/tracking.jpg"
)
)
Log.d("Sendblue", "Sent: ${response.messageHandle}, status: ${response.status}")
}Handle the JSON response
The status field tells you where the message is in the delivery pipeline:
when (response.status) {
"QUEUED" -> showToast("Message queued for delivery")
"SENT" -> showToast("Delivered to Apple servers")
"DELIVERED" -> showToast("Delivered to device")
"READ" -> showToast("Message read by recipient")
"FAILED" -> {
val code = response.errorCode ?: -1
val msg = response.error ?: "Unknown error"
showToast("Failed ($code): $msg")
}
else -> Log.w("Sendblue", "Unknown status: ${response.status}")
}For real-time status updates (especially READ receipts), set up a webhook rather than polling the send endpoint.
Receive replies via webhook
Replies from iPhone users arrive at your server as HTTP POST requests from Sendblue. Set the webhook URL in the Sendblue dashboard. Here is a minimal Ktor server handler:
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class SendblueWebhook(
val accountEmail: String? = null,
val content: String? = null,
val isOutbound: Boolean,
val status: String,
val messageHandle: String,
val fromNumber: String,
val number: String,
val mediaUrl: String? = null
)
fun Application.configureRouting() {
routing {
post("/webhook/sendblue") {
val payload = call.receive<SendblueWebhook>()
if (!payload.isOutbound && payload.content != null) {
println("Reply from ${payload.fromNumber}: ${payload.content}")
// Push FCM notification to Android app here
// or store in your database for polling
}
call.respond(mapOf("status" to "ok"))
}
}
}To push the reply to your Android app, use Firebase Cloud Messaging (FCM) from your Ktor server after processing the webhook.
Next steps
- API reference — Full endpoint documentation and request/response schemas
- Getting started guide — Sandbox setup and rate limits
- AI sales agent guide — Combine Sendblue with an LLM for automated iMessage outreach
- Swift version of this guide — For teams building iOS apps
Ready to send iMessages from Android?
Free sandbox, no credit card required. Get API keys in minutes.