v1.0.0

DIGIHCE SDK Documentation

Complete reference for integrating NFC HCE payments into your Android application.

Quick Start

Get NFC payments running in your app in under 5 minutes.

1

Add the SDK dependency to your build.gradle.kts

2

Initialize the SDK in your Application.onCreate()

3

Create a reader and start collecting payments

Installation

Add the DIGIHCE Maven repository and SDK dependency:

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven {
            url = uri("https://maven.hce.vas.et/releases")
            credentials {
                username = "token"
                password = providers.gradleProperty("DIGIHCE_TOKEN").get()
            }
        }
    }
}
app/build.gradle.kts
dependencies {
    implementation("com.digihce:sdk:1.0.0")
}

Alternative: AAR file

If using the AAR distribution, place the file in app/libs/ and add implementation(files("libs/digihcesdk.aar")) to your dependencies.

Initialization

Initialize the SDK in your Application class with your API key:

SampleApp.kt
class SampleApp : Application() {
    override fun onCreate() {
        super.onCreate()

        DigiHceSdk.init(applicationContext) {
            apiKey = "digi_live_xxxxxxxxxxxx"
            aid = "F0746C6272507961"        // optional, uses default
            environment = Environment.SANDBOX // or PRODUCTION
            cardIdentity = CardIdentity(
                name = "User A",
                accountId = "ACC-12345",
                msisdn = "+251912345678"     // optional, for USSD 2FA
            )
            cardEnabled = true
        }
    }
}

What happens during init()

  • Validates API key with backend via POST /sdk/activate
  • Receives JWT session token (24h TTL) and encryption key material
  • Stores credentials in EncryptedSharedPreferences
  • Dynamically registers the AID for NFC payment routing

Collecting Payments

Use the reader API to accept NFC tap payments from payers:

MainActivity.kt
val reader = DigiHceSdk.createReader(activity)

// Start collecting — returns a Flow scoped to this session
val sessionFlow: Flow<DigiHceEvent> = reader.collect(
    amount = "100.00",
    currency = "ETB"
)

// Observe events
sessionFlow.collect { event ->
    when (event) {
        is DigiHceEvent.ChargeCompleted -> {
            showSuccess(event.result)
        }
        is DigiHceEvent.ChargePending2FA -> {
            showWaiting("Awaiting USSD confirmation...")
        }
        is DigiHceEvent.TransactionReversed -> {
            showReversed(event.reason)
        }
        is DigiHceEvent.Error -> {
            showError(event.error)
        }
    }
}

// Stop when done
reader.stop()

The reader is lifecycle-aware and automatically cleans up when the Activity is destroyed.

Sending Payments (Push)

Prepare a push payment that broadcasts via HCE when another device taps:

PushPayment.kt
// Prepare server-signed payment payload
val result: Result<TransactionResult> = DigiHceSdk.preparePayment(
    amount = "50.00",
    currency = "ETB"
)

when {
    result.isSuccess -> {
        // HCE now broadcasts the signed payload
        // On tap + CONFIRM → emits PaymentSent
        showReady("Tap to send payment")
    }
    result.isFailure -> {
        showError(result.exceptionOrNull())
    }
}

// Cancel if needed
DigiHceSdk.cancelPayment()

Events & Callbacks

Flow API (recommended)

EventObserver.kt
DigiHceSdk.events.collect { event ->
    when (event) {
        is DigiHceEvent.ChargeReceived -> { }
        is DigiHceEvent.ChargePending2FA -> { }
        is DigiHceEvent.ChargeCompleted -> { }
        is DigiHceEvent.PaymentSent -> { }
        is DigiHceEvent.PaymentReceived -> { }
        is DigiHceEvent.TransactionReversed -> { }
        is DigiHceEvent.Error -> { }
        is DigiHceEvent.SdkDeactivated -> { }
    }
}

Callback API (alternative)

CallbackAdapter.kt
DigiHceSdk.setListener(object : DigiHceListener {
    override fun onChargeReceived(result: TransactionResult) {}
    override fun onChargePending2FA(transactionId: String) {}
    override fun onChargeCompleted(result: TransactionResult) {}
    override fun onPaymentSent(result: TransactionResult) {}
    override fun onPaymentReceived(result: TransactionResult) {}
    override fun onTransactionReversed(id: String, reason: String) {}
    override fun onError(error: DigiHceError) {}
    override fun onSdkDeactivated(reason: String) {}
})

Data Models

Models.kt
data class CardIdentity(
    val name: String,
    val accountId: String,
    val msisdn: String? = null   // for USSD 2FA
)

data class TransactionResult(
    val transactionId: String,
    val status: TransactionStatus,
    val amount: String,
    val currency: String,
    val counterparty: String,
    val timestamp: Long,
    val serverReceipt: String?
)

enum class TransactionStatus {
    SETTLED,
    PENDING_2FA,
    REVERSED,
    FAILED,
    PENDING_SYNC     // offline payer, awaiting connectivity
}

Server API

Your own backend can read what your gateway's terminals have recorded. Use a server key, issued on the gateway's page in the dashboard. It goes in the X-API-Key header. Never ship a server key inside an app: the SDK uses a separate key for that.

Terminal
curl https://api.hce.vas.et/v1/pg/transactions?status=settled&per_page=50   -H "X-API-Key: digi_live_..."
EndpointReturns
GET /v1/pg/meThe gateway and environment the key belongs to, and its payer visibility setting.
GET /v1/pg/transactionsThis gateway's transactions, newest first. Filters: type, status, merchant_account_id, from, to, sort (created_at | amount), order, page, per_page.
GET /v1/pg/transactions/{id}One transaction. Another gateway's transaction answers 404.
GET /v1/pg/merchantsThe merchants your terminals have reported.
  • A key only sees its own environment: a digi_test_ key never returns production traffic.
  • Payer names and numbers appear according to the gateway's payer visibility setting, which you change on the gateway's page.
  • Limit: 120 requests a minute per key. Over the limit you get 429; wait and retry.
  • 401 means the key is missing, revoked or not a server key. 402 means the organization or gateway is suspended.

Webhooks

Add an https:// endpoint under Webhooks in the dashboard and choose its events. DIGIHCE then sends a POST with a JSON body each time one happens. An endpoint receives either every event in your organization, or only one payment gateway's.

transaction.authorizedtransaction.pin_requiredtransaction.pending_2fatransaction.settledtransaction.failedtransaction.reversedtransaction.expiredtransaction.flaggedtransaction.refundedreconciliation.completedcard.provisionedcard.revokedcard.uid_mismatch
Request body
{
  "id": "dlv_3f9a1c7e2b4d6a80",
  "event": "transaction.settled",
  "timestamp": "2026-11-01T09:30:12.418Z",
  "payment_gateway_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "data": {
    "transactionId": "txn_9c3e7b1a2f4d5e6c",
    "type": "charge",
    "amount": "250.00",
    "currency": "ETB",
    "receipt": "...",
    "settledAt": "2026-11-01T09:30:12.401Z",
    "payerAccountId": "...",
    "payerName": "...",
    "pinVerified": true,
    "pinMethod": "client_side_pin_v1"
  }
}

payment_gateway_id is the payment gateway the event came from, or nullfor your organization's own keys. Fields inside datavary by event, and are camelCase. Ignore fields you don't recognise; new ones may be added.

Verify the signature

Every request carries these headers:

X-DigiHCE-Signature-256sha256=<hex>: HMAC-SHA256 over <timestamp>.<raw body>. Use this one.
X-DigiHCE-TimestampISO 8601 time of this attempt. Reject requests more than 5 minutes off.
X-DigiHCE-Signaturesha256=<hex>: HMAC-SHA256 over the raw body only. Kept for existing receivers.

The HMAC key is the SHA-256 hex digest of your signing secret, not the secret itself. Compute it over the bytes you received, before parsing the JSON, and compare in constant time. The Test button sends the same headers, so you can check your code with it.

verify.js
const crypto = require("crypto");

// webhookSecret: the whsec_... value shown once when you added or rotated the endpoint.
function verifyDigiHce(rawBody, headers, webhookSecret) {
  const key = crypto.createHash("sha256").update(webhookSecret).digest("hex");
  const ts = headers["x-digihce-timestamp"];
  if (!ts || Math.abs(Date.now() - Date.parse(ts)) > 5 * 60 * 1000) return false;
  const expected =
    "sha256=" + crypto.createHmac("sha256", key).update(`${ts}.${rawBody}`).digest("hex");
  const got = headers["x-digihce-signature-256"] ?? "";
  return got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Lost the secret, or think it leaked? Use Rotate secret on the webhook. The old secret stops working on the next delivery, and deliveries your endpoint rejects meanwhile are retried.

  • Answer with any 2xx within 10 seconds. Do slow work after responding.
  • A failed delivery is tried 3 times in all, about 10 seconds and then 1 minute apart. The id stays the same, so use it to skip duplicates.
  • After 50 failures in a row the endpoint is disabled. Fix it, then re-enable it in the dashboard.
  • X-DigiHCE-Event and X-DigiHCE-Delivery repeat the event and id for routing before you parse.

Security Architecture

The SDK implements 8 layers of security:

1

API Key Gating

Session tokens via POST /sdk/activate

2

Server-Signed Transactions

ECDSA P-256 signed transaction tokens

3

Encrypted NFC Payloads

AES-256-GCM with per-transaction keys

4

USSD 2FA

Configurable per-MSISDN with amount thresholds

5

License Validation

Periodic checks every 6 hours

6

Certificate Pinning

SHA-256 TLS pin with backup rotation

7

SDK Obfuscation

ProGuard with string encryption

8

Device Integrity

Device fingerprint bound to session

Error Handling

Errors.kt
sealed class DigiHceError {
    data class AuthFailed(val message: String) : DigiHceError()
    data class NfcUnavailable(val message: String) : DigiHceError()
    data class TransactionFailed(
        val transactionId: String?,
        val message: String
    ) : DigiHceError()
    data class NetworkError(val message: String) : DigiHceError()
    data class LicenseExpired(val message: String) : DigiHceError()
    data class CardDisabled(val message: String) : DigiHceError()
}

APDU Status Words

90 00Success6A 82AID not found69 85Conditions not satisfied69 82Security not satisfied6F 01Offline limit exceeded6F 02Verification key expired6F 00General error