Generate wallet passes from any runtime.
passmint is an open-source TypeScript library for creating Apple Wallet and Google Wallet passes. One unified schema, two outputs — a signed .pkpass for iOS and a Google Wallet save-link JWT for Android. Web Crypto only, so it runs on Cloudflare Workers, Vercel Edge, Deno, Bun, and Node 20+ without polyfills.
When to use passmint
The passmint package is ideal when you want full control over pass generation — you manage your own Apple certificates, Google service accounts, and signing infrastructure. It gives you the lowest-level building block: take a pass definition in, get signed bytes or a save-link out.
This is a good fit if you:
- Need to run on edge runtimes or serverless workers
- Want to self-host the entire pass pipeline
- Are building a platform that issues passes on behalf of others
- Prefer open-source dependencies you can audit and fork
When to use the Passmint platform
Generating a pass is only the first step. In production, you also need to distribute passes to holders, track installs and engagement, handle certificate renewals, push live updates to installed passes, and process webhook events — all while managing templates across your team.
The Passmint platform handles all of this so you can focus on your product:
- Visual designer — design passes in a live phone-frame preview, no code required
- Managed certificates — no .p12 files, no WWDR chains, no renewal calendar
- Distribution tracking — views, downloads, and installs from day one
- Live updates — push field changes to passes already in a holder's wallet
- Webhooks — get notified when passes are installed, updated, or removed
- REST API + Node SDK — typed client with retries, idempotency, and webhook verification
Many teams start with passmint for prototyping and move to the platform when they need the full lifecycle. Get started with the platform →
Install
npm install passmintRequires Node 20+ or any runtime with Web Crypto. ~21 KB gzipped.
Apple Wallet
Load your Apple Pass Type ID certificate, WWDR intermediate, and private key once, then sign passes. Keys must be in PKCS#8 PEM format — convert with openssl pkcs8 -topk8 -nocrypt -in key.pem -out key.pkcs8.pem.
import { Pass, SigningMaterial } from "passmint"
const material = await SigningMaterial.fromPem({
signerCertPem: process.env.APPLE_PASS_CERT!,
wwdrPem: process.env.APPLE_WWDR_CERT!,
privateKeyPkcs8Pem: process.env.APPLE_PASS_KEY!,
})
const signed = await Pass.eventTicket({
passTypeIdentifier: "pass.com.example.event",
serialNumber: crypto.randomUUID(),
teamIdentifier: "ABCD1234EF",
organizationName: "Example",
description: "Concert ticket",
images: { icon: { x2: { bytes: iconPng } } },
barcodes: [{ format: "qr", message: "TICKET-xyz" }],
})
.primaryField({ key: "event", label: "Event", value: "Beyoncé Live" })
.secondaryField({ key: "loc", label: "Location", value: "Apple Park" })
.sign(material)On Cloudflare Workers, build SigningMaterial once per isolate and reuse it across requests for best performance.
Google Wallet
Use a Google Cloud service account to generate a save-link JWT. Drop the resulting URL into an <a> tag with Google's "Add to Google Wallet" button.
import { Pass, GoogleSigningMaterial } from "passmint"
const google = await GoogleSigningMaterial.fromServiceAccount({
clientEmail: serviceAccount.client_email,
privateKeyPkcs8Pem: serviceAccount.private_key,
issuerId: "3388000000000000",
})
const pass = Pass.eventTicket({
passTypeIdentifier: "pass.com.example.event",
serialNumber: "ticket-42",
teamIdentifier: "ABCD1234EF",
organizationName: "Example",
description: "Concert ticket",
images: { icon: { x2: { bytes: iconPng } } },
barcodes: [{ format: "qr", message: "TICKET-42" }],
}).build()
const url = await pass.toGoogleSaveLink(google, {
origins: ["example.com"],
})
// → "https://pay.google.com/gp/v/save/<jwt>"One schema, two wallets
The same Pass produces both outputs — define your pass once:
const pass = Pass.eventTicket({ /* ... */ }).build()
const pkpass = await pass.sign(appleMaterial)
const saveLink = await pass.toGoogleSaveLink(googleMaterial, {
origins: ["example.com"],
})Pass styles
Five styles are supported, each mapping to the corresponding Apple and Google pass types:
Pass.eventTicket()— event ticketsPass.boardingPass()— air, train, bus, boatPass.storeCard()— loyalty cardsPass.coupon()— coupons and offersPass.generic()— generic passes
Per-style field-count limits are enforced at construction time. Use the fluent builder (Pass.eventTicket(...).primaryField(...)) or the raw object API (Pass.from(...)).
Output formats
signed.toUint8Array() // Promise<Uint8Array>
signed.toStream() // ReadableStream<Uint8Array>
signed.toResponse(init?) // HTTP Response with content-type settoResponse() sets Content-Type: application/vnd.apple.pkpass and a Content-Disposition: attachment header so the pass installs on tap.
Platform-specific escape hatch
For fields the unified schema doesn't model — smart-tap redemption, rotating barcodes, Apple semantic tags — use applyRaw:
Pass.eventTicket({
// ...
applyRaw: {
apple: { sharingProhibited: true },
google: { smartTapRedemptionValue: "nfc-payload" },
},
})applyRaw.apple deep-merges into pass.json. applyRaw.google deep-merges into the generated Google object.
Supported runtimes
Works anywhere with Web Crypto, Web Streams, and Uint8Array. No node:* imports, no Buffer.
- Node.js 20+
- Cloudflare Workers
- Vercel Edge Functions
- Deno
- Bun
- Supabase Edge Functions
- Netlify Edge Functions
Errors
Every throw extends PassmintError with a stable code property:
PassmintSchemaError— input validation (includes full Valibot issue list)PassmintRenderError— Apple / Google render layerPassmintSigningError— CMS signing, key import, unsupported key formatPassmintPackagingError— ZIP assemblyPassmintGoogleError— Google save-link JWT