Skip to main content

Kotlin SDK (server-side)

This SDK holds a secret key — do not ship it in an Android app

Every call in this guide authenticates with your secret API key (zy_live_* / zy_test_*). A secret key embedded in an APK is extractable by anyone who downloads it — including one passed through BuildConfig, which still lands in the compiled artifact. It can send on your account, read your subscribers, and manage your configuration.

Use this SDK from a JVM backend, or from the server your Android app talks to. Your app calls your backend; your backend calls Zyphr. The snippets below assume that shape.

For the end-user auth flows that are safe to call from an app, use the client-safe ZyphrClient with a publishable application key (za_pub_*) instead. It ships in @zyphr-dev/node-sdk and covers browser and React Native today — there is no publishable-key Kotlin surface, so a native Android app needs a backend proxy.

The official Zyphr Kotlin SDK for JVM backends, built with OkHttp4 and Kotlin coroutines. Auto-generated from the OpenAPI specification.

Installation

Gradle (Kotlin DSL)

dependencies {
implementation("dev.zyphr:zyphr-sdk:0.1.0")
}

Gradle (Groovy)

implementation 'dev.zyphr:zyphr-sdk:0.1.0'

Configuration

Read the key from the environment on your server. Never inline it in source, and never route it through BuildConfig — that ships it in the APK.

import dev.zyphr.sdk.infrastructure.ApiClient

val client = ApiClient(
apiKey = System.getenv("ZYPHR_API_KEY"),
basePath = "https://api.zyphr.dev/v1" // Optional custom endpoint
)

API Classes

The SDK exposes one API class per resource:

ClassDescription
EmailsApiSend and manage email messages
PushApiSend push notifications
SmsApiSend SMS text messages
InboxApiIn-app notification inbox
SubscribersApiManage subscriber profiles
DevicesApiRegister push notification devices
TemplatesApiCreate and manage templates
TopicsApiPub/sub topics
WebhooksApiConfigure webhook endpoints

All API methods are suspend functions and must be called from a coroutine scope.

Emails

Send Email

import dev.zyphr.sdk.api.EmailsApi
import dev.zyphr.sdk.models.SendEmailRequest

val emails = EmailsApi(client)

val result = emails.sendEmail(
SendEmailRequest(
to = "user@example.com",
from = "hello@yourapp.com",
subject = "Welcome!",
html = "<h1>Hello!</h1><p>Thanks for signing up.</p>",
text = "Hello! Thanks for signing up.",
replyTo = "support@yourapp.com",
tags = listOf("welcome", "onboarding"),
metadata = mapOf("userId" to "user_123")
)
)
println("Email sent: ${result.id}")

Send with Template

val result = emails.sendEmail(
SendEmailRequest(
to = "user@example.com",
templateId = "welcome-email",
templateData = mapOf(
"name" to "John",
"actionUrl" to "https://yourapp.com/activate"
)
)
)

Get Email Status

val email = emails.getEmail(id = "msg_abc123")
println("Status: ${email.status}")

List Emails

val response = emails.listEmails(page = 1, perPage = 25)
for (email in response.data) {
println("${email.id}: ${email.status}")
}

Push Notifications

Send Push

import dev.zyphr.sdk.api.PushApi
import dev.zyphr.sdk.models.SendPushRequest

val push = PushApi(client)

push.sendPush(
SendPushRequest(
userId = "user_123",
title = "New Message",
body = "You have a new message",
data = mapOf("messageId" to "msg_456")
)
)

Send to Specific Device

push.sendPush(
SendPushRequest(
deviceId = "device_abc",
title = "New Message",
body = "You have a new message"
)
)

Silent Push

push.sendPush(
SendPushRequest(
userId = "user_123",
contentAvailable = true,
data = mapOf("type" to "sync", "resource" to "messages")
)
)

Rich Notifications

push.sendPush(
SendPushRequest(
userId = "user_123",
title = "Photo shared",
body = "Jane shared a photo with you",
imageUrl = "https://yourapp.com/photo.jpg",
badge = 3,
sound = "default",
actionButtons = listOf(
ActionButton(id = "view", title = "View"),
ActionButton(id = "dismiss", title = "Dismiss")
)
)
)

SMS

import dev.zyphr.sdk.api.SmsApi
import dev.zyphr.sdk.models.SendSmsRequest

val sms = SmsApi(client)

val result = sms.sendSms(
SendSmsRequest(
to = "+14155551234",
body = "Your verification code is 123456"
)
)
println("SMS sent: ${result.id}")

In-App Inbox

import dev.zyphr.sdk.api.InboxApi
import dev.zyphr.sdk.models.SendInboxRequest

val inbox = InboxApi(client)

inbox.sendInboxNotification(
SendInboxRequest(
subscriberId = "user_123",
title = "New Comment",
body = "John commented on your post",
actionUrl = "/posts/123#comments"
)
)

Subscribers

Create Subscriber

import dev.zyphr.sdk.api.SubscribersApi
import dev.zyphr.sdk.models.CreateSubscriberRequest

val subscribers = SubscribersApi(client)

val subscriber = subscribers.createSubscriber(
CreateSubscriberRequest(
externalId = "user_123",
email = "user@example.com",
phone = "+14155551234",
name = "John Doe",
metadata = mapOf("plan" to "pro")
)
)

Get Subscriber

val subscriber = subscribers.getSubscriber(id = "user_123")
println("Name: ${subscriber.name}")

Update Subscriber

subscribers.updateSubscriber(
id = "user_123",
UpdateSubscriberRequest(
name = "Jane Doe",
metadata = mapOf("plan" to "enterprise")
)
)

Delete Subscriber

subscribers.deleteSubscriber(id = "user_123")

Device Management

Register Device

Device registration is server-side only

The device API authenticates with your secret key, which must never ship in an APK. Post the FCM token to your own backend and register from there, deriving userId from the authenticated session. See Device Management and the Android push guide.

The snippets below show the request shape for reference — run them from your server, not the app.

import dev.zyphr.sdk.api.DevicesApi
import dev.zyphr.sdk.models.RegisterDeviceRequest

val devices = DevicesApi(client)

devices.registerDevice(
RegisterDeviceRequest(
userId = "user_123",
platform = RegisterDeviceRequest.Platform.android,
token = fcmToken,
metadata = mapOf("appVersion" to appVersion) // forwarded from the app
)
)

List Devices

val response = devices.listDevices(userId = "user_123")
for (device in response.data) {
println("${device.platform}: ${device.lastActiveAt}")
}

Unregister Device

devices.deleteDevice(id = "device_abc")

Topics

Subscribe to Topic

import dev.zyphr.sdk.api.TopicsApi

val topics = TopicsApi(client)

topics.subscribe(
topicId = "promotions",
subscriberId = "user_123"
)

Unsubscribe from Topic

topics.unsubscribe(
topicId = "promotions",
subscriberId = "user_123"
)

Send to Topic

push.sendToTopic(
topicId = "promotions",
SendPushRequest(
title = "Flash Sale!",
body = "50% off everything today"
)
)

Error Handling

All API methods are suspend functions that throw exceptions on failure:

import dev.zyphr.sdk.infrastructure.ClientException
import dev.zyphr.sdk.infrastructure.ServerException

try {
emails.sendEmail(request)
} catch (e: ClientException) {
// 4xx errors
println("Client error (${e.statusCode}): ${e.message}")
when (e.statusCode) {
400 -> println("Bad request — check your parameters")
401 -> println("Unauthorized — check your API key")
404 -> println("Resource not found")
429 -> println("Rate limited — slow down")
}
} catch (e: ServerException) {
// 5xx errors
println("Server error (${e.statusCode}): ${e.message}")
} catch (e: Exception) {
// Network errors
println("Network error: ${e.message}")
}

Retry with Exponential Backoff

suspend fun <T> withRetry(maxAttempts: Int = 3, operation: suspend () -> T): T {
var lastException: Exception? = null

repeat(maxAttempts) { attempt ->
try {
return operation()
} catch (e: ClientException) {
if (e.statusCode == 429) {
lastException = e
delay(2.0.pow(attempt).toLong() * 1000)
} else {
throw e // Don't retry other 4xx errors
}
} catch (e: ServerException) {
lastException = e
delay(2.0.pow(attempt).toLong() * 1000)
}
}

throw lastException!!
}

// Usage
val result = withRetry {
emails.sendEmail(request)
}

Coroutines

All API methods are suspend functions, so call them from whatever coroutine scope your server framework gives you — a Ktor route handler, a Spring suspend controller method, or an explicit CoroutineScope in a worker:

// Ktor — your app POSTs here; this handler calls Zyphr
post("/notifications") {
val body = call.receive<SendNotificationBody>()
val userId = call.principal<UserPrincipal>()!!.id // from the session, not the client

push.sendPush(
SendPushRequest(
userId = userId,
title = body.title,
body = body.body
)
)

call.respond(HttpStatusCode.Accepted)
}

Derive userId from the authenticated session rather than trusting a value the app sent, so one user cannot send notifications as another.

Your Android app then calls your endpoint — with your own session token, not a Zyphr key — using whatever HTTP client the app already uses. The Zyphr SDK does not belong in the app's dependency graph.

Thread Safety

The SDK uses OkHttp's connection pool and is safe to share across threads and coroutine scopes. Create a single ApiClient instance on your server and reuse it:

// In your server's DI module — the key comes from the server environment.
// Do NOT source it from BuildConfig: that compiles the secret into the APK.
val zyphrClient = ApiClient(apiKey = System.getenv("ZYPHR_API_KEY"))

// Inject into your service/handler classes
val emailsApi = EmailsApi(zyphrClient)
val pushApi = PushApi(zyphrClient)

Requirements

  • JVM 8+ (any Kotlin server runtime)
  • Kotlin 1.8+
  • OkHttp 4.x

The artifact also targets Android API 21+, but see the warning at the top of this page — an Android target must not carry a secret key.

Next Steps