Skip to main content
Encore hosts the Web SDK for you at https://www.encorekit.com/offers. Your app opens that URL in a plain webview, passes its configuration in the query string, and verifies the result from your backend. No package to install, no build step, and nothing to ship in a client release. Use it when installing an SDK is not on the table: a native app with no Encore integration, a kiosk or device shell, a partner surface whose build you do not own, or a pilot you want running this week. Everything else on web should use the Web SDK, which hands the presentation result back to your own code. Three routes serve the identical page, so pick whichever fits your app:
Claiming an offer replaces this page. A webview has no second tab to open, so the handoff to the advertiser is a top-level navigation in the same webview. Your app owns the way back. Read What your host app must get right before you build anything else.

What your host app must get right

The page is built to work in a webview nobody configured, so there is very little to do. These three things are the exceptions, and each one breaks an integration that does not know about it.

Open the page at top level, never in an iframe

Load the URL as the webview’s own document. The advertiser handoff navigates the frame the offer page is running in. Inside an iframe that means the advertiser loads into your iframe rather than taking over the view, so the user ends up on a third-party site rendered in a box inside your app, with your own chrome still wrapped around it and no sensible way out. A top-level document is the only arrangement the handoff is designed for. The symptom is easy to recognize once you know it: the offer itself looks fine, and only after a claim does the user end up stuck on an advertiser page inside a small frame. If you see that, the page is being loaded in a frame.

Claiming replaces the page, so own the way back

This is the one genuinely surprising behavior, and it only surfaces after a successful claim, which is the worst moment to discover it. A locked webview with no back gesture and no surrounding chrome strands the user on the advertiser’s site.
1

Expect the webview to navigate away

After a claim, the webview is showing the advertiser’s site rather than Encore or your app. That is the normal, successful path, not a failure.
2

Give the user a way back

Provide your own close or back control, either in the chrome around the webview or through the webview’s own back stack. The offer page cannot return the user to your app, and there is no confirmation screen to close.
3

Let the navigation through

If your WebViewClient.shouldOverrideUrlLoading or your WKNavigationDelegate.decidePolicyFor restricts navigation to your own domain, the handoff is blocked at the last step. Allow www.encorekit.com and outbound advertiser domains, or pass a blocked URL to the system browser instead of cancelling it.
4

Do not tear the webview down on navigation

A host that closes the webview the moment the URL leaves encorekit.com closes it on the user mid-claim.

This holds for the hosted page only

The hosted page does that work for you. If you ever hand-roll your own page around @encorekit/web-sdk instead, the behavior changes underneath you. The SDK reaches the advertiser through window.open, inside the user’s tap on Claim. That is correct in a browser and a no-op in a webview: window.open returns null in an Android WebView unless the host sets setSupportMultipleWindows(true) and implements WebChromeClient.onCreateWindow, and null in a WKWebView unless the host implements WKUIDelegate.createWebViewWith. There is no second tab to be had, so Claim becomes a dead tap. The hosted page replaces window.open with a stand-in that navigates the current frame, which needs no cooperation from the host at all.
So the dead tap is a consequence of not using the hosted page. A hand-rolled page in a webview must implement onCreateWindow and createWebViewWith itself, or reproduce the same-frame handoff. Using /offers costs you nothing here.
Because the advertiser is reached by a same-frame navigation rather than the SDK’s noopener,noreferrer popup, the page declares a no-referrer policy. Your API key and your end user’s id never travel to the advertiser in a Referer header. The route is also excluded from indexing and from the sitemap.

The URL contract

Every value is trimmed, and a blank value is treated as absent, so ?headline= behaves exactly like omitting headline. Values longer than 256 characters are truncated rather than rejected.

Required

Omit either one and the page renders a diagnostic screen naming what is missing, instead of an offer. Build your URLs against that screen before you ship.

Optional

This page defaults to useCase=rewardUsers, which differs from the Web SDK’s own default of reduceChurn. The SDK’s default suits a save attempt fired automatically as someone leaves. Nothing about this page is automatic: your app opens it at a moment of its own choosing, and that moment is a reward far more often than a cancellation. Pass ?useCase=reduceChurn to get the churn sheet instead. What each value renders is documented in Sheet Variants.
An unrecognized useCase or placement does not fall back to the default. The page refuses to start and names the invalid parameter, because silently presenting the wrong screen to someone who asked for a specific one is the harder failure to track down. An invalid language is the exception: the page ignores it, still starts, and follows the device language.

Targeting attributes

Attributes go to Encore in their own request, before you open the page. You POST them to POST /users/attributes against the same userId the link carries, and the page picks them up when it fetches offers. That call authenticates with your publishable key, the same one the link itself carries in plain sight, so it can run wherever you assemble the link: a backend, or the app that opens the webview. Nothing about it requires a server.
1

Store the set

POST the attributes with your publishable key. The write replaces the stored set rather than merging into it, so send the complete set every time. That also makes a retry safe, though only a network failure or a 5xx is worth retrying. A 4xx means the request itself needs fixing, and repeating it produces the same refusal every time.
2

Open the link

https://www.encorekit.com/offers?apiKey=...&userId=..., plus whichever of useCase, placement, headline, subheadline, language, and autoPresentOffer you need.
Store the attributes
Then open the page as you normally would, with the same userId you just stored against. Worked URLs has the link shapes and a snippet that builds one. A user whose attributes did not store is still served, just without the targeting the set would have added. The endpoint reference carries the full vocabulary and which apps may send which keys, including an age and, from an app rated 18+ in its store listing, the direct identifiers. Read its rules on age before you send one. Three of them catch people out: a user under 18 is still served offers, a range is sent as its lowest end, and for a user under 18 only age and countryCode are kept out of whatever you sent.

Worked URLs

Build the URL with the platform’s own encoder rather than string concatenation, so a value carrying a space or an ampersand cannot corrupt the query string:
Build the URL

Webview settings

Nothing here is required to make the page work. Both settings buy you better behavior.

Turning on DOM storage

Android WebView ships with DOM storage turned off. The SDK degrades cleanly, falling back from localStorage to sessionStorage to in-memory, so nothing crashes and nothing looks wrong. What it costs you is that every marker the SDK keeps per user is re-derived on each open: the measurement exposure marker, analytics deduplication markers, the cached entitlement snapshot, and any queued events still waiting to be delivered. The most visible effect is an inflated exposure count in reporting. Two things are unaffected, and worth knowing so you do not go hunting for a problem that is not there. Identity is stable, because userId arrives in the URL on every open rather than out of storage. Experiment cohort is stable too, because it is derived deterministically from that same userId rather than stored and looked up. One line removes the whole issue:

Load it over HTTPS

Point the webview at https://www.encorekit.com directly. Cohort assignment uses WebCrypto, which browsers expose only in a secure context. Serving this page through a plain http:// origin silently drops every user out of measurement.

Verify the entitlement from your server

There is no client bridge, and none is needed. The page and your app never talk to each other. They agree on a userId, and your backend asks Encore what that user holds. Call GET /entitlements/server with the same userId you put in the URL. It is HMAC-signed, so it is a backend call only:
Backend (Node)
Gate real access on verified only. provisional is the immediate unlock a client shows while the brand-side completion is still unverified. Treat it as a courtesy, never as proof.
Two details cause most signature failures, and the endpoint reference covers both: you sign the path without the /encore gateway prefix, and a GET signs a canonical query string rather than a body. Multi-app projects also send X-Platform.
Prefer the push side where you can. The Offer Completed webhook delivers each completion as it verifies, which beats polling for something that may complete days after the user closed the webview.

When the page does not start

Missing or invalid parameters produce a plain screen reading “This link isn’t complete”, followed by the specific problem. Every missing name is listed at once, then every invalid one, so one look is enough: missing apiKey, userId, or missing userId · invalid useCase. That text is aimed at whoever is assembling the URL. Check it first when a webview opens to something other than an offer. A page that starts but presents nothing is a different, normal outcome. It means no offers are eligible for that user right now, and it renders “Nothing available right now”. That is neither an error nor a misconfiguration.

See also