Home
/
Blog
/
iMessage API in Kotlin
April 5, 2026
9 min read
Nikita Jerschow

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

Ready to send iMessages from Android?

Free sandbox, no credit card required. Get API keys in minutes.

Get API Access