Send WhatsApp with Kotlin: REST API example (OkHttp and Android)
To send WhatsApp with Kotlin you POST with OkHttp to /api/v1/{instanceId}/send/text, with the x-api-key and x-api-secret headers and a JSON body with number and text. It fits a backend (Ktor, Spring) and tests. On Android the rule is different: the app should never hold the key and secret, because anyone can extract strings from an APK. The app calls your backend, and the backend calls ZapZap.
Published on · 3 min read
What you need before starting
A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication uses x-api-key and x-api-secret. Add okhttp to build.gradle.kts (com.squareup.okhttp3:okhttp).
Validate the instance with cURL before coding.
cURL
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"number":"5511999999999","text":"Olá! Teste de envio pela API."}'How to send a text message with Kotlin and OkHttp
The class below creates a single OkHttpClient with timeouts and sends the JSON. On Android, network calls cannot run on the main thread; here the method is suspend and uses Dispatchers.IO.
To build JSON safely, use kotlinx.serialization or org.json instead of concatenating text. The example uses org.json.JSONObject to avoid escaping mistakes.
Kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class ZapZapClient(
private val instancia: String,
private val key: String,
private val secret: String,
) {
private val http = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
private val json = "application/json".toMediaType()
suspend fun enviarTexto(numero: String, texto: String): String = post(
"/send/text",
JSONObject().put("number", numero).put("text", texto),
)
private suspend fun post(caminho: String, corpo: JSONObject): String =
withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("https://api.zapzapapi.com/api/v1/" + instancia + caminho)
.header("x-api-key", key)
.header("x-api-secret", secret)
.post(corpo.toString().toRequestBody(json))
.build()
http.newCall(req).execute().use { r ->
val texto = r.body?.string().orEmpty()
check(r.isSuccessful) { "ZapZap " + r.code + ": " + texto }
texto
}
}
}How to send an image, PDF and audio with Kotlin
The route is /send/media. The type field picks the kind (image, video, audio, ptt, document, sticker and others), file is the public URL, text the caption and docName the document name.
Kotlin
// dentro de ZapZapClient
suspend fun enviarPdf(numero: String, url: String, nome: String, legenda: String = ""): String = post(
"/send/media",
JSONObject()
.put("number", numero)
.put("type", "document")
.put("file", url)
.put("docName", nome)
.put("text", legenda),
)Why an Android app should call your backend
Everything inside an APK can be extracted. If the API key and secret are in the app, anyone can send messages through your number. The safe design is: the app calls a route of yours, authenticated with the user's login, and your server calls ZapZap with the credentials kept there.
The Ktor backend below exposes POST /avisos and forwards it to ZapZap. It is also the right place for your own rules: who can send, per-user limits and send logging.
Kotlin (Ktor)
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.*
data class Aviso(val numero: String, val texto: String)
fun Application.rotas(zap: ZapZapClient) {
routing {
post("/avisos") {
// aqui: autentique o usuário do app e aplique limites seus
val aviso = call.receive<Aviso>()
val resposta = runCatching { zap.enviarTexto(aviso.numero, aviso.texto) }
resposta.onSuccess { call.respond(HttpStatusCode.Accepted, it) }
.onFailure { call.respond(HttpStatusCode.BadGateway, "falha no envio") }
}
}
}How to receive replies and what to check before production
Receiving messages is always server side: set the URL in configure webhook, turn on the messages event and handle data.message (text, chatid, fromMe), ignoring fromMe true. To notify the app, use push (FCM) from your server. Signature and deduplication are in the webhooks in production guide.
Checklist: credentials only on the server; timeouts; status checks; retry only on 429 and 5xx; a per-user limit; and sending only to people who agreed to receive. Other languages: Java, Node.js, Python. All endpoints are in the API reference.
Quick summary
- OkHttp with coroutines and Dispatchers.IO handles the send; never on the main thread.
- The API key and secret never go inside the app: anything in the APK can be extracted.
- The app calls your backend, and the backend calls ZapZap.
- Receiving messages happens on the server, by webhook, and the app is notified by push.
Keep reading
Also read
Frequently asked questions
Can I call the API straight from the Android app?
Technically yes, but you should not. The key and secret would sit inside the APK and can be extracted. Call your backend, which holds the credentials.
Does it work with Retrofit or Ktor Client?
Yes. The API is REST: the same headers and the same JSON apply to any HTTP client.
What about Swift or iOS?
The same rule holds: the app calls your backend. On the server, any language in this series works, and the app only needs one route of yours.
Can using an unofficial API get my number banned?
It can. An unofficial API goes against WhatsApp's terms and the number can be limited or banned. Opt-in, a human pace, warm-up and a clean list reduce the risk, but there is no guarantee. For a sanctioned channel, use Meta's official API.
Test Kotlin on your instance
Create the account, connect a number and send the first message. R$15 per active number per month, no per-message fee.
