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.
Add the SDK dependency to your build.gradle.kts
Initialize the SDK in your Application.onCreate()
Create a reader and start collecting payments
Installation
Add the DIGIHCE Maven repository and SDK dependency:
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://maven.hce.vas.et/releases")
credentials {
username = "token"
password = providers.gradleProperty("DIGIHCE_TOKEN").get()
}
}
}
}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:
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:
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:
// 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)
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)
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
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.
curl https://api.hce.vas.et/v1/pg/transactions?status=settled&per_page=50 -H "X-API-Key: digi_live_..."| Endpoint | Returns |
|---|---|
| GET /v1/pg/me | The gateway and environment the key belongs to, and its payer visibility setting. |
| GET /v1/pg/transactions | This 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/merchants | The 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. 401means the key is missing, revoked or not a server key.402means 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{
"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-256 | sha256=<hex>: HMAC-SHA256 over <timestamp>.<raw body>. Use this one. |
| X-DigiHCE-Timestamp | ISO 8601 time of this attempt. Reject requests more than 5 minutes off. |
| X-DigiHCE-Signature | sha256=<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.
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
idstays 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-EventandX-DigiHCE-Deliveryrepeat the event and id for routing before you parse.
Security Architecture
The SDK implements 8 layers of security:
API Key Gating
Session tokens via POST /sdk/activate
Server-Signed Transactions
ECDSA P-256 signed transaction tokens
Encrypted NFC Payloads
AES-256-GCM with per-transaction keys
USSD 2FA
Configurable per-MSISDN with amount thresholds
License Validation
Periodic checks every 6 hours
Certificate Pinning
SHA-256 TLS pin with backup rotation
SDK Obfuscation
ProGuard with string encryption
Device Integrity
Device fingerprint bound to session
Error Handling
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