Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Offers and grants

An offer that carries money, such as 10% off an order, renders only when the SDK holds a grant for it. A grant is a short-lived token that Grona’s server signs for one identified shopper, one campaign and one variant. This page explains why grants exist, what a grant contains, how the device uses it, and why your order backend has the final say.

Three facts about a mobile app rule out trusting the device with money:

  • The configuration is public. The workspace key ships in your app, so anyone can fetch the configuration. It cannot hold customer lists, so it cannot say who is eligible.
  • The device user may be an attacker. Anything the app decides alone, the user can change: cached files, the clock, deep links, a repackaged build.
  • Network data is untrusted, even over TLS. Integrity comes from signatures, not from the channel.

So money never rests on a client assertion. Grona’s server decides who is eligible, using customer-unit bucketing and segments the server knows, and signs a grant for that shopper. The device uses the grant to decide what to show. Your order backend verifies the same grant again before it applies anything, so a treatment the device reports is display state until then.

  1. The shopper signs in, and your app calls identify with their opaque customer id, for example cus_7Hq2Lm9x.

  2. The SDK asks your app’s grant source for grants. Your grant source calls your backend inside the shopper’s authenticated session.

  3. Your backend fetches the shopper’s grants from Grona, server to server, and returns them to the app unchanged.

  4. The SDK verifies each grant and keeps the valid ones in memory.

  5. An offer renders only when a grant matches its campaign, variant, treatment type and value.

  6. At checkout, your app reads the SDK’s active treatments, each with its grant token, and sends the tokens with the order.

  7. Your order backend verifies each token, re-checks the campaign’s conditions against the real order, uses each grant once, and applies the value from the verified claims.

A grant is a compact JWS (RFC 7515, section 7.1) signed with ES256 by a Grona grant key. The SDK trusts the grant keys your app pins, the current key and the next one. The protected header holds exactly three fields:

{"alg": "ES256", "kid": "acme-market-grants-2026-10", "typ": "grona-grant+jwt"}

Any other header field, another alg or typ, or an unknown kid rejects the grant.

Claim Type Rule
sub string The customer id. It equals the id your app passed to identify
wks string The workspace key
cmp id The campaign id
var id The variant id
trt id The treatment type. It equals the campaign’s treatment type
val int The treatment value in thousandths, 0 or more. It equals the campaign’s treatment value
iat int Issued at, in Unix seconds between 2020-01-01 and 2100-01-01, and not more than 5 minutes ahead of the device clock
exp int Expiry, in Unix seconds between 2020-01-01 and 2100-01-01. It is after now, after iat, and at most 24 hours after iat
jti id A single-use nonce. Your order backend enforces single use
prv bool Always false. Preview never produces a grant

Ids match [A-Za-z0-9._:-]{1,128}. Unknown claims are ignored, and a token is at most 4 KB. A decoded payload for Acme Market’s weekend offer looks like this, where val 10000 means 10, that is 10% off for a percent_off treatment:

{
"sub": "cus_7Hq2Lm9x",
"wks": "acme-market",
"cmp": "weekend-offer",
"var": "B",
"trt": "percent_off",
"val": 10000,
"iat": 1793264400,
"exp": 1793268000,
"jti": "9f1c2e7a4b6d8e0f1a3c5b7d",
"prv": false
}

Treatment types are ids that the campaign defines. percent_off is this example’s type, not a fixed list.

  • Fetching. The SDK fetches grants when your app calls identify, again shortly before the nearest grant expires, and at least once an hour while the shopper stays identified. A failed fetch is retried with backoff of up to 15 minutes. Each fetch waits at most 10 seconds and runs off the SDK’s own thread. The SDK keeps at most 50 grants.
  • Storage. Grants live in memory only. The SDK clears them when the customer changes and on reset.
  • Matching. An offer renders only when a verified, unexpired grant matches its campaign, variant, treatment type and value. A value mismatch fails closed, because the copy would promise something your backend will not apply.
  • No grant. Without a matching grant the shopper is not treated. The campaign leaves the placement to the next campaign, as a control arm does.
  • Who can see offers. Every campaign with a money treatment uses the customer unit, so shoppers who are not identified never see an offer.
  • Active treatments. The SDK’s active treatments are the offers rendered in a live slot, each with its grant token. A slot is live while it is attached, shown and in a visible window. A slot on a stopped or hidden screen keeps its content but is left out.

To connect a grant source and read active treatments, see Offers and grants for Android or iOS.

Bucketing happens before the grant check. So a shopper in an offer’s treatment arm may never see the offer, and the events say why in a delivery field:

delivery Meaning
granted A verified grant matched. grant_jti names it
grants_pending No grant fetch has completed since identify
no_grant A fetch completed and no grant matched, or the app has no grant source
demo Demo mode rendered the offer without a grant
preview A preview pin rendered the offer
  • The eligible event keeps the assigned arm, treatment, and records the delivery state at the first decision of the visit. That is often grants_pending, because identify fetches grants in the background.
  • A grant that lands later in the visit makes the offer replace the fallback. That logs a served event with delivery: granted and the grant’s jti, not a second eligible.
  • Analysis takes the treated share from served events, not from Grona’s issuance log. A minted grant can fail to reach the device, arrive after the shopper left, or expire first. See Events and exposure.
  • Preview. A preview pin can render an offer without a grant so a tester can check it. The offer never appears in the active treatments, and every event carries preview: true. A pin never shows a customer-unit campaign to a shopper who is not identified, so logged-out testers never see an offer either.
  • Demo. Demo mode, which needs both an app option and a setting in the signed configuration, renders offers without a grant. Their treatments carry no token and are flagged as demo, and a real backend must ignore them. See Signed configuration.

Neither preview nor demo produces a grant, and every grant carries prv: false.

The order backend is the enforcement point

Section titled “The order backend is the enforcement point”

Everything on the device is display. Money moves only when your order backend verifies the token:

  • the signature, with a current grant key;
  • exp;
  • sub against the customer of the order;
  • wks;
  • prv is false;
  • single use on jti;
  • the campaign’s basket conditions against the real order.

Anything that fails means no treatment, and the order still succeeds.

The value applied comes only from the verified claims: cmp, var, trt and val. The value, demo flag and placement that the app reports with a treatment, and the treatments in the app’s purchase event, are display data, and your backend ignores them.