WooCommerce Membership Cards with QR Codes: A Practical Tutorial
If you sell memberships through WooCommerce — gym, co-working space, club, professional association — at some point a staff member is going to want to scan a phone at the door instead of looking up names in a list. This post is the build guide: data model, QR payload, scan endpoint, and the trade-offs between a “URL-only” QR and a signed-token QR that can’t be forged offline.
Scan orders, not spreadsheets. Order Barcodes for WooCommerce adds a scannable barcode and QR code to every order — built for check-ins, pickups, and fulfillment.
We’ll assume you’re already using WooCommerce + WooCommerce Memberships (or Subscriptions). If you’re picking a stack from scratch, jump to the comparison section.
What a membership QR card actually needs to do
Three jobs, in priority order:
- Identify the member at the door. Cashier scans, system says “Sarah Chen, Gold tier, expires 2027-03-14, paid up.” Sub-second.
- Reject expired or revoked memberships without a round-trip to a human.
- Be hard to forge. A static QR with
member_id=42printed on it is trivially copyable. If your membership unlocks anything valuable (entry, equipment, content), the QR needs a signature or short-lived token.
Most “membership card” tutorials skip job #3 entirely. They generate a QR pointing to /?member=42, which is fine for a yoga studio with a friendly front desk and worthless for anything with real access control.
The two architectures
Option A: QR contains a URL
Simple. The QR encodes https://yourstore.com/check-in/?token=abc123. Staff scans, browser opens, server looks up the token, returns a pass/fail page.
- Pros: trivial to build, works with any QR scanner app, no custom client.
- Cons: requires internet at the door, slow (browser load + redirect), staff has to look at a screen.
Use this if you’re a small business and the door has wifi.
Option B: QR contains a signed payload
The QR encodes a JSON Web Token (JWT) like:
eyJhbGciOiJIUzI1NiJ9.eyJtaWQiOjQyLCJ0aWVyIjoiZ29sZCIsImV4cCI6MTc4NDU2MDAwMH0.<sig>
The token contains member ID, tier, expiry, and is signed with a server-side secret. Your scanning app (custom PWA, or a barebones page on a tablet) verifies the signature locally. No server round-trip needed for the common “valid card” case — you only call the server when you need to check revocation.
- Pros: offline-friendly, fast, forgery-resistant.
- Cons: you need a scanning app, and you must rotate tokens (24-hour expiry is sane) so a stolen QR screenshot can’t be reused indefinitely.
Use this if you have any meaningful access control or do high-volume check-ins.
Building it: data model
Whichever architecture you pick, the data model is the same. Add three pieces of user metadata:
// On membership creation or renewal
update_user_meta( $user_id, 'pd_member_id', wp_generate_uuid4() );
update_user_meta( $user_id, 'pd_member_tier', $plan_slug );
update_user_meta( $user_id, 'pd_member_expires', $expiry_unix );
Hook this to wc_memberships_user_membership_created and wc_memberships_user_membership_status_changed. The UUID matters — never put the WordPress user_id in the QR. User IDs are sequential and leak the size of your member base.
Generating the QR
For Option A (URL), any QR plugin works. The Order Barcodes & QR Codes for WooCommerce plugin generates QRs at order/subscription creation time and can attach them to the order confirmation email or a downloadable PDF — that PDF becomes the member card.
For Option B (signed token), generate the JWT server-side and pass it to the QR generator:
use Firebase\JWT\JWT;
function pd_generate_member_token( int $user_id ): string {
$payload = [
'mid' => get_user_meta( $user_id, 'pd_member_id', true ),
'tier' => get_user_meta( $user_id, 'pd_member_tier', true ),
'exp' => time() + DAY_IN_SECONDS, // rotate daily
];
return JWT::encode( $payload, PD_MEMBER_SECRET, 'HS256' );
}
Then render the QR with that token as its data. Re-issue daily via WP-Cron and email the new card, or — better — push it to the member’s wallet (next section).
The check-in endpoint
Here’s a bare-bones REST endpoint for Option A or for revocation checks in Option B:
add_action( 'rest_api_init', function () {
register_rest_route( 'pd/v1', '/check-in', [
'methods' => 'POST',
'callback' => 'pd_handle_check_in',
'permission_callback' => 'pd_staff_can_check_in',
] );
} );
function pd_handle_check_in( WP_REST_Request $req ) {
$token = $req->get_param( 'token' );
try {
$decoded = JWT::decode( $token, new Key( PD_MEMBER_SECRET, 'HS256' ) );
} catch ( Exception $e ) {
return new WP_REST_Response( [ 'ok' => false, 'reason' => 'invalid_token' ], 200 );
}
$users = get_users( [ 'meta_key' => 'pd_member_id', 'meta_value' => $decoded->mid, 'number' => 1 ] );
if ( empty( $users ) || get_user_meta( $users[0]->ID, 'pd_member_revoked', true ) ) {
return new WP_REST_Response( [ 'ok' => false, 'reason' => 'revoked' ], 200 );
}
return new WP_REST_Response( [
'ok' => true,
'name' => $users[0]->display_name,
'tier' => $decoded->tier,
'expires' => $decoded->exp,
], 200 );
}
Two things worth flagging here: the permission callback should require an authenticated staff role (don’t expose check-in publicly — bots will probe it), and you should rate-limit it. A scanner hammering check-in at a busy event can look like an attack to Wordfence.
Apple Wallet and Google Wallet
A QR on a PDF works. A pass in Apple Wallet or Google Wallet is what members actually want — it auto-surfaces near the venue, doesn’t require digging through email, and updates remotely when the tier or expiry changes.
Two realistic paths:
- Roll your own with a PHP library like chiiya/passes. You’ll need an Apple Developer account ($99/yr) for Wallet pass certificates and a Google Cloud project with the Wallet API enabled. Budget a day or two of work plus the recurring cert renewal.
- Use PassKit (or similar SaaS). Pay per pass issued. Saves you the Apple/Google cert dance. Worth it if you have under ~5,000 active members; rolling your own pays off above that.
Either way, the QR inside the pass is the same JWT you generated above. The wallet is a delivery mechanism, not a security boundary.
Which stack should you actually use?
Honest comparison of the three common starting points:
| Stack | Best for | Membership card story |
|---|---|---|
| WooCommerce Memberships ($199/yr) | Stores already on Woo, content-gating + physical perks | No native card — you build it (this post) |
| Paid Memberships Pro + Membership Card add-on | Membership-first sites, association/club use | Built-in card with QR via shortcode/block, weaker WooCommerce integration |
| WooCommerce Subscriptions + your own meta | Fitness/recurring services with simple tiers | Maximum flexibility, most code to write |
If your membership card is the product (a club membership where the card is the proof), PMPro’s add-on is faster to ship and worth a look. If you’re already running a Woo store and memberships are an upsell to physical or digital products, build it on Woo Memberships and treat the QR card as a feature, not a project.
Wiring it together
Order of operations for a real implementation:
- Add member meta on membership creation (UUID, tier, expiry).
- Generate signed token, render QR with Order Barcodes & QR Codes or a library, attach to confirmation email or PDF.
- Build a tablet check-in PWA (or use any JWT-aware QR scanner). For tiny ops, a phone scanning to a
/check-inURL is fine. - Add a daily WP-Cron to re-issue tokens and (if you went with wallet passes) push updates.
- Add a “revoke membership” admin action that sets
pd_member_revokedmeta — this is your kill switch.
If you only do step 1–2, you have a working scannable card by the end of an afternoon. The rest is the difference between “scannable” and “secure.”
FAQ
Can I do this without a paid plugin? Yes. WooCommerce Memberships isn’t required — you can drive the same logic off WooCommerce Subscriptions or even raw user roles. The QR generation is the only piece that’s annoying to roll yourself, and that’s what plugins like Order Barcodes & QR Codes solve.
How do I prevent QR sharing? You can’t fully prevent screenshot-sharing of a static QR. Rotating tokens (24h expiry) makes shared screenshots stale by the next day. For high-value access, pair the QR with a photo on the wallet pass and have staff glance at both.
Does it work with subscription pauses? Yes if you flip a meta flag (pd_member_revoked or set pd_member_expires to now) when the subscription pauses. Hook woocommerce_subscription_status_on-hold.
What about chargebacks? Same hook pattern — listen for failed renewal payments and revoke immediately. A QR card that keeps working after a chargeback is a leak.
Ready to speed up check-ins and fulfillment? Every order gets a unique code you can scan from any phone camera or USB scanner — no third-party service, no order data leaving your site.
Want help applying this?
Tell us your workflow and we'll point you to the right plugin or next step.