MemberPress reference
This page is for developers and the MemberPress team. It explains what Plandalf for MemberPress sends to Plandalf, what Plandalf sends back, and what it writes to MemberPress. For setup, see the MemberPress guide.
The shape of it
WordPress + MemberPress Plandalf
─────────────────────── ────────
Connect with Plandalf ─── consent + code ───────▶ API key for this site
◀── key (server to server)
─── register endpoint ────▶ Signed event delivery
Plandalf tab ────────── link price ↔ membership ─▶ Links (+ drift)
Membership saved ────── refresh snapshot ────────▶ Links
Membership page ─────── SDK mount(design, price) ▶ Checkout
│ paid
/wp-json/plandalf/v1/events ◀── invoice.paid ──────┘ (signed)
├─ find or create the WordPress user
├─ MeprSubscription (subscr_id = Plandalf subscription id)
└─ MeprTransaction (trans_num = Plandalf invoice number)
Daily check ─────────── GET subscription ────────▶ Subscription state
Account "Cancel" ────── cancel subscription ─────▶ Cancel at period endThree rules hold throughout:
- Plandalf is the source of truth for money. Prices, payments, renewals and refunds are decided in Plandalf.
- MemberPress is the source of truth for access. The plugin only writes members, transactions and subscriptions. It never touches your rules or content.
- Only signed events grant access. The browser and the thank-you page never grant a membership.
Connecting
Connect with Plandalf sends the admin to Plandalf’s consent screen with the site address, a return address on the same site, and a random state. Plandalf then:
- has the user pick an account and Live or Test;
- creates an API key named after the site (for example “MemberPress · members.example.com”);
- redirects back with a single-use code that expires after five minutes.
The plugin checks the state, then exchanges the code server to server at POST /api/connect/exchange, so the key never passes through a browser. A code can only be exchanged once, and only for the return address it was issued for.
With the key, the plugin:
- reads
GET /api/v1/organization. The response includes the key’s id (used as thekidof identity tokens), its mode, and the checkout SDK URL; - registers
https://your-site/wp-json/plandalf/v1/eventsfor signed events and stores the signing secret it gets back.
The endpoint’s mode follows the key: a Test key receives test purchases only, a Live key live ones only. Reconnecting replaces the old endpoint; disconnecting deletes it. As a fallback under Advanced, you can paste an API key instead of clicking Connect.
Links between prices and memberships
A link says “buying this Plandalf price grants this MemberPress membership on this site”. Links live in Plandalf and are managed from the membership’s Plandalf tab.
Plandalf price ──grants──▶ MemberPress membership #9 on members.example.com
(any amount, (access level; its own price is
any interval) only used for comparison)- Created just in time. Link an existing Plandalf price, or have the plugin create a product and price from the membership’s own terms. Nothing is bulk-imported.
- Many to one. Any number of prices can grant the same membership. One price can grant several memberships; its amount is then split between them in MemberPress.
- Snapshot and drift. Each time a membership is saved, the plugin sends its current terms (
amount_cents,currency,interval,interval_count,trial_period_days) to all of its links. Plandalf compares them with the price and reports the differences. Update MemberPress to match copies the Plandalf terms onto the membership. Keep the difference hides the warning until either side changes again. - Shown on both sides. In MemberPress, the Plandalf column on the Memberships list. In Plandalf, a chip in the price’s Bindings column, next to its Stripe binding.
When a Plandalf price is first sold, Plandalf creates the matching Stripe price in the checkout’s own Stripe account (test or live) and records it as a binding. Membership prices don’t need to exist in Stripe beforehand.
Creating a price from a membership isn’t available for memberships with a fixed expiry date or a paid trial. Link an existing Plandalf price to those instead.
What the plugin puts on the page
For a membership that sells through Plandalf, the plugin replaces MemberPress’s signup form. That covers the classic form, the block and ReadyLaunch. The replacement is a standard Plandalf SDK mount:
<div data-plandalf-mount="<design offer slug>"
data-plandalf-price="<linked price lookup key>"
data-plandalf-mode="test"
data-plandalf-membership="123"></div>data-plandalf-mountis the site’s checkout design, or the membership’s own design if it has one. Only the offer’s look is used.data-plandalf-priceis the linked price the page sells. It replaces the offer’s own items.data-plandalf-mode="test"appears only on sites connected in Test mode.data-plandalf-membershipis read by the plugin’s own script only, to pick the right thank-you page. It doesn’t affect what’s granted.
Popup and Full screen render a data-plandalf-present button with the same attributes. The [plandalf_buy] shortcode renders the same button.
The plugin’s script then:
- identifies logged-in members;
- sends the buyer to the MemberPress thank-you page as soon as the first payment is confirmed (
plandalf:purchase), tagged with the invoice number and the invoice’s private id. Checkout pages after the payment, such as an upsell, are not shown yet; - waits there for the signed event to be applied.
Identifying members
For logged-in members, the plugin signs a short-lived identity token and passes it to the SDK before checkout opens. It’s an HS256 JWT signed with the site’s API key, with the key’s id as the kid header:
{
"sub": "wp:42",
"email": "jane@example.com",
"name": "Jane Smith",
"exp": 1790000000
}sub is always wp: followed by the WordPress user ID. Plandalf stores it as the customer’s external ID. It only trusts a token signed by a key of the same Plandalf account as the offer. Guests get no token; the plugin matches or creates their account when the purchase event arrives.
Purchase events
Plandalf sends events to POST https://your-site.com/wp-json/plandalf/v1/events. Every event has the same envelope:
{
"id": "evt_01J8Z3Q4X7N2K5V9W0T6R1M3B8",
"type": "invoice.paid",
"created": 1790000000,
"livemode": false,
"data": {
"object": {
"object": "invoice",
"id": "01J8Z3Q4X7N2K5V9W0T6R1M3B8",
"number": "INV-0042",
"status": "paid",
"billing_reason": "checkout",
"currency": "usd",
"total": 2900,
"customer": { "id": 5812, "email": "jane@example.com", "name": "Jane Smith", "external_id": "wp:42" },
"subscription": { "id": "sub_1Q2w3E4r5T6y", "status": "active", "current_period_end": 1792592000 },
"lines": [
{
"description": "Gold Monthly",
"amount": 2900,
"price": { "id": 2831, "lookup_key": "gold-monthly", "recurring": { "interval": "month", "interval_count": 1 } },
"links": [
{ "system": "memberpress", "site": "members.example.com",
"external_type": "membership", "external_id": "9", "role": "grants" }
]
}
]
}
}
}Events the plugin handles
invoice.paid, first purchase: creates the member if needed and records the purchase. A subscription gets a MemberPress subscription and a transaction running tocurrent_period_end. A one-off purchase gets a completed transaction.invoice.paidwithbilling_reason: subscription_cycle: a renewal. It records a transaction running to the newcurrent_period_end. Renewals carry the payment provider’s invoiceidandnumber, plusoriginal_invoice.invoice.refunded: carriesamount_refundedandrefunded. A full refund marks the transaction refunded and ends access; a partial refund is only logged.subscription.updated: keeps the MemberPress subscription’s status in step, including a cancellation scheduled for the end of the period.subscription.canceled: marks the MemberPress subscription cancelled. Access ends when the current transaction expires.invoice.payment_failed: logged only. Stripe retries the payment on the connected account’s retry settings.
Only lines whose links grant a membership on this site (matching site) become memberships. Other lines are ignored.
Signatures
Every request carries two headers:
Plandalf-Signature: t=1790000000,v1=5f8c0e…
Plandalf-Event-Id: evt_01J8Z3Q4X7N2K5V9W0T6R1M3B8v1 is the hex HMAC-SHA256 of <t>.<raw request body>, keyed with the endpoint’s signing secret. The plugin rejects any request whose signature doesn’t match, or whose timestamp is more than five minutes old:
[$t, $v1] = [null, null];
foreach (explode(',', $_SERVER['HTTP_PLANDALF_SIGNATURE'] ?? '') as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, null);
if ($k === 't') { $t = (int) $v; }
if ($k === 'v1') { $v1 = $v; }
}
$expected = hash_hmac('sha256', $t . '.' . file_get_contents('php://input'), $secret);
$valid = $v1 && hash_equals($expected, $v1) && abs(time() - $t) <= 300;Delivery and retries
- Any 2xx response counts as delivered. The plugin answers 200 once it has recorded the event, including events it chose to ignore, and 500 if applying it failed.
- Failed deliveries are retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. An endpoint that has failed everything for three days is switched off.
- The same event can arrive more than once. The plugin stores each event id and never applies one twice. Each payment is also recorded under its invoice number, which MemberPress won’t duplicate.
- Events can arrive out of order. An expiry is never moved backwards.
The thank-you page and first password
The thank-you page asks GET /wp-json/plandalf/v1/purchase-status?invoice=<number>&ref=<invoice id> until the invoice’s event has been applied. The endpoint only answers applied or not.
When a purchase created a new WordPress account, the plugin makes one MemberPress set-password link. It emails that link with MemberPress’s own “Set Your New Password” email, and holds the same link for 30 minutes. The status endpoint hands it out once, to a caller presenting the invoice’s private id (ref, a random ULID only the buyer’s browser receives), and the page redirects there. After the password is set, MemberPress logs the member in and the plugin sends them to the membership’s content. For these first passwords, MemberPress’s “Password Lost/Changed” admin email is skipped. Existing accounts are never handed a link.
What the plugin writes to MemberPress
The plugin uses MemberPress’s own gateway helpers (record_create_sub, record_sub_payment, record_one_time_payment). Group upgrades, grace periods, welcome emails and receipts therefore behave exactly as they do with MemberPress’s built-in gateways.
User. Matched in this order:
- the WordPress user in
external_id; - an existing user with the checkout email;
- otherwise, a new MemberPress user.
Subscription (MeprSubscription, recurring prices only):
subscr_id: the Plandalf subscription idgateway: the Plandalf payment methodstatus: active, or cancelled when Plandalf reports a cancellation
Transaction (MeprTransaction):
trans_num: the Plandalf invoice number (:<membership id>is appended when one invoice grants several memberships). A renewal applied by the daily check, not by an event, isrenewal:<subscription id>:<period end>gateway: the Plandalf payment methodstatus: complete, or refunded after a full refundexpires_at: the subscription’s period end, the membership’s own expiry for one-off purchases, or never for lifetime
The plugin adds a record-only Plandalf payment method to MemberPress. It never appears on MemberPress’s own checkout. Its Cancel action calls Plandalf and cancels at the end of the current period, and MemberPress also uses it when a group upgrade ends the old subscription.
Daily check
Once a day, WP-Cron asks Plandalf for the state of every Plandalf subscription whose access ends within 48 hours. It applies anything it hasn’t already seen from an event: a renewal, a scheduled cancellation, or an ended subscription. This covers renewal and cancellation events that were blocked or missed while the site was down, for subscriptions near the end of their period. It doesn’t recover a missed first purchase, or a cancellation long before the period ends.
Plandalf API used by the plugin
All calls go to /api/v1 with Authorization: Bearer <site key>:
GET organization: the account, the key’s id and mode, and the SDK URLPOST webhook_endpoints,DELETE webhook_endpoints/{id}: register and remove the site’s event endpointGET offers: checkout designs, with the price ids each offer sellsGET prices?search=: the price picker on the Plandalf tabPOST products,POST prices: create a Plandalf product and price from a membershipGET links,POST links,POST links/refresh,POST links/{id}/acknowledge,DELETE links/{id}: manage links and driftGET subscriptions/{id},POST subscriptions/{id}/cancel: the daily check and account-page cancellation
The links, events and subscription endpoints aren’t specific to MemberPress. Any client with an API key can use them.
Developer hooks
// Change the identity token's claims before it is signed.
add_filter('plandalf_mepr_identity_claims', function (array $claims, WP_User $user) {
return $claims;
}, 10, 2);
// Pick or create the WordPress user for an event. Return a WP_User to override matching.
add_filter('plandalf_mepr_resolve_user', function (?WP_User $user, array $event) {
return $user;
}, 10, 2);
// Runs after a membership is granted or extended.
add_action('plandalf_mepr_granted', function (MeprTransaction $txn, array $event) {
// e.g. tag the member in your CRM
}, 10, 2);
// Runs for every verified event, including ones the plugin ignores.
add_action('plandalf_mepr_event', function (array $event) {}, 10, 1);Testing the plugin
The plugin ships with a test suite that runs against a real WordPress and MemberPress install. Each test runs in a database transaction that is rolled back afterwards. Plandalf’s API is faked, and outgoing email is captured, not sent. From the WordPress folder:
wp eval-file wp-content/plugins/plandalf-memberpress/tests/run.phpAdd a word after the command to run only matching tests, for example … run.php refund. The command exits non-zero when a test fails, so it can run in CI.
Early access limits
- No proration when switching memberships within a group.
- MemberPress coupons don’t apply; use Plandalf discounts.
- The buyer goes to the thank-you page as soon as the first payment is confirmed, so checkout pages after payment (upsells) aren’t shown yet.
- Promo tier pricing doesn’t apply on membership pages: the page sells the linked price.
- A renewal applied by the daily check keeps its
renewal:transaction number, so a later refund of that invoice isn’t matched to it. - Requires PHP 8.2.
- Creating a price from a membership isn’t supported for fixed expiry dates or paid trials; link an existing price instead.
- No “Manage billing” link on the MemberPress account page yet; members update their card from Plandalf’s billing emails.
- MemberPress corporate (sub-account) memberships aren’t supported yet.