KashierDevelopersKashier Developers
Accept payments

Android SDK

Accept cards, saved cards, and wallet payments in your native Android app

A native Kotlin SDK that drops a complete, Kashier-hosted payment experience into your Android app. Your app hands the SDK a session ID created by your backend; the SDK renders the payment sheet, collects card or wallet details, handles 3-D Secure, and returns a single typed outcome.

Your app never touches card numbers, CVVs, or Kashier API credentials.

FieldValue
Artifactio.kashier:kashier-android-sdk
Latest version1.0.0
DistributionMaven Central
AudienceMerchant developers integrating Kashier payments into native Android apps
Minimum Android API24

SDK overview

What it does

  • Renders the Kashier payment sheet as a self-contained Activity inside your app.
  • Supports card payments (new card and saved cards) and wallet payments.
  • Handles 3-D Secure in an in-SDK WebView, including OTP challenges.
  • Reconciles in-flight payments automatically, and reports a pending outcome rather than guessing when settlement is still running.
  • Ships English and Arabic UI with full right-to-left layout for Arabic.
  • Returns exactly one terminal outcome per launch, as a typed sealed result.

What it does not do

  • No Apple Pay. Apple Pay is iOS-only and is not part of this SDK.
  • No session creation. Sessions are created by your backend.
  • No merchant credentials in the app. The SDK reads a server-provided hash from the session; it performs no client-side cryptography.
  • No logging. The published AAR contains no logging interceptor and never writes request URLs, headers, bodies, hashes, card data, or customer data to logcat.

Payment method availability

The SDK renders card and wallet, filtered by the session's allowed-methods list. An empty allowed-methods list means all supported methods are shown. Your app cannot select the method — it is a server-side decision made when the session is created.

Kashier payment sheet on Android showing the store name, amount, a Wallet method tile, and an expanded Card section with an Add New Card button

The payment sheet your users see. Method tiles are driven entirely by the session.

Architecture at a glance

Your Activity
  │  KashierSDK.initialize(KashierConfig(mode, language))      // once per process
  │  registerForActivityResult(KashierPaymentContract())       // in onCreate
  │  launcher.launch(KashierPaymentRequest(sessionId))

Kashier payment Activity (internal, not exported)
  ├── GET session  →  payment sheet (card / wallet)
  ├── card    →  POST order  →  3-D Secure WebView  →  reconcile
  └── wallet  →  request-to-pay →  poll (max 5 min) →  reconcile

KashierResult  →  Succeeded | Failed | Pending | Cancelled | Unavailable

Requirements

RequirementValue
Minimum Android API24 (Android 7.0)
Compiled againstAndroid API 36
Java compatibility17 (source and target)
Kotlin (SDK build)2.1.20
Android Gradle Plugin (SDK build)8.11.1
Gradle (SDK build)8.14.3
Host Activity typeandroidx.activity.ComponentActivity or a subclass (e.g. AppCompatActivity)
NetworkInternet access (permission is merged in automatically)
BackendA server that creates Kashier payment sessions

Your app module must compile with Java 17 compatibility:

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

The versions above are the toolchain the SDK itself is built with. Newer Android Gradle Plugin and Kotlin versions that still support Java 17 and compileSdk 36 are expected to work.

What is a session ID?

A session ID identifies a payment your backend created with Kashier before the user reaches checkout. It encodes the amount, currency, order reference, allowed payment methods, customer details, and the server-computed payment hash.

The SDK takes the session ID as its only input. It never creates sessions and never needs your Kashier API credentials. Create sessions server-to-server using the Kashier merchant API, then pass the returned session ID to your app over your own authenticated channel.

Note

Backend integration is out of scope for this guide. See the Payment Sessions API documentation for backend details.

Installation and setup

Add the dependency

The SDK is published on Maven Central as io.kashier:kashier-android-sdk. Integration is a single dependency — no extra repository, no manual AAR handling.

mavenCentral() is already present in default Android Studio templates. If your project pins its repositories, make sure it is declared:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

Then add the dependency:

// app/build.gradle.kts
dependencies {
    implementation("io.kashier:kashier-android-sdk:1.0.0")

    // Required: the SDK's public API takes an androidx ComponentActivity, and the SDK
    // declares its androidx dependencies as `implementation` (they are not exported).
    implementation("androidx.activity:activity:1.9.3")
}

1.0.0 is the current public release. Pin an exact version — published versions are immutable.

Everything else the SDK needs at runtime — its Compose UI, networking, and coroutines dependencies — resolves transitively from Maven Central. The AAR also ships its own R8 / ProGuard rules, so there is nothing to add to your ProGuard configuration (see R8, ProGuard and security).

Android project setup

Permissions — nothing to declare

The SDK's manifest declares android.permission.INTERNET, which is merged into your app automatically. Nothing else is required.

The SDK deliberately does not request ACCESS_NETWORK_STATE. Its connectivity check is a DNS reachability probe, so your app is not forced to hold a network-state permission.

Activities — nothing to declare

The SDK's payment Activity is declared in the SDK manifest and merged in automatically. It is android:exported="false" with no intent filters — it can only be started through the SDK's own contract. Do not declare, reference, or attempt to start it yourself.

Compose is not required in your app

The SDK's UI is built with Jetpack Compose internally, but no Compose type appears on its public API. The Kashier demo app uses plain Android views. Your app can use views, Compose, or both.

Right-to-left

The SDK pins its own locale and layout direction from KashierConfig.language — Arabic renders full RTL regardless of the device locale, and English renders LTR regardless of an Arabic device. The SDK never mutates your app's configuration or windows.

You do not need android:supportsRtl="true" for the SDK's sheet to render correctly. Set it only if your own screens need RTL.

ProGuard / R8

Nothing to add. See R8, ProGuard and security.

Quick start

The smallest complete integration. It initializes the SDK, registers the result contract in onCreate, launches a payment, and handles every terminal outcome.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import io.kashier.sdk.KashierConfig
import io.kashier.sdk.KashierLanguage
import io.kashier.sdk.KashierMode
import io.kashier.sdk.KashierPaymentContract
import io.kashier.sdk.KashierPaymentRequest
import io.kashier.sdk.KashierResult
import io.kashier.sdk.KashierSDK
import io.kashier.sdk.KashierUnavailableReason

class CheckoutActivity : ComponentActivity() {

    private lateinit var paymentLauncher: ActivityResultLauncher<KashierPaymentRequest>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 1. Initialize once per process, before any launch.
        if (!KashierSDK.isInitialized()) {
            KashierSDK.initialize(
                KashierConfig(
                    mode = KashierMode.TEST,
                    language = KashierLanguage.EN,
                )
            )
        }

        // 2. Register in onCreate — the Activity Result API forbids later registration.
        paymentLauncher = registerForActivityResult(KashierPaymentContract()) { result ->
            when (result) {
                is KashierResult.Succeeded -> {
                    // Display-safe. Confirm with your backend before fulfilling the order.
                    showReceipt(result.result.orderId, result.result.transactionId)
                }

                is KashierResult.Failed -> {
                    // result.error.message is English; result.error.messageAr is Arabic.
                    showError(result.error.message)
                }

                is KashierResult.Pending -> {
                    // Terminal: no success or failure will follow this launch.
                    // result.pending.orderId is the authoritative reference.
                    showPending(result.pending.orderId, result.pending.message)
                }

                is KashierResult.Cancelled -> {
                    returnToCheckout()
                }

                is KashierResult.Unavailable -> {
                    // The outcome could not be determined — do NOT assume failure.
                    when (result.reason) {
                        KashierUnavailableReason.processDeath,
                        KashierUnavailableReason.malformedResult,
                        KashierUnavailableReason.configMissing,
                        -> reconcileWithBackend(result.requestId)
                    }
                }
            }
        }
    }

    // 3. Launch with a session ID your backend created.
    private fun startPayment(sessionId: String) {
        paymentLauncher.launch(KashierPaymentRequest(sessionId))
    }
}

Blank session IDs throw

KashierPaymentRequest throws IllegalArgumentException if sessionId is blank. Validate the value you got from your backend before constructing it, or use the compatibility wrapper, which reports a blank session ID as a validationFailed callback instead of throwing.

SDK configuration

KashierSDK.initialize(
    KashierConfig(
        mode = KashierMode.LIVE,
        language = KashierLanguage.AR,
    )
)

KashierConfig

PropertyTypeMeaning
modeKashierModeTEST or LIVE — selects the Kashier environment
languageKashierLanguageEN or AR — language and layout direction of the SDK payment UI

KashierMode

ValueAPI base URLPayment-processing base URL
KashierMode.TESThttps://test-api.kashier.iohttps://test-fep.kashier.io
KashierMode.LIVEhttps://api.kashier.iohttps://fep.kashier.io

Both URLs are readable as mode.baseUrl and mode.fepBaseUrl.

Initialization contract

These rules are enforced by the SDK — read them carefully:

  • Call initialize once, before any payment launch.
  • Calling initialize again with an equal config is a harmless no-op.
  • Calling initialize again with a different config throws IllegalStateException. Dynamic reconfiguration — for example toggling TESTLIVE, or switching language at runtime — is not supported.
  • Configuration is process-scoped and in-memory only. It does not survive process death.
  • KashierSDK.isInitialized() returns true once initialization succeeded.

The safest place to initialize is Application.onCreate() or a guarded call in your checkout Activity, as shown in the quick start.

If you launch a payment before initializing, the compatibility wrapper reports unknownError with the message "KashierSDK.initialize() must be called before startPayment().".

Launching a payment

The SDK offers two launch surfaces. Both must be registered in onCreate, before the host Activity reaches STARTED — this is an Activity Result API rule, and AndroidX throws IllegalStateException on late registration.

Typed, exhaustive, and lifecycle-safe. Use this for new integrations.

private lateinit var launcher: ActivityResultLauncher<KashierPaymentRequest>

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    launcher = registerForActivityResult(KashierPaymentContract()) { result: KashierResult ->
        handle(result)
    }
}

fun pay(sessionId: String) = launcher.launch(KashierPaymentRequest(sessionId))

On this path the result is typed-only: KashierPaymentResult.transactionData and KashierPaymentError.details are always null. Raw backend response maps never cross the Activity-Result boundary.

Compatibility wrapper (callback style)

A listener-based surface with callback semantics matching Kashier's other SDKs. Useful if you are porting an existing integration or prefer callbacks to a sealed result.

private lateinit var launcher: KashierLauncher

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    launcher = KashierSDK.register(
        this,
        { result -> showReceipt(result) },              // onSuccess
        { error -> showError(error.message) },           // onFailure
        { pending -> showPending(pending.orderId) },     // onPending (nullable)
    )
}

fun pay(sessionId: String) = launcher.start(sessionId)

launcher.start(sessionId) never throws. Failures arrive on the failure listener:

SituationCallback
SDK not initializedonFailure(unknownError)
Blank sessionIdonFailure(validationFailed, "sessionId must not be blank")
Another payment already runningonFailure(validationFailed, "payment already in progress")

Wrapper callback semantics

OutcomeWrapper behavior
SuccessonSuccess fires after the user taps Done on the success sheet
FailureonFailure fires after the failure sheet is dismissed
User cancellationonFailure(KashierErrorCode.userCancelled)
PendingonPending fires before the pending modal renders. The terminal result is not re-fired. If onPending is null, a pending payment produces zero callbacks
Undeterminable outcomeonFailure(unknownError, "Payment outcome unavailable (<reason>). Reconcile via your backend/webhook.")

At most one merchant callback fires per payment.

Difference between the two surfaces

On the canonical Activity Result API, Pending is always delivered — after the pending modal's Done — even when no pending listener exists.

Wrapper registration rules

  • A second register(...) on the same Activity instance is idempotent: it returns the existing launcher with the originally supplied listeners. To change listeners, register on a fresh Activity instance.
  • Registration is cleaned up automatically at ON_DESTROY. There is no unregister, and no callback fires after destruction.
  • A KashierLauncher must not be reused across Activity instances. Obtain a fresh one in each Activity's onCreate.
  • onPending may run while your Activity is STOPPED — it fires before the pending modal, while the SDK Activity is still in front. Treat it as a background callback and defer UI work behind your usual stopped-state guards.

Concurrency

The SDK holds a process-scoped single-payment lock. Launching a second payment while one is active is rejected with validationFailed and the message "payment already in progress" — it does not cancel or corrupt the running payment.

Handling payment results

KashierResult is a sealed interface with five variants. Every variant is terminal — a launch produces exactly one of them, and nothing follows.

Every variant carries a non-null requestId identifying the launch. Succeeded, Failed, and Pending always carry a real ID; Cancelled and Unavailable may carry KashierRequestId.UNKNOWN (the empty string) when identity could not be recovered.

VariantPayloadMeaningYour action
Succeededresult: KashierPaymentResultPayment completed successfullyConfirm server-side, then fulfil
Failederror: KashierPaymentErrorPayment failed or was rejectedShow error.message; allow a retry with a new session
Pendingpending: KashierPaymentPendingSettlement still running; no later success/failure for this launchShow "processing"; reconcile via backend/webhook
CancelledUser dismissed the sheet or pressed backReturn to checkout
Unavailablereason: KashierUnavailableReasonOutcome could not be determinedDo not assume failure — reconcile via backend/webhook

Payment Successful sheet with a green check icon and a summary listing Amount, Transaction ID and the masked card number, above a Done button

The success sheet. Succeeded reaches your app only after the user taps Done — the fields shown here map to KashierPaymentResult. The transaction ID is masked in this screenshot.

Which identifiers are authoritative

FieldUse
KashierPaymentResult.orderIdKashier order ID (system order ID) — the reference to reconcile against
KashierPaymentResult.orderReferenceYour own merchant order reference
KashierPaymentResult.transactionIdTransaction reference for support and receipts
KashierPaymentPending.orderIdAuthoritative for a pending payment
KashierPaymentPending.transactionIdMay be an empty string for a card pending — the reconcile response carries no transaction ID yet
KashierResult.requestIdCorrelates the outcome with the launch inside your app (SDK-local, not a Kashier identifier)

When backend reconciliation is required

Always treat your backend and Kashier webhooks as the source of truth before releasing goods. Reconciliation is mandatory for:

  • Pending — the SDK explicitly stopped waiting.
  • Unavailable — including after process death.
  • Any case where your app was killed before the result was delivered.

KashierPaymentResult fields

KashierCardInfo exposes brand, maskedNumber (e.g. 541674******7777), and wallet. It never carries a full PAN, CVV, or card token, so it is safe to show on receipts.

KashierPaymentPending fields

FieldTypeNotes
orderIdStringAuthoritative reference
transactionIdStringMay be empty
messageStringLocalized to the configured language
messageEnString?English message
messageArString?Arabic message

Card and saved cards

Shown when the session allows card.

New-card flow

  1. The user opens the card form and enters number, expiry, CVV, and cardholder name.
  2. The SDK validates locally before any network call: Number — exactly 16 digits and a valid Luhn checksum. Expiry — month 1–12, not in the past. CVV and cardholder name — required.
  3. Brand is detected from the number prefix and shown inline: Visa 4…; Mastercard 51–55 or 2221–2720; Meeza BIN range 507803–507960.
  4. The SDK posts the order. If the gateway requires 3-D Secure, the SDK opens its own WebView (see 3-D Secure); otherwise it goes straight to the result sheet.

Add Card sheet with card number, expiry and CVV fields, a cardholder name field, a Save this card for future payments checkbox, and a Pay button

The new-card form as it appears in KashierMode.LIVE for a session that permits card saving. In KashierMode.TEST the save-card checkbox is hidden and a "Use Testing Data" button is added above the form — see Testing and going live.

Invalid input is reported inline inside the SDK sheet and never reaches your callbacks. API errors that are not 3-D Secure handoffs are shown inline on the form so the user can correct and retry without losing the session.

Saving a card after payment

The "save card" checkbox appears only when all of these hold:

  • the session does not hide card saving, and
  • the session carries customer data, and
  • the SDK is not in TEST mode.

If the session forces card saving, the checkbox is pre-selected.

What your app sees

SituationResult
ApprovedSucceeded — after the user taps Done
Declined by the issuerFailed(paymentDeclined), with error.responseCode when the gateway supplies one
Authorization rejected (HTTP 403)Failed(authorizationFailed)
3-D Secure failedFailed(authenticationFailed)
Still settling after reconciliationPending
User dismissed the sheetCancelled

Reconciliation safety net

After a 3-D Secure challenge the SDK polls the order until it reaches a terminal state:

  • Post-OTP: 8 attempts — 5s, 5s, 5s, 5s, 10s, 10s, 10s, 10s (first at +5 s, ≈60 s total).
  • After cancel, or a stuck INITIATED order: 5 attempts — 0s, 5s, 10s, 15s, 30s.

If polling exhausts while the server still reports a non-terminal state, the SDK reports Pending. If every attempt errored, it reports a failure instead — the SDK never claims "pending" without a real server response.

Only one terminal result can ever be emitted; a late redirect and an in-flight reconcile cannot both fire a callback.

PCI scope

Card data is entered only inside the SDK's own UI, is held in memory for the duration of the request, and is never persisted, logged, or exposed to your app. Your app never sees a PAN, a CVV, or a card token.

Saved cards

Saved cards let a returning customer pay without re-entering their card number.

When saved cards are fetched

The SDK fetches saved cards only when all of these hold:

  • card is an allowed method for the session, and
  • the session has saved-card retrieval enabled, and
  • the session carries a non-empty customer reference.

Otherwise the sheet goes directly to the new-card form. If the fetch fails, the SDK degrades silently to the new-card form — your app sees nothing and no error is surfaced.

Presentation

  • With saved cards present, the card section renders as an accordion listing each card with its brand icon, masked number, and expiry.
  • Selecting a card reveals an inline CVV field on that row.
  • An "Add card" entry opens the standard new-card form.
  • While the fetch is in flight the section shows a loading state; if the customer has no saved cards, the section renders empty and the user proceeds with a new card.

Payment sheet with the card section expanded into an accordion showing a selected Visa card ending 7585 with an inline CVV field, a Mastercard ending 6802, and an Add New Card row

The saved-card accordion. Selecting a card reveals its inline CVV field; the card numbers are masked by the gateway and no card token is ever exposed.

Paying with a saved card

Paying with a saved card requires the CVV to be re-entered. It must be exactly 3 digits; otherwise the SDK reports invalidCardDetails ("Invalid CVV.") inline and no network call is made. From there the flow is identical to a new card, including 3-D Secure.

Saved-card accordion showing a red CVV Validation Error banner beneath the card list and a disabled Pay button

A CVV that is not exactly 3 digits is rejected inside the sheet. No network call is made and your app receives no callback — the user simply corrects the field.

Deleting a saved card

  • Swipe a saved-card row to reveal the delete action.
  • A confirmation dialog appears before anything is deleted.
  • A failed deletion shows a snackbar inside the SDK sheet and surfaces as cardDeletionFailed internally. Your app is not notified — the flow continues normally.

Security

Card tokens are never logged, never persisted by the SDK, and never appear in the SDK's accessibility semantics. Your app never receives a card token; the only card data it can see is the display-safe KashierCardInfo on a successful result.

Wallet payments

Shown when the session allows wallet.

Flow

  1. The user picks the wallet method and enters an Egyptian mobile number. The SDK validates the format 01[0125] followed by 8 digits (11 digits total) and keeps the Pay button disabled until it matches.

Wallet phone-number entry sheet showing the amount, a phone field hinting 01XXXXXXXXX, and a disabled Pay button

  1. The SDK sends one request-to-pay. This request is never retried automatically.
  2. The user approves the payment in their wallet app.
  3. The SDK shows a 3-step progress sheet and polls for the outcome on a fixed schedule.

Polling schedule

PhaseIntervalsFires at
Phase 112 × 10 s10 s → 120 s
Phase 230 s, 45 s, 60 s150 s, 195 s, 255 s
Hard cap5 minutes (300 s) from poll start

No request is ever armed past the cap. Longer phase-2 intervals are defined in the schedule but are unreachable under the 5-minute cap.

Controls on the waiting sheet

ControlWhen it appearsWhat it does
ReloadWhile pollingImmediately re-checks the payment status. Errors are shown inline on the sheet; polling continues either way
I have paidOnly after the 5-minute cap is reached with the payment still pendingEmits the terminal Pending result
CancelThroughoutWhile polling, returns to the phone-input step and aborts the attempt. At exhaustion, ends the flow as userCancelled

Wallet waiting sheet with a three-step progress indicator reading Notifying, Processing, Successful Payment, above a Reload Payment Status button and a Cancel action

The waiting sheet while polling. Note Reload Payment Status — there is no resend control.

There is no "resend" button. If the request-to-pay needs to be re-sent, the user cancels back to the phone-input step and starts again — which creates a new order.

No duplicate-charge prevention

The SDK makes no duplicate-charge prevention claim for repeated wallet attempts. Duplicate protection is a backend concern.

Exhaustion and reconciliation

When the 5-minute cap is reached with the payment still pending, the SDK stops polling and shows the "I have paid" button. Tapping it emits a terminal Pending result. Your app should then wait for the Kashier webhook or query your backend — pending.orderId is the authoritative reference.

What your app sees

SituationResult
Wallet approved and reconciledSucceeded
Wallet declined / reconcile failureFailed
Request-to-pay could not be initiatedFailed(walletInitFailed)
5-minute cap reached, user tapped "I have paid"Pending
User cancelledCancelled

3-D Secure

3-D Secure is handled entirely inside the SDK — your app has nothing to implement.

What the user sees

  1. A full-screen in-SDK WebView loads the issuer's authentication page.
  2. If the issuer requires a challenge, the OTP screen renders inside that WebView.
  3. On completion the SDK shows a verifying state while it confirms the outcome, then the result sheet.

Full-screen 3-D Secure WebView headed Verifying your payment, containing the issuer bank's Enter Your Code page with an empty one-time-password field and Submit, Resend Code and Cancel buttons

A 3-D Secure OTP challenge inside the SDK's WebView. The page content is served by the cardholder's bank — its layout and wording vary by issuer. Transaction identifiers are masked in this screenshot.

How completion is detected

The SDK recognises the end of the challenge from the merchant redirect, from terminal query parameters on the redirect URL, from gateway redirect URLs that require a reconcile, and from issuer page markers. Timers keep the experience responsive: an OTP loader after 10 s, a reconcile handoff after 20 s, a frictionless-verify fallback after 15 s, plus short DOM probes.

Cancel, timeout, retry

ActionBehavior
Cancel the challengeuserCancelled immediately, with no reconciliation
TimeoutFull-screen timeout sheet; paymentTimeout is reported only after the user taps Done
Try AgainRestarts the challenge and emits no callback to your app

Full-screen 3-D Secure timeout sheet reading Still confirming your payment, with guidance to check payment status shortly, above a Done button

The timeout sheet. paymentTimeout reaches your app only after the user taps Done.

Security

HTTP redirects are disabled at the network layer — any 3xx on an API call is treated as an error, not followed. Issuer-controlled content (URLs, console output, page text) is never logged.

Lifecycle and process death

Registration

Register in onCreate, before the Activity reaches STARTED. This applies to both launch surfaces. Late registration is rejected by AndroidX with IllegalStateException.

Activity vs Fragment

KashierSDK.register(...) requires a ComponentActivity. From a Fragment, either:

  • use the canonical contract — Fragment.registerForActivityResult(KashierPaymentContract()) works normally; or
  • call KashierSDK.register(requireActivity(), …) and keep the launcher's lifetime tied to that Activity.

Threading

Register and launch from the main thread. Result callbacks are delivered on the main thread by the Activity Result API. The onPending wrapper callback may arrive while your Activity is STOPPED — guard UI work accordingly.

Configuration changes and rotation

Rotation of the SDK's payment Activity is handled internally: the in-flight session and flow state are retained across recreation. Card-entry fields are intentionally not retained.

Your own Activity is recreated normally; because you register in onCreate, the launcher is re-established and the pending result is delivered to the new instance.

Process death

If Android kills your process mid-payment:

  • No automatic replay. The SDK never re-submits a payment.
  • No crash. Teardown is safe.
  • If AndroidX restored a pending result, it is delivered typed-only — raw maps are null.
  • If only the launch identity survived, you receive Unavailable(KashierUnavailableReason.processDeath). On the compatibility wrapper this surfaces as onFailure(unknownError); a not-yet-fired onPending may be lost entirely.
  • Otherwise nothing is delivered.

Reconcile after process death

In every process-death case, reconcile through your backend or Kashier webhooks. A payment may well have succeeded.

Multiple concurrent payments

Not supported by design. A second launch while one is active is rejected with validationFailed and the message "payment already in progress".

Malformed results

If the returned payload is present but corrupt or of an unknown version, you receive Unavailable(malformedResult). parseResult never throws.

Kotlin and Java integration

Kotlin

Use an exhaustive when so new result variants become compile errors rather than silent bugs:

private fun handle(result: KashierResult) = when (result) {
    is KashierResult.Succeeded   -> onPaid(result.result)
    is KashierResult.Failed      -> onFailed(result.error)
    is KashierResult.Pending     -> onPending(result.pending)
    is KashierResult.Cancelled   -> onCancelled()
    is KashierResult.Unavailable -> onUnavailable(result.reason, result.requestId)
}

Building an error with the SDK's built-in English/Arabic default messages:

val error = KashierPaymentError.fromCode(KashierErrorCode.networkError)
println(error.message)    // "A network error occurred. Please check your connection."
println(error.messageAr)  // "حدث خطأ في الشبكة. يرجى التحقق من اتصالك."

Reading the environment URLs, if you need them for diagnostics:

val api = KashierMode.LIVE.baseUrl      // https://api.kashier.io
val fep = KashierMode.LIVE.fepBaseUrl   // https://fep.kashier.io

Java

The public API is fully usable from Java. Entry points are @JvmStatic, model constructors are @JvmOverloads, and all three listeners are functional interfaces, so Java lambdas work.

package com.example.checkout;

import android.os.Bundle;

import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;

import io.kashier.sdk.KashierConfig;
import io.kashier.sdk.KashierLanguage;
import io.kashier.sdk.KashierLauncher;
import io.kashier.sdk.KashierMode;
import io.kashier.sdk.KashierPaymentContract;
import io.kashier.sdk.KashierPaymentRequest;
import io.kashier.sdk.KashierResult;
import io.kashier.sdk.KashierSDK;

public final class CheckoutActivity extends ComponentActivity {

    private ActivityResultLauncher<KashierPaymentRequest> launcher;
    private KashierLauncher wrapperLauncher;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!KashierSDK.isInitialized()) {
            KashierSDK.initialize(new KashierConfig(KashierMode.TEST, KashierLanguage.EN));
        }

        // 1) Canonical Activity Result API.
        launcher = registerForActivityResult(new KashierPaymentContract(), (KashierResult result) -> {
            if (result instanceof KashierResult.Succeeded) {
                String txId = ((KashierResult.Succeeded) result).getResult().getTransactionId();
                showReceipt(txId);
            } else if (result instanceof KashierResult.Failed) {
                showError(((KashierResult.Failed) result).getError().getMessage());
            } else if (result instanceof KashierResult.Pending) {
                showPending(((KashierResult.Pending) result).getPending().getOrderId());
            } else if (result instanceof KashierResult.Cancelled) {
                returnToCheckout();
            } else if (result instanceof KashierResult.Unavailable) {
                reconcileWithBackend(((KashierResult.Unavailable) result).getReason().name());
            }
        });

        // 2) Compatibility wrapper — onPending may be null, in which case a pending
        //    payment produces no callback at all.
        wrapperLauncher = KashierSDK.register(
                this,
                result -> showReceipt(result.getTransactionId()),
                error -> showError(error.getMessage()),
                pending -> showPending(pending.getOrderId()));
    }

    private void pay(String sessionId) {
        launcher.launch(new KashierPaymentRequest(sessionId));
        // or: wrapperLauncher.start(sessionId);
    }
}

Java notes:

  • KashierResult is a Kotlin sealed interface — use instanceof and cast; there is no exhaustiveness check.
  • KashierSDK.register takes all four parameters in Java; pass null for onPending if you do not need it.
  • KashierPaymentError.fromCode(...) is available as a static method, with overloads for the optional arguments.
  • new KashierPaymentRequest(sessionId) throws IllegalArgumentException on a blank session ID.

API reference

Everything below is the entire supported surface. Anything in io.kashier.sdk.internal is not API: it carries no compatibility guarantee and is renamed or removed by R8 in your app.

KashierSDK

MemberSignatureNotes
initializefun initialize(config: KashierConfig)Throws IllegalStateException if already initialized with a different config
isInitializedfun isInitialized(): Boolean
registerfun register(activity: ComponentActivity, onSuccess: KashierSuccessListener, onFailure: KashierFailureListener, onPending: KashierPendingListener?): KashierLauncherCall in onCreate

KashierConfig

data class KashierConfig(val mode: KashierMode, val language: KashierLanguage)

KashierMode

enum class KashierMode(val baseUrl: String, val fepBaseUrl: String) // TEST, LIVE

KashierLanguage

enum class KashierLanguage // EN, AR

KashierPaymentContract

class KashierPaymentContract : ActivityResultContract<KashierPaymentRequest, KashierResult>

parseResult never throws. Precedence:

  1. RESULT_CANCELED, null intent, or an absent result payload → Cancelled
  2. payload present but corrupt, of an unknown version, or missing requestIdUnavailable(malformedResult)
  3. well-formed payload → the typed outcome

KashierPaymentRequest

data class KashierPaymentRequest(val sessionId: String) : Parcelable

Throws IllegalArgumentException when sessionId is blank. toString() is redacted.

KashierResult

sealed interface KashierResult { val requestId: String }
VariantSignature
Succeededdata class Succeeded(requestId: String, result: KashierPaymentResult)
Faileddata class Failed(requestId: String, error: KashierPaymentError)
Pendingdata class Pending(requestId: String, pending: KashierPaymentPending)
Cancelleddata class Cancelled(requestId: String)
Unavailabledata class Unavailable(requestId: String, reason: KashierUnavailableReason)

KashierResult is intentionally not Parcelable.

KashierUnavailableReason

ValueMeaning
processDeathThe OS killed the process mid-flow; only the launch identity survived
malformedResultThe delivered payload was corrupt, of an unknown version, or missing its request ID
configMissingReserved. The current SDK never emits it — a missing-config relaunch is reported as processDeath

KashierRequestId

object KashierRequestId { const val UNKNOWN: String = "" }

KashierPaymentResult

data class KashierPaymentResult @JvmOverloads constructor(
    val sessionId: String,
    val status: String,
    val orderId: String? = null,
    val orderReference: String? = null,
    val transactionId: String? = null,
    val authorizationNumber: String? = null,
    val amount: Double? = null,
    val currency: String? = null,
    val card: KashierCardInfo? = null,
    val message: String? = null,
    val messageAr: String? = null,
    val transactionData: Map<String, Any?>? = null,
)

See Handling payment results for field semantics. toString() deliberately excludes transactionData.

KashierPaymentError

data class KashierPaymentError @JvmOverloads constructor(
    val code: KashierErrorCode,
    val message: String,
    val messageAr: String? = null,
    val responseCode: String? = null,
    val details: Any? = null,
) {
    companion object {
        @JvmStatic @JvmOverloads
        fun fromCode(
            code: KashierErrorCode,
            message: String? = null,
            messageAr: String? = null,
            responseCode: String? = null,
            details: Any? = null,
        ): KashierPaymentError
    }
}

fromCode fills in the SDK's built-in English/Arabic default message for the code. message is always non-null and English. toString() deliberately excludes details.

KashierPaymentPending

data class KashierPaymentPending @JvmOverloads constructor(
    val orderId: String,
    val transactionId: String,
    val message: String,
    val messageEn: String? = null,
    val messageAr: String? = null,
)

KashierCardInfo

data class KashierCardInfo @JvmOverloads constructor(
    val brand: String? = null,
    val maskedNumber: String? = null,
    val wallet: String? = null,
) : Parcelable

Display-safe: never carries a full PAN, CVV, or card token.

KashierErrorCode

See Error handling for all 18 values.

KashierLauncher

interface KashierLauncher { fun start(sessionId: String) }

Never throws; failures reach the registered failure listener.

Listeners

fun interface KashierSuccessListener { fun onSuccess(result: KashierPaymentResult) }
fun interface KashierFailureListener { fun onFailure(error: KashierPaymentError) }
fun interface KashierPendingListener { fun onPending(pending: KashierPaymentPending) }

Error handling

KashierPaymentError carries:

FieldMeaning
codeKashierErrorCode — machine-readable classification
messageHuman-readable English message (never null)
messageArArabic message, when available
responseCodeIssuer/processor response code (e.g. "57"), when available
detailsRaw error details. Always null on the Activity Result API. Sensitive

Error codes

Apple Pay codes on Android

The last four values exist only so the enum matches Kashier's other SDKs. No Android code path produces them. Handle them in an else branch; do not build UX around them.

Localized error copy

The SDK's own sheets are already localized by KashierConfig.language. For errors you surface yourself, use message (English) and messageAr (Arabic), or map code to your own strings — recommended if your app supports languages beyond English and Arabic.

Diagnostics

The SDK writes nothing to logcat, by design. To diagnose a payment, correlate on the server side using orderId / orderReference / transactionId from the result, and use responseCode for issuer declines.

R8, ProGuard and security

R8 / ProGuard

You do not need to add any rules.

The SDK ships its consumer rules inside the AAR. They keep exactly the public API — every class listed in API reference, plus the attributes consumers need (Signature, InnerClasses, EnclosingMethod, annotations) — and deliberately do not keep io.kashier.sdk.internal.*, so internal classes stay eligible for renaming and removal in your minified build.

Do not add rules such as -keep class io.kashier.sdk.** { *; }. Broad wildcards defeat the SDK's encapsulation and keep code that is meant to be stripped.

This is verified by an automated check in the SDK repository, which builds a minified consumer app and asserts that every public class survives under its original name while io.kashier.sdk.internal types do not. The single unavoidable exception is the manifest-declared payment Activity, since Android component names cannot be renamed.

Your responsibilities

  • Create sessions on your backend. Never create a payment session from the app.
  • Never embed Kashier API credentials or your merchant ID in the app. The SDK does not need them and never asks for them.
  • Reconcile server-side before fulfilling. Treat webhooks and your backend as the source of truth — especially for Pending and Unavailable.
  • Treat the session ID as a capability. It authorizes a payment. Do not log it, persist it in analytics, or embed it in URLs or crash reports.
  • Never log transactionData or details. They may contain the full raw backend response. They are provided in-process only, on the compatibility wrapper path, and are always null on the Activity Result API. The SDK's own toString() implementations already exclude them.
  • Do not collect card data yourself. The SDK's UI is the only place PAN and CVV are entered.

What the SDK guarantees

  • Card data is never persisted, never logged, and never stored in SDK fields — it exists only as request arguments in memory.
  • No logging interceptor exists in the published AAR; no URLs, headers, bodies, hashes, tokens, or customer data are ever written to logs.
  • The payment hash comes from the session, is forwarded verbatim, and is scoped to the specific Kashier host and paths that require it. There is no client-side cryptography.
  • HTTP redirects are disabled; any 3xx on an API call is an error.
  • The SDK's payment Activity is not exported and has no intent filters.
  • Saved state contains only the launch's request identifier — never card data, tokens, customer data, or response payloads.
  • Saved-card tokens never appear in the SDK's accessibility semantics.
  • KashierCardInfo is display-safe by construction.

Testing and going live

Test mode

KashierSDK.initialize(KashierConfig(KashierMode.TEST, KashierLanguage.EN))

Test mode points the SDK at https://test-api.kashier.io / https://test-fep.kashier.io and changes two UI behaviors on the card form:

  • A "Use Testing Data" button appears above the form and prefills it with card number 4508 7500 1574 1019, expiry 06/27, CVV 100, and name John Doe.
  • The "save card" checkbox is hidden (it is shown only in LIVE mode, and only when the session permits card saving and carries customer data).

These two differences are the quickest way to confirm which mode you are in: the card-form screenshot in Card and saved cards shows the LIVE appearance — checkbox present, no testing-data button.

Test sessions come from your Kashier test-mode merchant account. Obtain approving, declining, and 3-D Secure test instruments, and test wallet numbers, from your Kashier integration contact — they vary per merchant configuration.

What to exercise

PathHow
Card successSession allowing card; approving test card
Card declineDeclining test card → Failed(paymentDeclined)
Card validationSubmit invalid number / expiry / CVV → inline errors, no callback
3-D Secure successSession/card that triggers a challenge; enter the correct OTP
3-D Secure failureWrong OTP → challenge rejects; no crash
3-D Secure cancelCancel the challenge → Cancelled
Saved cardsSession with saved-card retrieval enabled and a customer reference
Saved-card deleteSwipe a row, confirm
Wallet successSession allowing wallet; approve in the wallet app
Wallet pendingDo not approve; wait out the 5-minute cap; tap "I have paid" → Pending
CancellationDismiss the sheet / press back → Cancelled
ConcurrencyLaunch twice quickly → second attempt validationFailed
RotationRotate mid-payment; the flow survives
Process deathEnable "Don't keep activities"; verify Unavailable(processDeath)
Localization + RTLSwitch to KashierLanguage.AR; verify Arabic copy and right-to-left layout

Demo app

Kashier provides a demo Android app that exercises the SDK end to end. It collects a session ID, lets you pick TEST/LIVE and EN/AR, and exposes both launch surfaces side by side so you can compare their result semantics against the same session — useful for seeing the expected behaviour of a path before you implement it, and for checking whether an issue is in the SDK or in your integration.

Request the demo build from your Kashier integration contact.

Going-live checklist

  • KashierMode.LIVE is used in release builds, and TEST never ships to production.
  • KashierSDK.initialize(...) is called exactly once, before any launch.
  • Both launch surfaces are registered in onCreate.
  • Every KashierResult variant is handled — including Pending, Cancelled, and Unavailable.
  • Backend/webhook reconciliation is live and is the source of truth for fulfilment.
  • No Kashier API credentials or merchant IDs exist anywhere in the app or its resources.
  • Session IDs are not logged, persisted, or sent to analytics/crash reporting.
  • transactionData and details are never logged or persisted.
  • No -keep class io.kashier.sdk.** wildcard was added to your ProGuard configuration.
  • Release build tested with R8 enabled, end to end.
  • Arabic locale and RTL layout verified if you ship KashierLanguage.AR.
  • Process-death behavior verified with "Don't keep activities".
  • Retry paths create a new session rather than reusing a consumed one.

Frequently asked questions

Can I choose which payment method the sheet shows?

No. Methods come from the session's allowed-methods list, decided by your backend when the session is created. An empty list means all supported methods are shown.

Can I theme the payment sheet from my app?

There is no public theming API. The SDK applies its own design system; a brand color carried by the session recolors the result-sheet action buttons only.

Is Apple Pay supported?

No. Apple Pay is iOS-only. Four Apple-Pay-related values exist in KashierErrorCode purely so the enum matches Kashier's other SDKs; no Android code path emits them.

Can I call initialize again to switch between TEST and LIVE, or change language?

No. Re-initializing with a different config throws IllegalStateException. Decide mode and language at process start.

Why is amount in KashierPaymentResult nullable and only "approximate"?

It is a display convenience. Invalid or missing backend values map to null rather than throwing. Use your backend's amount for anything financial.

Why is transactionData always null?

On the Activity Result API, raw backend maps deliberately never cross the process boundary. They are available best-effort, in-process only, on the compatibility wrapper path — and they are sensitive, so do not log them.

Do I need to handle configMissing?

Handle it for exhaustiveness, but the current SDK never emits it. A relaunch without configuration is by definition process death and is reported as processDeath.

What happens if the user backgrounds the app during a wallet payment?

Polling continues while the SDK Activity lives, up to the 5-minute cap. If the process is killed, reconcile via your backend.

Is there a "resend" button on the wallet waiting screen?

No. The waiting screen offers Reload (re-check status) while polling, and "I have paid" plus Cancel once the 5-minute cap is reached. To send a new request-to-pay, the user cancels back to the phone-input step.

Can I run two payments at once?

No. The second launch is rejected with validationFailed and the message "payment already in progress".

Does the SDK require Jetpack Compose in my app?

No. The SDK uses Compose internally; no Compose type is on its public API. The Kashier demo app uses plain Android views.

Which ProGuard rules do I add?

None. The SDK ships its own consumer rules inside the AAR.

Is Pending really terminal?

Yes. No success or failure follows it for that launch. Reconcile via backend or webhook.

Does the SDK need android:supportsRtl="true"?

Not for the SDK's own sheet — it pins its layout direction internally. Set it only if your own screens need RTL.

UI screens reference

The SDK renders these screens. Your app does not build or control any of them; this reference exists so you know what your users will see and which screen produces which result.

ScreenWhen it appearsExit paths
Landing sheetAfter the session loadsWallet tile → wallet phone input; card tile / accordion → card form or saved-card pay; dismiss → Cancelled
Card formNew-card entryPay → 3-D Secure or result; back → landing; dismiss → Cancelled
Card form, test modeCard form while KashierMode.TESTAdds the "Use Testing Data" button; hides "save card"
Card form, validation errorsInvalid field on submitInline errors; no network call, no callback
Saved-card accordionSaved cards available for the sessionSelect a card → inline CVV → Pay; swipe → delete confirmation; "Add card" → card form
Saved-card loading / emptyWhile fetching, or with no saved cardsFalls through to the new-card path
Delete confirmation dialogSwipe on a saved-card rowConfirm → delete (snackbar on failure); cancel → accordion
Wallet phone inputWallet method chosenPay → waiting sheet; back → landing
Wallet waitingRequest-to-pay sentReload; Cancel → phone input; terminal reconcile → result
Wallet pending exhausted5-minute cap reached, still pending"I have paid" → Pending; Cancel → Cancelled
3-D Secure WebViewIssuer requires authenticationCompletion → verifying; cancel → Cancelled
3-D Secure verifyingChallenge finished, outcome confirmingAutomatic → result or pending
3-D Secure timeoutAuthentication timed outDone → Failed(paymentTimeout); Try Again → restart, no callback
Success sheetPayment approvedDone → Succeeded
Failure sheetPayment failedDismiss → Failed; Try Again → back into the flow, no callback
Pending modalSettlement still runningDone → Pending

Every screen renders in English (LTR) or Arabic (RTL) according to KashierConfig.language.

Screenshots for the landing sheet, card form, saved-card accordion, CVV validation, wallet phone entry, wallet waiting, 3-D Secure challenge, 3-D Secure timeout, and the success sheet appear in their respective sections above. All were captured against a test merchant account with transaction identifiers masked.

Screens not yet illustrated: the delete-confirmation dialog, the wallet pending-exhausted state, the pending modal, and the failure sheet. Their behaviour is described in the table above.

Support and license

Support

  • Developer portal: developers.kashier.io
  • For integration issues, include: SDK mode (TEST/LIVE), Android API level, device model, the KashierErrorCode, the responseCode if present, and the order ID or order reference.
  • Never share session IDs, card data, OTPs, raw transactionData, or raw details in a support request.

License

The Kashier Native Android SDK is proprietary software owned by Kashier. It is distributed under the Kashier Proprietary SDK License: read the full license.

The same license text ships as the LICENSE file inside the SDK distribution, and the license name and URL above are declared in the published Maven POM.

In summary — the full license text governs, this is not a substitute for it:

  • You may download, integrate, and distribute the SDK inside your own application solely to enable Kashier payment services.
  • You may not redistribute the SDK as a standalone product, modify it, create derivative works, or reverse engineer, decompile, or disassemble it.
  • You may not remove or alter Kashier's proprietary, trademark, or copyright notices, or use the SDK to provide payment services other than through Kashier.
  • Third-party components bundled with or required by the SDK remain subject to their own license terms.
  • Use of Kashier payment services is additionally governed by your merchant agreement and the applicable Kashier terms of service.

On this page

SDK overviewWhat it doesWhat it does not doPayment method availabilityArchitecture at a glanceRequirementsWhat is a session ID?Installation and setupAdd the dependencyAndroid project setupPermissions — nothing to declareActivities — nothing to declareCompose is not required in your appRight-to-leftProGuard / R8Quick startSDK configurationKashierConfigKashierModeInitialization contractLaunching a paymentCanonical Activity Result API (recommended)Compatibility wrapper (callback style)Wrapper callback semanticsWrapper registration rulesConcurrencyHandling payment resultsWhich identifiers are authoritativeWhen backend reconciliation is requiredKashierPaymentResult fieldsKashierPaymentPending fieldsCard and saved cardsNew-card flowSaving a card after paymentWhat your app seesReconciliation safety netPCI scopeSaved cardsWhen saved cards are fetchedPresentationPaying with a saved cardDeleting a saved cardSecurityWallet paymentsFlowPolling scheduleControls on the waiting sheetExhaustion and reconciliationWhat your app sees3-D SecureWhat the user seesHow completion is detectedCancel, timeout, retrySecurityLifecycle and process deathRegistrationActivity vs FragmentThreadingConfiguration changes and rotationProcess deathMultiple concurrent paymentsMalformed resultsKotlin and Java integrationKotlinJavaAPI referenceKashierSDKKashierConfigKashierModeKashierLanguageKashierPaymentContractKashierPaymentRequestKashierResultKashierUnavailableReasonKashierRequestIdKashierPaymentResultKashierPaymentErrorKashierPaymentPendingKashierCardInfoKashierErrorCodeKashierLauncherListenersError handlingError codesLocalized error copyDiagnosticsR8, ProGuard and securityR8 / ProGuardYour responsibilitiesWhat the SDK guaranteesTesting and going liveTest modeWhat to exerciseDemo appGoing-live checklistFrequently asked questionsUI screens referenceSupport and licenseSupportLicense