Skip to main content

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 of example.com (not the apex; add https://example.com separately 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.

Add BOTH certificates

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_RESTRICTED reason 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.