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

iMessage API in Swift — Send Blue Bubble Messages from iOS Apps (2026)

Apple does not expose a public API for sending iMessages programmatically inside an iOS app. The way around it is a REST API: your Swift code calls Sendblue, which routes the message through a real Apple account and delivers a genuine blue bubble. This guide shows exactly how to do it with URLSession, handle delivery receipts, and wire up a webhook for replies.

Why use an API instead of MessageUI?

MessageUI.MFMessageComposeViewController lets users send a message from within your app — but it opens the native Messages compose sheet and requires the user to tap Send. There is no way to send silently in the background, no delivery receipts, and no way to receive replies programmatically.

Sendblue's REST API solves all three problems. Your app makes a URLSession POST, and Sendblue handles everything: iMessage delivery, fallback to RCS/SMS for Android recipients, read receipts, and webhook callbacks when the contact replies.

  • No user interaction required — send from background tasks, App Intents, or server-side Swift
  • iMessage delivery receipts — know when the blue bubble was read
  • Automatic fallback — Android recipients get RCS or SMS, not a failed send
  • SOC 2 Type II and HIPAA compliant — safe for healthcare, finance, and enterprise apps

Get your API keys

Create a free account at dashboard.sendblue.com/company-signup. You will get two credentials immediately — no credit card required:

  • sb-api-key-id — your public key identifier
  • sb-api-secret-key — your secret key

Store these in your app's Secrets.xcconfig or in the iOS Keychain. Never hardcode them in source files. For server-side Swift (Vapor, Hummingbird) use environment variables.

Send your first iMessage with URLSession

No third-party SDK needed — the standard URLSession in Foundation handles everything:

import Foundation struct SendblueClient { let apiKeyID: String let apiSecretKey: String let baseURL = URL(string: "https://api.sendblue.co/api/send-message")! struct MessagePayload: Encodable { let number: String let content: String let send_style: String? let media_url: String? } struct MessageResponse: Decodable { let accountEmail: String? let content: String let isOutbound: Bool let status: String let sendStyle: String? let messageHandle: String let dateCreated: String let dateSent: String? let fromNumber: String let number: String let hasReaction: Bool let error: String? let errorCode: Int? } func sendMessage( to number: String, content: String, style: String? = nil, mediaURL: String? = nil ) async throws -> MessageResponse { var request = URLRequest(url: baseURL) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(apiKeyID, forHTTPHeaderField: "sb-api-key-id") request.setValue(apiSecretKey, forHTTPHeaderField: "sb-api-secret-key") let payload = MessagePayload( number: number, content: content, send_style: style, media_url: mediaURL ) request.httpBody = try JSONEncoder().encode(payload) let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(MessageResponse.self, from: data) } } // Usage let client = SendblueClient( apiKeyID: ProcessInfo.processInfo.environment["SENDBLUE_API_KEY_ID"]!, apiSecretKey: ProcessInfo.processInfo.environment["SENDBLUE_API_SECRET_KEY"]! ) let result = try await client.sendMessage( to: "+14155551234", content: "Hello from Swift!", style: "invisible" ) print("Sent: \(result.messageHandle)")

The send_style field is optional. Valid values include "invisible" (invisible ink effect), "celebration", "shooting_star", and more — these are the same iMessage screen effects your users know from the native app.

Send images and media

Add a media_url pointing to a publicly accessible file. Sendblue supports JPEG, PNG, GIF, MP4, and vCard (.vcf):

// Send a photo let result = try await client.sendMessage( to: "+14155551234", content: "Here is your receipt", mediaURL: "https://yourserver.com/receipts/receipt-123.jpg" ) // Send a contact card — no content needed let vcardResult = try await client.sendMessage( to: "+14155551234", content: nil, // optional with vCard mediaURL: "https://yourserver.com/contacts/sales-rep.vcf" )

The media file must be hosted on a public HTTPS URL. For user-generated content, upload to S3 or Cloudflare R2 first, then pass the signed URL to Sendblue.

Handle the API response

The response includes a status field you should inspect before assuming the message was delivered:

switch result.status { case "QUEUED": // Message accepted, delivery in progress print("Queued: \(result.messageHandle)") case "SENT": // Delivered to Apple's servers print("Sent at: \(result.dateSent ?? "unknown")") case "DELIVERED": // Confirmed delivered to the device print("Delivered") case "READ": // Recipient opened the message print("Read") case "FAILED": print("Failed: \(result.error ?? "unknown") code=\(result.errorCode ?? -1)") default: print("Unknown status: \(result.status)") }

For real-time delivery updates, use the webhook (below) rather than polling the API.

Receive replies with a webhook

Set your webhook URL in the Sendblue dashboard under Settings → Webhooks. Sendblue will POST to your endpoint whenever a message is received or a status changes.

Here is a minimal Vapor 4 handler that echoes received messages back:

import Vapor struct SendblueWebhookPayload: Content { let accountEmail: String? let content: String? let isOutbound: Bool let status: String let messageHandle: String let dateCreated: String let dateSent: String? let fromNumber: String let number: String let mediaUrl: String? let error: String? } func routes(_ app: Application) throws { app.post("webhook", "sendblue") { req async throws -> HTTPStatus in let payload = try req.content.decode(SendblueWebhookPayload.self) guard !payload.isOutbound, let text = payload.content else { return .ok } req.logger.info("iMessage from \(payload.fromNumber): \(text)") // Echo reply via Sendblue API let body: [String: String] = [ "number": payload.fromNumber, "content": "Got your message: \(text)" ] _ = try await req.client.post( "https://api.sendblue.co/api/send-message", headers: [ "sb-api-key-id": Environment.get("SENDBLUE_API_KEY_ID")!, "sb-api-secret-key": Environment.get("SENDBLUE_API_SECRET_KEY")!, "content-type": "application/json" ], content: body ) return .ok } }

For local development, expose your server with ngrok http 8080 and paste the HTTPS URL into the dashboard.

Full send + receive example (SwiftUI)

A complete SwiftUI view that sends a message and displays the status:

import SwiftUI @MainActor class MessagingViewModel: ObservableObject { @Published var status: String = "idle" @Published var messageHandle: String = "" private let client = SendblueClient( apiKeyID: Bundle.main.infoDictionary!["SENDBLUE_API_KEY_ID"] as! String, apiSecretKey: Bundle.main.infoDictionary!["SENDBLUE_API_SECRET_KEY"] as! String ) func sendWelcomeMessage(to phoneNumber: String) async { status = "sending..." do { let result = try await client.sendMessage( to: phoneNumber, content: "Welcome to our app! Reply anytime.", style: "celebration" ) messageHandle = result.messageHandle status = result.status } catch { status = "error: \(error.localizedDescription)" } } } struct ContentView: View { @StateObject private var vm = MessagingViewModel() @State private var phone = "+14155551234" var body: some View { VStack(spacing: 16) { TextField("Phone number", text: $phone) .textFieldStyle(.roundedBorder) .keyboardType(.phonePad) Button("Send iMessage") { Task { await vm.sendWelcomeMessage(to: phone) } } .buttonStyle(.borderedProminent) Text("Status: \(vm.status)") .foregroundStyle(.secondary) if !vm.messageHandle.isEmpty { Text("Handle: \(vm.messageHandle)") .font(.caption) .foregroundStyle(.secondary) } } .padding() } }

Next steps

Ready to send blue bubbles from Swift?

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

Get API Access