Android SDK
Kotlin, minSdk 24. Presents YallaPay checkout as a sheet over your activity, confirms the outcome with the API, and returns a typed result. Three calls: configure, pay, and a check on launch for interrupted payments.
On this page
Before you start, your server must be able to create sessions and your dashboard must have a return target registered. If you have not done that yet, read Dashboard setup and Server integration first.
Requirements
| Language | Kotlin 2.0 or newer. Callable from Java, see below. |
| Minimum SDK | API 24 (Android 7.0). compileSdk 34 or newer. |
| Build | Android Gradle Plugin 8.x, Java 11 toolchain. |
| On the device | Any browser. Chrome 107 or newer gives the sheet presentation; older browsers get a full-screen tab, which still works. |
| Permissions | INTERNET, merged in by the library. Nothing else. |
| Dependencies pulled in | Kotlin coroutines, kotlinx-serialization, OkHttp 4, AndroidX Browser and Core. No Google Play services requirement. |
Install
Add the dependency to your app module:
// app/build.gradle.kts
dependencies {
implementation("net.yallapay:yallapay-android:0.1.0")
}
Your project must resolve from mavenCentral(), which every new Android project already does.
While the SDK is in early access the artifact is provided directly by YallaPay rather than from Maven Central. Email [email protected] for the package and a source repository; the coordinates above are the ones it publishes under, so nothing in your build changes later.
Declare your return scheme
Checkout returns the customer to your app through a custom URL scheme. Choose one that is unique to your brand, then tell the build about it. The SDK's own manifest registers the receiving activity for you; you never edit a manifest.
// app/build.gradle.kts
android {
defaultConfig {
manifestPlaceholders["yallapayReturnScheme"] = "myapp"
}
}
Groovy build files use the same placeholder: manifestPlaceholders = [yallapayReturnScheme: "myapp"].
This makes your app answer to myapp://yallapay/…. The host part is always yallapay; only the scheme is yours to choose. Register the same scheme against your merchant account under Apps & Return URLs, and use myapp://yallapay/return as the return URL when your server creates sessions. Unregistered schemes are rejected at session creation, and a mismatch between the three means the customer pays and your app never hears about it.
Android lets any app claim any scheme, and the system decides which one opens. A scheme like "shop" invites collisions; "acmeshop" does not. A scheme can also belong to only one YallaPay account.
Configure
Call configure once, before any payment. The natural place is Application.onCreate().
import net.yallapay.sdk.Environment
import net.yallapay.sdk.YallaPay
class App : Application() {
override fun onCreate() {
super.onCreate()
YallaPay.configure(
publishableKey = "pk_live_…",
returnUrl = "myapp://yallapay/return",
)
}
}
For debug builds, point at the sandbox with a test key. Tie it to the build type so a release can never ship against the sandbox:
YallaPay.configure(
publishableKey = if (BuildConfig.DEBUG) "pk_test_…" else "pk_live_…",
returnUrl = "myapp://yallapay/return",
environment = if (BuildConfig.DEBUG) Environment.SANDBOX else Environment.LIVE,
)
| publishableKey | Your pk_live_ or pk_test_ key. Anything beginning with sk_ throws immediately: a secret key must never be in an app. |
| returnUrl | Your scheme plus ://yallapay/return. Passed to your server in practice, but the SDK keeps it so the sample and the tests can build sessions. |
| environment | Environment.LIVE (default) or Environment.SANDBOX. |
| baseUrl | Optional override of the host, for pointing a debug build at your own YallaPay deployment. Leave unset in anything you ship. |
Take a payment
Ask your server for a session id, then hand it to the SDK from an Activity. The call suspends until the customer finishes, cancels, or something fails.
import net.yallapay.sdk.PaymentResult
import net.yallapay.sdk.YallaPay
class CheckoutActivity : AppCompatActivity() {
private fun payOrder(orderId: String) {
lifecycleScope.launch {
val sessionId = api.createPaymentSession(orderId) // your backend
when (val result = YallaPay.pay(this@CheckoutActivity, sessionId)) {
is PaymentResult.Completed -> showPaid(result.transactionId)
is PaymentResult.Canceled -> showCancelled()
is PaymentResult.Failed -> showError(result.error)
}
}
}
}
What pay does, in order:
- Reads the session with your publishable key. If it is already paid, expired or failed, returns straight away without opening anything.
- Remembers the session id on disk, so an interrupted payment can be recovered on the next launch.
- Opens checkout in a sheet over your activity.
- Waits. Either the return deep link brings the customer back, or the customer closes the sheet.
- Reads the session again and returns a result based on what the API says, never on the redirect.
- Forgets the remembered session id.
Call it from the main thread inside a coroutine scope tied to your Activity or ViewModel. Calling it a second time while a payment is in flight is not supported; disable your Pay button while waiting.
Handling the result
| Completed | YallaPay confirmed the session is paid. Fields: sessionId, transactionId, paymentMethod (CARD, APPLE_PAY, GOOGLE_PAY or UNKNOWN, nullable). Show a receipt and poll your own server for the order to flip to paid. |
| Canceled | The customer closed the sheet without paying, or the session is still open for any other reason. Nothing was charged. Keep the session id; while it has not expired, the customer can tap Pay again and continue with the same session. |
| Failed | Carries a YallaPayError with a code and a message safe to show. Act on the code, see the table below. |
private fun showError(error: YallaPayError) {
when (error.code) {
YallaPayError.Code.PAYMENT_FAILED -> toast("Your card was declined. Please try another.")
YallaPayError.Code.SESSION_EXPIRED -> startNewSession() // ask your server for a fresh one
YallaPayError.Code.NETWORK_ERROR -> recheckLater() // may still have succeeded!
else -> toast(error.message)
}
}
It means the phone could not reach YallaPay after checkout closed. The customer may have paid. Call sessionStatus again when connectivity returns, and never show "payment failed" for this code.
Whatever the app shows, ship the order from your server on the webhook. See Webhooks & fulfilment.
Interrupted payments
Android can kill your app while the customer is in the checkout sheet: low memory, a phone call, the customer switching to their banking app for 3-D Secure and coming back later. When that happens, the coroutine waiting on pay is gone. The customer may have paid, and nobody in your app knows.
The SDK keeps the in-flight session id on disk for exactly this case. On every launch, check for one and resolve it:
import net.yallapay.sdk.internal.PendingSessionStore // In your launcher Activity, or wherever you restore state on start PendingSessionStore.peek(this)?.let { sessionId -> lifecycleScope.launch { val result = YallaPay.sessionStatus(sessionId) PendingSessionStore.clear(this@MainActivity) if (result is PaymentResult.Completed) { showPaid(result.transactionId) // and refresh the order from your server } } }
peekreturns the pending session id, or null when nothing was interrupted, which is the case on almost every launch.sessionStatusreads the session from the API and returns the same three results aspay. It never opens a sheet.clearforgets the id. Call it once you have acted on the result, so the next launch starts clean.
It is five lines, it runs in milliseconds when nothing is pending, and it is the difference between a customer who paid and sees their order and a customer who paid and sees nothing. Skipping this loses payments.
What the customer sees
Checkout is a YallaPay-hosted page presented as a Chrome Custom Tab in bottom-sheet form. It slides up over your activity, takes most of the screen and can be dragged to full height. The toolbar takes your app's primary colour, there is no share button or overflow menu, and the address bar tucks away as the customer scrolls. A small line under the title shows the YallaPay domain; that is a Chrome security requirement and cannot be removed.
On the sheet the customer sees your business name, the purpose you gave the session, the amount, Apple Pay and Google Pay buttons where the device supports them, and a card form. Card numbers are entered on the YallaPay page, never in your app, so your app stays out of PCI scope. 3-D Secure, when a bank requires it, happens inside the same sheet.
When payment completes, checkout redirects to your return scheme, Android hands the deep link to the SDK, the sheet closes, and pay returns. The customer never sees a browser back button or a "return to app" page.
The sheet needs a browser that supports Custom Tabs. Chrome, Samsung Internet, Edge and Firefox all do. On a device with no browser at all, pay returns Failed(BROWSER_UNAVAILABLE).
Using it from Java
configure, peek and clear are static methods from Java. The two suspending calls need a coroutine to run in; the easiest bridge from Java is a small Kotlin helper in your own module:
// PaymentBridge.kt, in your app
object PaymentBridge {
@JvmStatic
fun pay(activity: AppCompatActivity, sessionId: String, callback: (PaymentResult) -> Unit) {
activity.lifecycleScope.launch { callback(YallaPay.pay(activity, sessionId)) }
}
}
// Java
YallaPay.configure("pk_live_…", "myapp://yallapay/return");
PaymentBridge.pay(this, sessionId, result -> {
if (result instanceof PaymentResult.Completed) {
showPaid(((PaymentResult.Completed) result).getTransactionId());
} else if (result instanceof PaymentResult.Failed) {
showError(((PaymentResult.Failed) result).getError().getMessage());
}
return Unit.INSTANCE;
});
Error codes
| Code | Meaning and what to do |
|---|---|
| INVALID_KEY | The publishable key was missing, malformed, for the wrong environment, or not accepted. A configuration problem; fix the key or the environment. |
| SESSION_NOT_FOUND | No such session for this account. Usually the session was created on the other environment, or the id was mangled in transit. |
| SESSION_EXPIRED | The session passed its expiry before the customer paid. Ask your server for a new one. |
| PAYMENT_FAILED | The customer reached checkout and the payment did not succeed, for example a decline. Offer to try again. |
| BROWSER_UNAVAILABLE | No browser on the device can present checkout. Rare; tell the customer to install one. |
| NETWORK_ERROR | Could not reach YallaPay. The payment may still have succeeded. Re-check with sessionStatus. |
| UNKNOWN | YallaPay returned something the SDK does not recognise. Show the message and re-check later. |
YallaPayError is a value, not an exception, so a failed payment can never escape as an unhandled crash. It has code, message, an optional param and an optional cause.
Reference
| YallaPay.configure(publishableKey, returnUrl, environment = LIVE, baseUrl = null) | Once, at launch. Throws if the key is not a publishable key. |
| suspend YallaPay.pay(activity, sessionId): PaymentResult | Presents checkout and resolves the outcome from the API. |
| suspend YallaPay.sessionStatus(sessionId): PaymentResult | Reads a session without presenting anything. Never throws. |
| PendingSessionStore.peek(context): String? | The session id of an interrupted payment, or null. |
| PendingSessionStore.clear(context) | Forget it. |
| PaymentResult | Completed(sessionId, transactionId, paymentMethod), Canceled, Failed(error) |
| PaymentMethod | CARD, APPLE_PAY, GOOGLE_PAY, UNKNOWN |
| Environment | LIVE, SANDBOX |
| YallaPayError(code, message, param?, cause?) | See error codes above. |
There is deliberately no method to create a session. Creating one needs your secret key, and a secret key inside an APK is a secret key in everyone's hands.
Sample app
The SDK ships with a one-screen sample: a text field for a session id and a Pay button. It is the whole integration in about a hundred lines, including the launch-time recovery check. Build and install it on an emulator with ./gradlew :sample:installDebug, paste a session id created against the same environment, and press Pay. It is also a good way to confirm your return scheme and keys before touching your own app.
Troubleshooting
| Build fails with "yallapayReturnScheme" not found | The manifest placeholder is missing from defaultConfig. Add it to the app module, not the library. |
| The sheet opens, the customer pays, but pay returns Canceled | The placeholder scheme, the registered scheme and the session's return URL are not all identical. Check case too: schemes are lower-cased on registration. |
| The customer is asked which app should open the link | Another installed app claims the same scheme. Choose a more specific scheme and re-register it. |
| The sheet is full-screen rather than a bottom sheet | The default browser is older than Chrome 107 or does not support partial Custom Tabs. Everything still works. |
| configure throws about the key | You passed a secret key. Use the publishable one from Apps & Return URLs. |
| R8 or ProGuard strips something | The library ships consumer rules; nothing to add. If you see serialization errors in release builds, make sure you are not overriding those rules. |
General problems, test cards and the go-live checklist are on Sandbox & go-live.