NaabigaPay · Kotlin / Ktor
QuickstartsDocs

DocsQuickstarts › Kotlin / Ktor

Backend

Intégrer NaabigaPay en Kotlin / Ktor

Un client Ktor crée le lien de paiement, une route webhook vérifie la signature HMAC-SHA256, puis marque la commande payée.

Deux morceaux côté serveur : l'un crée un lien de paiement quand un client veut payer, l'autre vérifie la signature du webhook une fois le paiement réussi. La clé secrète (sk_live_…) reste dans vos variables d'environnement et n'atteint jamais le navigateur.

1.Créer le lien

Votre front appelle votre route ; elle appelle POST/v1/payment-links avec la clé secrète via le client io.ktor.client, puis renvoie l'url de paiement.

Paiement.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.*

const val NPAY = "https://api.pay.naabiga.com/api/v1"
val SECRET: String = System.getenv("NPAY_SECRET") // sk_live_... : reste côté serveur

val client = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
}

// Appelle NaabigaPay avec la clé secrète, renvoie l'URL de paiement.
suspend fun creerLien(commande: String, montant: Int): String {
    val reponse = client.post("$NPAY/payment-links") {
        header(HttpHeaders.Authorization, "Bearer $SECRET")
        header("Idempotency-Key", "cmd-$commande") // évite un double lien si retry
        contentType(ContentType.Application.Json)
        setBody(buildJsonObject {
            put("amount", montant)                 // entier, en FCFA
            put("reference", commande)             // votre numéro de commande
            put("description", "Commande $commande")
            put("return_url", "https://monsite.com/merci")
            put("cancel_url", "https://monsite.com/panier")
            put("single_use", true)
        })
    }
    val corps = reponse.body<JsonObject>()
    return corps["url"]!!.jsonPrimitive.content // le front ouvre cette URL
}

La réponse contient url (ex. https://pay.naabiga.com/link/9f3c…) : la page où le client paie. Pour un montant libre, remplacez amount par amount_min, amount_max et une description.

2.Vérifier le webhook

NaabigaPay envoie POST à l'URL enregistrée dans le portail marchand, avec les en-têtes X-NPay-Event et X-NPay-Signature (HMAC-SHA256 du corps brut). Lisez le corps brut avec call.receiveText(), recalculez le HMAC hexadécimal avec javax.crypto.Mac, comparez en temps constant, et ne créditez qu'ensuite.

Webhook.kt
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

val WEBHOOK_SECRET: String = System.getenv("NPAY_WEBHOOK_SECRET")

// HMAC-SHA256 du corps brut, rendu en hexadécimal.
fun hmacHex(secret: String, corps: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
    return mac.doFinal(corps.toByteArray()).joinToString("") { "%02x".format(it) }
}

// Comparaison à temps constant (évite les attaques temporelles).
fun tempsConstant(a: String, b: String): Boolean {
    if (a.length != b.length) return false
    var diff = 0
    for (i in a.indices) diff = diff or (a[i].code xor b[i].code)
    return diff == 0
}

fun Route.webhookNaabiga() {
    post("/webhooks/naabiga") {
        val corps = call.receiveText() // corps BRUT : la signature porte dessus
        val signature = call.request.headers["X-NPay-Signature"] ?: ""
        val attendu = hmacHex(WEBHOOK_SECRET, corps)

        if (!tempsConstant(signature, attendu)) {
            call.respond(HttpStatusCode.BadRequest, "signature invalide")
            return@post
        }

        val data = Json.parseToJsonElement(corps).jsonObject["data"]!!.jsonObject
        if (call.request.headers["X-NPay-Event"] == "payment.succeeded") {
            // data.merchant_reference = VOTRE numéro de commande (la reference du lien).
            marquerCommandePayee(data["merchant_reference"]!!.jsonPrimitive.content)
        }
        call.respond(HttpStatusCode.OK)
    }
}

Astuce. Lisez le corps avec call.receiveText() avant tout décodage typé : la signature porte sur les octets bruts, un objet déjà désérialisé ne redonne pas la même chaîne.

Référence rapide

MéthodeEndpointRôle
POST/v1/payment-linksCréer un lien de paiement
GET/v1/payments/{reference}Statut d'un paiement

Base : https://api.pay.naabiga.com/api/v1 · Auth : Authorization: Bearer sk_…. Voir la documentation complète et le format des webhooks.