Skip to main content
Every app declares itself with a manifest served at /.well-known/bankroll.jwt on its own origin. Bankroll fetches it over HTTPS to learn three things: what your app is called, which capabilities it uses, and the address that receives its payments. Because Bankroll reads it from your origin, the baseline manifest is bound to your app by where it’s served. It is an Unsecured JWT with alg: none and an empty signature; you need no manifest-signing key. Verified apps serve a Bankroll-signed version and can declare an app key for server API calls.

Claims

Decoded, the manifest payload is a small JSON object:

App key and notifications

appKey names the public key your server uses for app authentication. The private key stays on your server. manifestRoute includes the public key by default when the app credential is configured. To enable notifications, set push: () => true on manifestRoute; this declares capabilities.push: true. A false or null result omits the capability. Having an appKey alone does not enable notifications, and declaring either field does not replace Bankroll verification. Legacy public-key strings in capabilities.push remain supported. A valid appKey takes precedence over that string. See legacy keys for configuration and upgrade behavior.

Your icon

Your icon is not a manifest claim. Serve a square PNG (256×256 or larger) at the fixed path /.well-known/bankroll-icon.png on your origin — a sibling of the manifest. Bankroll fetches it from there and shows it on your app’s tile and in Connected Sites; until you serve one, it shows a monogram of your app’s name. A manifest that still includes an iconUrl claim remains valid — the claim is simply ignored.

Updating your icon — iconDigest

If you replace the PNG at the same path, users who have already connected your app can keep seeing the old one. To make an update reach them, add the OPTIONAL iconDigest claim: a hash of the icon bytes as a Subresource Integrity hash-expression (sha256-<base64>). Bankroll treats a changed iconDigest as a manifest change, so your new icon propagates to connected users. Compute it from the exact bytes you serve, and omit the claim if you don’t serve an icon:
On Next.js, pass it to manifestRoute’s optional iconDigest and the SDK handles the omission rules.
The recipient is read from the fetched manifest, never supplied by your page. For a session-only app, omit payments and set session: true alone.

Rules

  • Served on your origin. The manifest must live at the exact path /.well-known/bankroll.jwt on the same origin your app loads from. Bankroll matches by origin (scheme + host + port), so https://acme.example and https://app.acme.example are different apps with different manifests — and each manifest’s sub must equal its own origin.
  • application/jwt. Serve it with Content-Type: application/jwt, no larger than 8 KiB, over HTTPS. Your app URL must be HTTPS too.
  • Unsigned baseline. Its header is exactly { "alg": "none", "typ": "bankroll-app-manifest+jwt" }, and the signature segment is empty — the token is header.payload., with a trailing dot and nothing after it. Verified apps instead serve the Bankroll-signed JWT verbatim.
  • Must be valid. If the manifest is missing, unreachable, malformed, the wrong media type, or its sub / aud / manifestVersion are wrong, session() and charge() reject with manifest_error.
  • Declare what you use. Any capability you call must be enabled here. Calling one you didn’t declare rejects with capability_not_registered. See error codes.
  • Changing it re-asks your users. A user’s grant is bound to the exact manifest they approved, so editing any claim — including name and supportUrl — means everyone who already connected your app is asked to approve it again on their next visit. Treat these as settings you pick once. Where a value may need to move later, point it somewhere you control and redirect: https://acme.example/support can change destination freely, but editing the claim itself cannot.

Serving it

On Next.js, manifestRoute() from @joinbankroll/sdk/next is the whole route. You declare what is genuinely your app’s — its name, where it launches, where payments settle — and the SDK owns the format, so a change to the format arrives with an npm upgrade rather than a diff you have to write:
sub is taken from the request’s own host, so preview deployments, custom domains, and local tunnels each declare themselves correctly with nothing to configure. payments returning null omits the capability rather than advertising an address the app cannot honor — which is what treasuryAddress() does before a treasury key is set. supportUrl and appTokens are omitted from the payload entirely when they resolve to nothing, because an empty claim is still a claim and would re-ask every connected user for consent once it gained a value.

Any other stack

The unsigned baseline is two Base64URL-encoded JSON segments joined by dots, with an empty signature. Building it by hand takes a few lines:
Before testing your app, make sure the manifest is live and well-formed — fetch it and decode the middle segment to check your claims: