How-to: Restrict an API key to your app
Your nu_app_… API key is meant to ship inside your app — a Capacitor
binary or a web build. You do not need to stand up your own backend just to
hide it. Instead, lock each key to the clients allowed to use it, in the
dashboard under Apps → API Keys → Restrictions.
A key with no restrictions works from anywhere (this is the default, and it
stays that way for existing keys). Once you add any restriction, only the listed
clients can use the key — everything else gets 403 API_KEY_RESTRICTED.
Web: allowed origins
Add the exact origins your web app runs on:
https://app.example.com— one exact origin.https://*.example.com— any sub-domain ofexample.com(not the apex; addhttps://example.comseparately if you need it).http://localhost:5173— for local development.
This is enforced against the browser's Origin header, which page JavaScript
cannot forge — so a web-origin restriction genuinely stops another site from
reusing your key. Because of this, the /v1/* update API is deliberately
callable straight from the browser (it sends permissive CORS); the real gate is
this per-key check, not CORS.
Android: allowed apps
Add your app's package name plus its signing-certificate fingerprints.
Get them with keytool:
# From your keystore:
keytool -list -v -keystore my-release.keystore -alias my-alias
# …or print a specific certificate file:
keytool -list -printcert -file cert.pem
Copy the SHA-256 (and optionally SHA-1) lines. Colons and spaces are fine — they are stripped automatically.
If your app is distributed through Google Play, add two certificates: your upload key AND the Play App Signing certificate (Play Console → your app → Test and release → App integrity). Google re-signs your app with the Play Signing key, so that is the certificate real installs present.
A package-only entry (no fingerprints) is accepted but weaker — the dashboard flags it — because any app declaring that package name would match.
iOS: allowed apps
Add your app's bundle identifier (e.g. com.example.app). Leave the team id
empty — the plugin does not currently send X-Ios-Team-Id.
Plugin version requirement
Android and iOS restrictions rely on identity headers the app sends, which only exist in native-update ≥ 3.2.0. An app built with an older version sends no identity and will be blocked by an Android/iOS restriction until you rebuild it on ≥ 3.2.0. Web origin restrictions work with any version.
The plugin attaches the headers automatically on its own update checks. If you run your own update-check fetch, read the identity first:
import { NativeUpdate } from 'native-update';
const id = await NativeUpdate.getAppIdentity();
// id = { platform, packageName?, certSha256?, certSha1?, bundleId? }
const headers: Record<string, string> = { 'X-API-Key': apiKey };
if (id.platform === 'android') {
if (id.packageName) headers['X-Android-Package'] = id.packageName;
if (id.certSha256) headers['X-Android-Cert-Sha256'] = id.certSha256;
if (id.certSha1) headers['X-Android-Cert-Sha1'] = id.certSha1;
} else if (id.platform === 'ios') {
if (id.bundleId) headers['X-Ios-Bundle-Id'] = id.bundleId;
}
What restrictions do and do not protect
- Web origin checks are strong — browser-enforced and not forgeable by page JS.
- Android/iOS identity headers are client-attested, not cryptographic
attestation — the same trust model as Google Maps API-key app restrictions.
They stop casual key reuse and quota abuse, but a determined attacker can
replay the headers with
curl. For hard device attestation, layer Play Integrity / App Attest yourself. - Restricting a key never leaks its allowlist: a blocked request only sees a
generic
API_KEY_RESTRICTEDreason plus its own observed origin/package.
Verify
After saving, the dashboard's API Keys table shows a summary
(2 origins · 1 Android · 1 iOS) instead of the amber Unrestricted badge,
and a Blocked requests counter appears once anything is rejected. Ship a
build (≥ 3.2.0) and confirm update checks still succeed from your real app while
a curl from an unlisted origin/app gets 403.