GSheet Membership

Description

Access — the login and page gate

GSheet Membership lets you use a private Google Sheet as your membership database.

  • Truly private — the sheet is never shared publicly. Access is granted exclusively to a Google Service Account that you control.
  • Login gate — only users whose email address appears in the sheet can log in to WordPress.
  • Membership codes — each row contains an email address and a membership code (e.g. GOLD, SILVER, PLATINUM).
  • Per-page restrictions — set required membership code(s) on any page or post via a sidebar panel.
  • Flexible access-denied handling — redirect to a custom URL or show a configurable inline message.
  • Caching — sheet data and OAuth2 access tokens are cached as WordPress transients.

How it works

The plugin authenticates using a Google Service Account — a special non-human Google account your server uses to read and update the sheet. You share the spreadsheet with it; no one else can access the sheet. Credentials are stored securely in your WordPress database and are never exposed in the browser or source code.

Built for chapter-based organizations

Most membership plugins assume one site and one member list. Many real organizations don’t work that way: a national society with local chapters, an alumni association with regional groups, a union with locals. GSheet Membership was designed for exactly that structure:

  • One national roster, many chapter rosters — the national organization keeps the master membership sheet; each chapter keeps its own local roster sheet. Both live in ordinary Google Sheets the organization owns.
  • Each chapter runs its own WordPress site — with its own pages, branding and admins, while membership checks stay consistent: a chapter site can honour the national roster, its local roster, or both.
  • Chapter-local sales and email — in chapter mode, membership sales write to the chapter’s own roster, and group email (Pro add-on) goes to the local chapter list.
  • No per-chapter SaaS fees — association platforms typically charge every chapter a monthly subscription and hold your member data. Here the data stays in your own Google Sheets and each chapter site is just WordPress.

Chapter mode is provided by the GSheet Membership — Chapters add-on; the free plugin covers everything a single-roster organization needs.

External services

This plugin connects to the following third-party services to provide its core functionality:

Google Sheets API and Google OAuth2 (developers.google.com)
Used to read and write membership data to your private Google Sheet, and to authenticate as your Google Service Account. When a page’s access is checked, when a purchase is recorded, or when you save settings and test the connection, the plugin sends: your Service Account’s signed authentication request to https://oauth2.googleapis.com/token, and membership-related row data (emails, membership codes, and — depending on which features you use — names, addresses, and purchase details) to https://sheets.googleapis.com. This only occurs because you have configured a Google Service Account and Spreadsheet ID; no data is sent to Google until you do so.
Google APIs Terms of Service: https://developers.google.com/terms
Google API Services User Data Policy: https://developers.google.com/terms/api-services-user-data-policy
Google Privacy Policy: https://policies.google.com/privacy

Stripe (stripe.com)
Used to process membership purchases via Stripe Checkout. When a buyer completes a purchase, their payment is handled entirely on Stripe’s hosted checkout page (card details never pass through this site); the plugin sends the buyer’s selection and price to Stripe to create the Checkout Session, and receives a webhook confirming payment so it can update your Google Sheet. This only occurs when Stripe is enabled and configured with your API keys.
Stripe Terms of Service: https://stripe.com/legal/ssa
Stripe Privacy Policy: https://stripe.com/privacy

GSheet Connect (access-manager-pro.replit.app) — free tier only
The plugin’s hosted payment service, used when you take card payments without the Pro add-on. It handles “Connect with Stripe” onboarding, creates the Stripe Checkout Session for each purchase (applying the disclosed platform commission), processes refunds you issue from the dashboard, and relays Stripe’s payment confirmations back to your site. The plugin sends: your connected Stripe account ID, the purchase amount, currency and description, the buyer’s email address, and your site’s webhook address at https://access-manager-pro.replit.app. The service stores no member data; payment processing itself is performed by Stripe. This only occurs after you click “Connect with Stripe” — sites using their own Stripe API keys (including all Pro sites) never contact this service.
Service overview: https://access-manager-pro.replit.app

PayPal (paypal.com) — requires the GSheet Membership Pro add-on
Used to process membership purchases via PayPal Checkout as an alternative to Stripe. Only active if you install the Pro add-on and configure your PayPal credentials; the free plugin does not send any data to PayPal on its own.
PayPal User Agreement: https://www.paypal.com/us/legalhub/paypal/useragreement-full
PayPal Privacy Statement: https://www.paypal.com/us/legalhub/paypal/privacy-full

Mailchimp (mailchimp.com) — requires the GSheet Membership Pro add-on
Optional delivery method for the Group Email feature. When you choose Mailchimp delivery, connect an API key and select an audience, each send first syncs that Mailchimp audience to your current membership filter — the plugin sends member email addresses and names from your Google Sheet to https://.api.mailchimp.com to add, update, or archive audience members — and then creates and sends the campaign through Mailchimp. Only active if you install the Pro add-on, choose Mailchimp as the delivery method, and configure your Mailchimp API key and audience; the free plugin does not send any data to Mailchimp on its own.
Mailchimp Standard Terms of Use: https://mailchimp.com/legal/terms/
Intuit Mailchimp Privacy Statement: https://www.intuit.com/privacy/statement/

Installation

  1. Upload the membership-google-sheets folder to /wp-content/plugins/ (or upload the ZIP under Plugins → Add New → Upload Plugin).
  2. Activate the plugin through the Plugins screen.
  3. Follow the Google setup steps below.
  4. Go to Membership Access (top-level menu in the WordPress admin sidebar) and configure the plugin.
  5. Click Save & Test Connection to verify the service account can read your sheet.

Google setup (~5 minutes)

  1. Go to https://console.cloud.google.com/ and create or select a project.
  2. Enable the Google Sheets API (APIs & Services Library search “Sheets”).
  3. Create a Service Account (IAM & Admin Service Accounts Create).
  4. Generate a JSON key for the service account (Keys tab Add Key JSON). Download the file.
  5. Open your Google Sheet Share add the service account email (looks like name@project.iam.gserviceaccount.com) as an Editor (required for the Sales feature and the Pro add-on’s Group Email; Viewer is enough if you only use page protection). The sheet stays private to everyone else.
  6. In Membership Access (top-level menu in the WordPress admin sidebar), paste the full contents of the JSON key file and enter your Spreadsheet ID.

Restricting pages

  1. Edit any page or post in WordPress.
  2. Find the Membership Access panel in the editor sidebar.
  3. Enter the required membership code(s), comma-separated (e.g. GOLD, PLATINUM).
  4. Save the page.

Codes are compared case-insensitively. Leave the field blank for unrestricted access.

Configuration reference

Setting
Description

Service Account JSON
Full contents of the JSON key file downloaded from Google Cloud Console

Spreadsheet ID
The ID from the spreadsheet URL (between /d/ and /edit)

Sheet (Tab) Name
The tab name inside the spreadsheet (default: Sheet1)

Email Column
Column letter containing email addresses (default: A)

Membership Code Column
Column letter containing membership codes (default: B)

Header Row Number
Row number of the header; data starts on the next row (default: 1)

Cache Duration (seconds)
How long to cache the sheet data locally (default: 300). Set 0 to disable.

Login Redirect URL
Where to send unauthenticated visitors trying to access a restricted page

Access-Denied Redirect URL
Where to redirect logged-in users without the required membership

Access-Denied Message
Message to display instead of redirecting (if no redirect URL is set)

FAQ

Does the Google Sheet need to be shared publicly?

No — never. You only share it with the service account email address. It remains completely private to all other users.

What PHP extensions are required?

The OpenSSL extension is needed to sign the JWT for authentication. It is enabled by default on virtually all shared and managed WordPress hosts (including WP Engine, Kinsta, SiteGround, Flywheel, and others).

Are administrators ever blocked?

No. Users with the manage_options capability always bypass all membership checks.

Where is the Service Account JSON stored?

It is stored in your WordPress database (the wp_options table), protected by the same security as your WordPress installation. It is never sent to the browser or written to a file.

How quickly do membership changes take effect?

After the cache expires (default 5 minutes / 300 seconds). Reduce the cache duration in settings or click Clear Cache to force an immediate refresh.

What happens if the Sheets API is unreachable?

The plugin logs an error and denies access as a safe default. Use Save & Test Connection in settings to verify connectivity.

Authenticated mailbox sending (Pro add-on)

With the GSheet Membership Pro add-on, you can connect a real mailbox (Google sign-in or an App Password) on the main settings page and send ALL plugin email — login verification codes, purchase confirmations, gift notices and group emails — from that account. Because mail truly originates from the mailbox, SPF/DKIM/DMARC align and messages stop landing in spam, with no separate SMTP plugin needed.

Email-in: forward an email to the roster (Pro add-on)

Group Email and Email-in require the GSheet Membership Pro add-on. Group emails can be delivered by the site’s own batched mailer or, optionally, through a connected Mailchimp account (the audience is synced to your membership filter before every send). With Pro active, you can send a group email simply by forwarding a message to a dedicated mailbox (e.g. blast@example.org). The plugin checks that mailbox on a schedule (IMAP) and any message from an authorized sender is queued to the roster through the same engine as the Group Email compose form — same audience rules (chapter sites always email only their local roster), individual per-member delivery, unsubscribe links, throttled background sending, and the per-recipient send log with retry/cancel.

Setting it up

  1. Create a dedicated mailbox for intake (a Gmail / Google Workspace account works well). Don’t reuse a personal inbox — every unread message is examined.
  2. For Gmail / Google Workspace: enable IMAP (Gmail Settings Forwarding and POP/IMAP), turn on 2-Step Verification for the account, and create an App Password (Google Account Security App passwords). The regular account password will NOT work over IMAP.
  3. In Membership Access Group Email, fill in the Email-in card: host imap.gmail.com, port 993, the mailbox address as username, and the App Password.
  4. Add the email addresses allowed to trigger a send (one per line) and — strongly recommended — a required subject keyword.
  5. Enable polling, click Test connection, then send yourself a trial: forward a message from an allowlisted address (with the keyword in the subject) and click Check mailbox now.

Why the subject keyword matters (spoofing)

The From: address of an email can be forged by anyone (spoofing) — an allowlist alone is not sufficient protection for something that can email your whole membership. The required subject keyword acts as a shared secret: only people who know it can trigger a send, even if they forge an authorized From: address. The keyword is stripped from the subject before delivery, so members never see it.

Audience & attachments

  • Embed a code filter in the subject to target membership levels: Meeting notes [codes: GOLD,SILVER]. The tag is removed before delivery. Without a tag, the membership code filter saved on the Group Email page is applied; if that filter is blank, all members receive the message. Works on both main and chapter sites.
  • Chapter sites always send to the local chapter roster only — the code filter narrows within that roster.
  • Attachments are uploaded to the media library. Files up to 5 MB are attached to the outgoing email; larger files (and any additional attachments beyond the first) become download links in the message body.
  • Processed messages are marked read in the mailbox and their Message-IDs are recorded, so nothing is ever sent twice. Messages from unauthorized senders are marked read, logged (when WP_DEBUG is on) and never sent.

Sales — sell memberships

The Sales half of GSheet Membership puts a configurable buy-membership picker on any page and, on a successful payment, writes (or updates) the buyer’s row in your private Google Sheet so they can immediately log in on your site.

  • Three-dimensional picker — buyers choose Delivery (Electronic / Print / Both) × Duration (1-Year and Lifetime included by default; the Pro add-on adds custom levels and multi-year durations) × Postage (Regular, First Class, Canada/Mexico, Other International), and the price updates live as they change selections.
  • Discrete pricing matrix — you set the exact price for every saleable combination in the admin. Leave any cell blank to mark that combination as not for sale: it is hidden / disabled in the picker and rejected server-side.
  • Stripe Checkout — redirect-style checkout (no card form on your site, lower PCI burden). Test mode and Live mode are stored side-by-side and toggled with one switch.
  • PayPal Standard Checkout (requires the Pro add-on) — buyers can optionally pay via PayPal instead of (or in addition to) Stripe. Both gateways share the same sheet-upsert pipeline, purchase log, and retry UI.
  • Sheet upsert — on a verified webhook the plugin first tries to update the row that matches the buyer’s email; if there is no row, it appends one. Renewals extend an existing future expiration date instead of overwriting it.
  • Reliable webhook pipeline — per-event and per-email atomic locks prevent duplicate writes from concurrent webhook redeliveries; a sheet-write failure returns HTTP 500 so Stripe retries, and the admin sees a notice + a per-row “Retry” button on the Recent Purchases page.
  • Buyer confirmation email — configurable subject and body with placeholders (first name, selection, amount, code, expiration, login URL, etc.).
  • Admin direct-apply — a separate [gsms_admin_picker] shortcode lets a site administrator apply a membership directly to the sheet without taking a payment, perfect for comp memberships, cheque / cash-in-hand orders, or back-filling an old purchase.

Requirements

  • PHP 7.4+ with the standard openssl, hash, and mbstring extensions (already enabled on every major WordPress host).
  • WordPress 5.9+.
  • The access features (above) configured with a working Service Account — the sales pipeline reuses the same sheet-writer.
  • A Stripe account for live or test payments (a PayPal developer account too, if you use the Pro add-on’s PayPal checkout).

Selling setup

  1. Configure the Access settings first (see Installation above) and confirm the Save & Test Connection button succeeds.
  2. Go to Membership Sales in the WordPress admin sidebar. Every sub-page shows a “Setup” notice at the top reminding you which shortcode to put on which page.
  3. Fill in the Pricing Matrix, Code Matrix, and Payments sub-pages (see Configuration below).
  4. Create a page containing only the shortcode [gsma_membership_purchase] — that is the buy page.
  5. Create (or pick) a thank-you page and put [gsma_user_info] on it (that shortcode shows the buyer’s status with an inline login form when they’re not yet signed in). Select that page on the Settings sub-page under Thank-you page.

Shortcodes

  • [gsma_membership_purchase] — renders the full picker (Delivery × Duration × Postage) with live total and the Buy membership button. Place this on the page where you want to take payments.
  • [gsms_admin_picker] — admin-only variant of the picker that applies a membership directly to the sheet without any payment. Only site administrators (manage_options) see it; everyone else sees a polite notice.
  • [gsma_user_info] — place it on the thank-you page so buyers see their membership status (and an inline login form when they aren’t logged in).

Admin sub-pages

  • Pricing Matrix — one row per Delivery, one column per Postage, repeated for each Duration. Type a price into a cell to make it saleable; leave it blank to make that combination unavailable.
  • Code Matrix — the membership code written into the buyer’s sheet row for each combination. Every membership level gets its own row of codes. Levels beyond the built-in 1-Year and Lifetime, including multi-year durations, can be added with the Pro add-on; each new level gets its own Code Matrix row automatically. A warning is shown if any saleable cell is missing a code.
  • Payments — enable/disable Stripe (and PayPal, with the Pro add-on); mode switch (Test / Live for Stripe, Sandbox / Live for PayPal); keys and secrets for each mode; webhook signing secret. The webhook URLs are shown here for paste-into-Stripe (and paste-into-PayPal with Pro).
  • Settings — currency, thank-you page picker, optional override of the access settings’ expiration column, buyer confirmation email templates, and option labels.
  • Recent Purchases — the last 200 webhook deliveries with their outcome (gateway status + sheet status). Failed sheet writes show an inline Retry form; completed Stripe and PayPal rows show a Refund control.

Configuration: webhooks

Stripe:

  1. Developers → Webhooks → Add endpoint.
  2. Paste the Stripe Webhook URL shown on the plugin’s Payments page (ends in /wp-json/gsma-sales/v1/stripe-webhook).
  3. Subscribe to a single event: checkout.session.completed.
  4. Copy the resulting Signing secret (whsec_...) and paste it into Webhook signing secret on the plugin’s Payments page.

PayPal (requires the Pro add-on):

  1. In the PayPal developer dashboard → My Apps & Credentials → your app → Add webhook.
  2. Paste the PayPal Webhook URL shown on the plugin’s Payments page (ends in /wp-json/gsma-sales/v1/paypal-webhook).
  3. Subscribe to CHECKOUT.ORDER.APPROVED and PAYMENT.CAPTURE.COMPLETED.
  4. Copy the resulting Webhook ID into the matching field on the plugin’s Payments page.

How to test without real money

Almost everything in this plugin can be exercised before you ever sign up for Stripe or PayPal.

1. Picker UI / pricing layout (no gateway at all)

  • Fill in just the Pricing Matrix (real prices) and the Code Matrix (any short string like TEST).
  • Visit your buy page. The picker renders, the cheapest combination is preselected, and clicking different options updates the live total.
  • Leave a cell blank in the Pricing Matrix and reload — that postage / duration is greyed-out and unselectable, and the Buy membership button is disabled if the current selection has no price.

2. End-to-end with Stripe in TEST mode (no real money)

  1. Sign up for a free Stripe account. No bank details are needed for test mode.
  2. On the plugin’s Payments page, enable Stripe, switch Mode to “Test”, paste your pk_test_... and sk_test_... keys, and save.
  3. Install the Stripe CLI on any laptop and run:

    stripe listen –forward-to https://yoursite.example/wp-json/gsma-sales/v1/stripe-webhook

    The CLI prints a whsec_... signing secret — paste that into Webhook signing secret and save.

  4. Visit the buy page, fill in any email/name, click Buy membership, and on the Stripe checkout page use the official test card:
  • Card number: 4242 4242 4242 4242
  • Expiry: any future date   CVC: any 3 digits   ZIP: any 5 digits
  1. After the redirect, open Membership Sales → Recent Purchases in the admin. You should see a new entry with paid from Stripe and inserted (or updated) for the sheet.

What happens if Stripe or PayPal sends the same event twice?

The first delivery is processed normally. Any duplicate of the same event is identified and returns HTTP 200 with status: duplicate without writing to the sheet. Two concurrent deliveries for the same event are serialized via an atomic per-event lock; the loser receives HTTP 409 so the gateway retries it. From 1.7.13 each processed event is stored as its own wp_options row with a 72-hour TTL — there is no fixed-size eviction window that could cause old-but-recent events to fall off and be reprocessed.

What happens if the Google Sheet write fails?

The plugin returns HTTP 500 to the gateway (so it retries delivery automatically), records a “failed” entry on the Recent Purchases page, and shows a persistent admin notice until the next successful write. The buyer confirmation email is not sent for a failed write — only a successful upsert triggers it. You can manually rerun the upsert from the Recent Purchases page.

Is the buyer’s payment ever lost if the sheet write fails?

No. The gateway has captured the payment regardless of what happens on your site. The sheet write being deferred (and retried) means the buyer’s membership row catches up automatically.

Can lifetime memberships and renewals coexist?

Yes. Renewals extend the existing expiration date forward, so an early renewal never shortens the buyer’s coverage. A lifetime purchase clears the expiration cell entirely.

What if a buyer clicks Cancel at PayPal and then tries to pay again?

The cancelled checkout row is kept visible in Recent Purchases for auditing, but it never blocks a retry. If the gateway somehow delivers a capture event for an order the cancel handler already marked cancelled, the finalisation pipeline now detects and recovers that row automatically rather than treating it as already-done.

Does the free plugin charge a fee on sales?

It depends on how you take card payments:

  • Stripe Connect (free, the default since 3.5.0): click Connect with Stripe on the Payments screen — no API keys to paste, Stripe hosts the onboarding. A 2% platform commission is deducted from each card sale (in addition to Stripe’s own standard processing fees). The current rate is always shown on the Payments screen before you connect.
  • Your own API keys (Pro add-on): the Pro add-on is commission-free — Pro sites use their own Stripe keys with a 0% platform fee, permanently. Deactivating Pro simply reverts the site to Connect mode without losing any settings.
  • Legacy free installs that saved their own Stripe keys before 3.5.0 keep working with no commission; the keys remain a supported deprecation path until you choose to connect.
  • PayPal (a Pro feature) carries no platform commission either.

The commission is applied server-side by the plugin’s hosted Stripe Connect service when the Checkout Session is created; the plugin itself never computes or transmits the fee amount.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“GSheet Membership” is open source software. The following people have contributed to this plugin.

Contributors

Translate “GSheet Membership” into your language.

Interested in development?

Browse the code, check out the SVN repository, or subscribe to the development log by RSS.

Changelog

4.5.8

  • Multi-address roster cells now split on any separator: a second email address added on a new line inside the cell, or separated only by a space, is now recognized. Previously only commas and semicolons split, so an address on its own line silently fused with its neighbor and disappeared from logins, rosters, and email lists.

4.5.7

  • Escaped result-box output at the point it’s printed, rather than relying on callers to have already done so, resolving a Plugin Check finding. No visible change.

4.5.6

  • Action-result boxes can now be placed next to the exact form or buttons that produced them: a result may carry a placement tag, and the page renders tagged results at the matching spot (used by the Pro Group Email send/test buttons). Untagged results keep rendering under the page title.

4.5.5

  • Action results and save confirmations now appear inline on the plugin’s own admin pages — directly under the page title, next to the forms that triggered them — instead of at the very top of the admin screen.

4.5.4

  • Admin feedback (save confirmations, action results, upload results, the Stripe connection status and the webhook-failure warning) is now shown in the plugin’s own message boxes instead of standard WordPress notice banners. Admin-cleanup and white-label plugins commonly hide or relocate every standard notice banner, which silently swallowed this feedback on affected sites.

4.5.3

  • Admin notices (save confirmations, error reports, action results shown after a redirect) now survive hosts whose object cache drops transients: each notice is also stored as a short-lived option and read from there first, so redirect round-trip feedback no longer disappears silently.

4.5.2

  • Reworded comments and documentation describing what the Pro add-on supplies, consistently phrased as capability it adds on top of the free plugin, never as something the free plugin restricts or withholds. No functional changes.

4.5.1

  • Free-checkout polish: when a free ($0) signup can’t be recorded, the buyer now always sees the generic translated failure message — the specific reason stays in the admin purchase log, matching every other checkout failure. Internal documentation now lists the ‘free’ gateway.

4.5.0

  • Free ($0) memberships are now supported: enter 0 in a Pricing Matrix cell to offer that combination at no charge. Buyers who pick it skip the payment step entirely — the membership is recorded and the confirmation email sent immediately, and the picker shows “Free” instead of a zero amount. A blank cell still means not-for-sale.

4.4.2

  • The Membership Levels block on the Pricing Matrix screen is now its own form with a “Save Levels” button for choosing which levels are offered for purchase. The price grid below keeps its own Save button for prices only.

4.4.1

  • The plugin no longer stores any Stripe API keys of its own — card payments on the free plugin always run through the hosted “Connect with Stripe” onboarding. Sites using their own Stripe keys via the Pro add-on are unaffected: the add-on now stores those keys itself. The PayPal enable toggle likewise moved to the Pro add-on’s settings defaults — the free plugin no longer defines any PayPal setting.

4.3.6

  • A fresh install’s two default membership levels are now 1-Year and Lifetime (previously 1-Year and 3-Year). Existing sites are unaffected — the defaults only apply when no levels have been saved yet. Pairs with 4.3.5: a Lifetime member on a stock free install can change their delivery/postage at the correct price differential, no Pro add-on required.

4.3.5

  • Existing members of a non-expiring level can now change their delivery/postage option without the Pro add-on: the plugin computes the price differential itself and charges exactly that. Same-option and lower-priced requests are declined with clear messages. The Pro add-on’s role narrows to gift-upgrade validation.

4.3.4

  • Readme: added a Mailchimp entry to the External services disclosure section (Pro’s optional Group Email delivery method). No functional changes.

4.3.3

  • Wording: the plugin header description now reflects the current Pro feature set (level editor, Mailchimp delivery option, authenticated mailbox sending, PDF watermarks, large-file links). No functional changes.

4.3.2

  • Wording: the Pro add-on summaries (settings-page teaser card and readme) now mention the optional Mailchimp delivery for Group Email. No functional changes.

4.3.1

  • Wording: the Pro teaser card on the Access settings page now describes the Pro add-on’s level editor instead of the outdated “multi-year and Lifetime durations” line (3-Year is included in the free plugin as of 4.3.0).

Earlier entries are in changelog.txt inside the plugin.