Offers and grants
This page connects Grona offers, such as 10% off this order at checkout, to your backend. An offer carries money, so the app alone never decides it. Grona signs a grant for each shopper who may see the offer, your backend fetches it, the SDK renders the offer only with a matching grant, and your order backend verifies the same grant before it applies anything.
How it fits together
Section titled “How it fits together”- The shopper signs in and your app calls
Grona.identify. - The SDK calls your
GronaGrantSourceon a background thread. - Your grant source calls your backend, inside the shopper’s authenticated session.
- Your backend calls Grona with its secret key and returns the grants it receives.
- The SDK verifies each grant with the grant keys in your options and renders offers that match.
- At checkout your app reads
Grona.activeTreatments()and sends each token to your order API. - Your order backend verifies each token and applies the treatment from the verified claims.
For the server side of steps 4 and 7, see Grants and order verification and the Mobile grants API.
Implement the grant source
Section titled “Implement the grant source”GronaGrantSource has one method. It returns the grants as compact JWS strings, or null on failure.
package com.example.acmemarket
import ai.grona.sdk.GronaGrantSourceimport java.io.IOException
class AcmeGrantSource(private val api: AcmeApi) : GronaGrantSource { // Called on a Grona background thread; may block for up to 10 seconds override fun fetchGrants(customerId: String): List<String>? = try { // Your endpoint, for example GET https://shop.example.com/api/grona/grants, // authenticated by the shopper's session api.fetchGronaGrants() } catch (e: IOException) { null // a failure: the SDK retries with backoff }}- Your backend picks the customer. Take the customer from your own authenticated session, not from a parameter the app sends. The
customerIdargument tells you which shopper the SDK expects. Grants for another customer fail verification on the device. - Threading. The SDK calls the source on its background threads, sometimes at the same time as your own code. Keep it thread-safe and do not touch views.
- Timeouts. Past 10 seconds the SDK stops waiting, counts a failure and retries with backoff. It calls the source again only once the earlier call has returned.
- Return values. Return
nullon failure. Return an empty list when the shopper has no grants.
Configure the keys and the source
Section titled “Configure the keys and the source”Add the grant source and the grant signing keys that Grona provides for your workspace. Add the current and the next key, so a key rotation needs no app release.
val options = GronaOptions.Builder("acme-market") .configUrl("https://api.grona.ai/v1/mobile/workspaces/acme-market/config") .bundledConfigAsset("grona/config.json") .configKey("acme-market-2026-10", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...") .grantKey("acme-market-grants-2026-10", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...") .grantSource(AcmeGrantSource(api)) .eventsUrl("https://api.grona.ai/v1/mobile/events") .build()For rotation, see Rotate signing keys.
When grants are fetched
Section titled “When grants are fetched”- On
identify, and again shortly before the nearest grant expires. - Grants live in memory only, at most 50 at a time.
- They are cleared when the customer changes, on
identify(null)and onreset().
An offer renders only when a verified, unexpired grant matches its campaign, variant, treatment type and value. Without one the shopper is not treated: the campaign yields to the next one in the placement, as a control arm does. Offers use customer-unit assignment, so a shopper who is not identified never sees one.
Right after sign-in the first grant fetch may still be running. The offer then appears when the grant arrives, replacing whatever the placement showed.
Send treatments with the order
Section titled “Send treatments with the order”At checkout, read the treatments the shopper can currently see and send their tokens to your order API.
import ai.grona.sdk.Gronaimport ai.grona.sdk.GronaEventimport ai.grona.sdk.GronaScreen
fun placeOrder(cart: AcmeCart) { val shown = Grona.activeTreatments() val claims = shown.mapNotNull { t -> t.token?.let { token -> TreatmentClaim(t.campaignId, t.variantId, t.type, token) } } orderApi.submit(cart.toOrderDraft(), claims) { result -> // delivered on the main thread val applied = shown.filter { t -> result.applied.any { it.campaignId == t.campaignId && it.variantId == t.variantId } } Grona.track(GronaEvent.purchase(result.orderId, result.totalMinor, cart.productIds(), applied)) // On the confirmation screen confirmationSlot.screen = GronaScreen.orderConfirmation(applied.map { it.type }) }}TreatmentClaim and orderApi are your own types. Send the token and the ids, never the value: your backend reads the value from the verified token.
activeTreatments() returns offers in live slots: attached, shown and in a visible window. Offers shown only because of a preview pin never appear in it.
GronaTreatment property |
Meaning |
|---|---|
campaignId, variantId |
The campaign and variant |
placement |
Where it was shown |
type |
The treatment type |
valueMilli |
Value in thousandths, so 10000 is 10 (10% off with percent_off) |
token |
The Grona-signed grant your backend must verify. null in demo mode |
isDemo |
true in demo mode |
Every field except token is display data. Your order backend must verify the token, enforce single use on the grant’s jti claim in its own records (never on the token string, because one grant can have more than one valid signature), and apply only what the verified claims say. A missing or invalid token means no treatment, and the order still succeeds. There is no Grona redeem endpoint.
Demo mode
Section titled “Demo mode”demoMode(true) lets offers render without a grant, so a demo build can run without a backend. It takes effect only when the signed configuration also sets demo mode. With only one switch on, the SDK behaves as in production and logs a warning. In demo mode treatments carry no token, isDemo is true, and every event is marked as demo and excluded from analysis.
Next steps
Section titled “Next steps”- Set up Grants and order verification on your server.
- Read Offers and grants for how offers are analysed.