---
description: "How the Plandalf for MemberPress plugin connects, links prices to memberships, renders checkout, receives signed purchase events, and records members, transactions and subscriptions."
---
# MemberPress reference

This page is for developers and the MemberPress team. It explains what [Plandalf for MemberPress](https://plandalf.com/platforms/memberpress) sends to Plandalf, what Plandalf sends back, and what it writes to MemberPress. For setup, see the [MemberPress guide](https://plandalf.com/docs/product-guides/platforms/memberpress).

## The shape of it

```text
 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 end
```

Three rules hold throughout:

1.  **Plandalf is the source of truth for money.** Prices, payments, renewals and refunds are decided in Plandalf.
2.  **MemberPress is the source of truth for access.** The plugin only writes members, transactions and subscriptions. It never touches your rules or content.
3.  **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:

1.  has the user pick an account and **Live** or **Test**;
2.  creates an API key named after the site (for example “MemberPress · members.example.com”);
3.  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 the `kid` of identity tokens), its mode, and the checkout SDK URL;
-   registers `https://your-site/wp-json/plandalf/v1/events` for 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.

```text
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:

```html
<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-mount` is the site’s checkout design, or the membership’s own design if it has one. Only the offer’s look is used.
-   `data-plandalf-price` is 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-membership` is 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:

```json
{
  "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:

```json
{
  "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 to `current_period_end`. A one-off purchase gets a completed transaction.
-   **`invoice.paid` with `billing_reason: subscription_cycle`:** a renewal. It records a transaction running to the new `current_period_end`. Renewals carry the payment provider’s invoice `id` and `number`, plus `original_invoice`.
-   **`invoice.refunded`:** carries `amount_refunded` and `refunded`. 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:

```text
Plandalf-Signature: t=1790000000,v1=5f8c0e…
Plandalf-Event-Id: evt_01J8Z3Q4X7N2K5V9W0T6R1M3B8
```

`v1` 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:

```php
[$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:

1.  the WordPress user in `external_id`;
2.  an existing user with the checkout email;
3.  otherwise, a new MemberPress user.

**Subscription** (`MeprSubscription`, recurring prices only):

-   `subscr_id`: the Plandalf subscription id
-   `gateway`: the Plandalf payment method
-   `status`: 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, is `renewal:<subscription id>:<period end>`
-   `gateway`: the Plandalf payment method
-   `status`: complete, or refunded after a full refund
-   `expires_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 URL
-   `POST webhook_endpoints`, `DELETE webhook_endpoints/{id}`: register and remove the site’s event endpoint
-   `GET offers`: checkout designs, with the price ids each offer sells
-   `GET prices?search=`: the price picker on the Plandalf tab
-   `POST products`, `POST prices`: create a Plandalf product and price from a membership
-   `GET links`, `POST links`, `POST links/refresh`, `POST links/{id}/acknowledge`, `DELETE links/{id}`: manage links and drift
-   `GET 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

```php
// 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:

```bash
wp eval-file wp-content/plugins/plandalf-memberpress/tests/run.php
```

Add 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.

Source: https://plandalf.com/docs/product-guides/platforms/memberpress-reference
