Creating a Membership Site with StayPal

Illustration of myriad international currencies falling from the sky, with the text "StayPal" overlayed

Until now, you couldn’t build a Statamic-powered site with automated user-management. As of today, you can do so with StayPal 1.1.

Let’s talk about the new features of StayPal by walking through the process of setting up a subscription-based membership site.


Some common requirements might look something like this:

  1. Allow client to customize subscriptions’ and trial periods’ (up to two trial periods per subscription)
    • price,
    • duration, and
    • number of recurrences.
  2. Automate member
    • creations,
    • updates, and
    • cancellations.
  3. Restrict access to specific content based on a member’s role.

Requirement #1

We can easily allow our client to customize just about anything with Statamic. We just need to create a fieldset. We should also use a StayPal settings file for things that remain constant for every subscription plan our client might want to create.

Requirement #2

StayPal can automate member creations, updates, and cancellations with a few tags in a subscription template and an IPN template.

Requirement #3

Statamic’s member functions don’t exactly help out much right now (v1.6.4), but StayPal can step in again to take care of this with a conditional tag in our content templates.


To meet our requirements, we need to create/edit:

  1. a subscription fieldset,
  2. a StayPal settings file,
  3. a subscription template,
  4. an IPN template, and
  5. our content templates.


Subscription Fieldset

This serves only as a working example. You can handle this any way you want.

  # Subscription
    type: section
    display: Subscription

    display: Price
    instructions: Numbers only.
    type: text

    display: Duration
    instructions: "Numbers only. Possible ranges: 1-90 days, 1-52 weeks, 1-24 months, 1-5 years"
    type: text

    display: Duration Unit
    type: select
      D: Days
      W: Weeks
      M: Months
      Y: Years

    display: Set payments to recur.
    default: yes
    type: checkbox

    display: How many times should payments recur?
    instructions: Leave blank for indefinite subscriptions.
    type: text

  # Trials
    type: section
    display: Trials
    instructions: Leave blank for no trials.

    display: "Trial #1 Price"
    instructions: Numbers only. Set to 0 for a free trial.
    type: text

    display: "Trial #1 Duration"
    instructions: "Numbers only. Possible ranges: 1-90 days, 1-52 weeks, 1-24 months, 1-5 years"
    type: text

    display: "Trial #1 Duration Unit"
    type: select
      D: Days
      W: Weeks
      M: Months
      Y: Years

    display: "Trial #2 Price"
    instructions: Numbers only. Set to 0 for a free trial.
    type: text

    display: "Trial #2 Duration"
    instructions: "Numbers only. Possible ranges: 1-90 days, 1-52 weeks, 1-24 months, 1-5 years"
    type: text

    display: "Trial #2 Duration Unit"
    type: select
      D: Days
      W: Weeks
      M: Months
      Y: Years

StayPal Settings File Excerpt

This belongs in _config/add-ons/staypal/staypal.yaml.

Don’t forget to update:

  1. business,
  2. cert_id, and perhaps
  3. currency_code.

cmd: _xclick-subscriptions

# Assuming you work with US dollars. If not, update to the appropriate
# three-letter currency code.
currency_code: USD

☑ Requirement #1
☐ Requirement #2
☐ Requirement #3

Subscription Template

Before writing our subscription template, let’s create a partial called subscribe_form.html for the registration form to keep things DRY.

<form method="post">
 <input type="text"     name="username" placeholder="username" value="{{ get_post:username }}">
 <input type="password" name="password" placeholder="password" value="{{ get_post:password }}">
 <button type="submit">Submit</button>

The names of the inputs must remain username and password in order for StayPal to create a new member.

Now that we have a partial, we can set up our template. You should definitely tweak this at least a little bit. You can improve the error messages, and you probably don’t want to leave the settings for forbidden_chars and space.

<!-- The user hasn't filled out the form yet. -->
{{ if !get_post:username AND !get_post:password }}

  {{ theme:partial src='subscribe_form' }}

<!-- The user didn't enter either a username or a password. -->
{{ elseif !get_post:username OR !get_post:password }}

  <p>Please enter both a username and a password.</p>
  {{ theme:partial src='subscribe_form' }}

<!-- The user entered an unavailable username. -->
{{ elseif !{ staypal:username_available } }}

  <p>Sorry, someone else already claimed that username. Please try another.</p>
  {{ theme:partial src='subscribe_form' }}

<!-- The user entered an invalid username. -->
{{ elseif !{ staypal:input_valid name='username' } }}

  <p>Sorry, but usernames may contain only letters, numbers, and underscores.</p>
  {{ theme:partial src='subscribe_form' }}

<!-- The user entered an invalid password. -->
{{ elseif !{ staypal:input_valid name='password' forbidden_chars='|a ' } }}

  <p>Sorry, but the password you entered contains the following forbidden characters:</p>
    {{ staypal:forbidden_chars forbidden_chars="|a " space='spizzace' }}
      <li>{{ name }}</li>
    {{ /staypal:forbidden_chars }}
  {{ theme:partial src='subscribe_form' }}

<!-- The user succesfully chose a username and password. -->
{{ else }}

  {{ staypal:encrypt
      item_name   = '{ title }'

      a1 = '{ trial_1_price }'
      p1 = '{ trial_1_duration }'
      t1 = '{ trial_1_duration_unit }'
      a2 = '{ trial_2_price }'
      p2 = '{ trial_2_duration }'
      t2 = '{ trial_2_duration_unit }'

      a3  = '{ price }'
      p3  = '{ duration }'
      t3  = '{ duration_unit }'
      src = '{ if recur == 'true' }1{ endif }'
      srt = '{ recur_times }'

      custom = 'get_post:username|get_post:password'

      button_text = 'Subscribe'

{{ endif }}

In the code above, the comments before each conditional explain what they check.

On :13, we use the StayPal tag {{ staypal:username_available }} to check the availability of the user’s desired username. This tag returns true for available usernames and false for unavailable usernames.

:19 checks to make sure the username contains nothing other than letters, numbers, and underscores with {{ staypal:input_valid }}. Statamic saves members’ usernames as filenames, so we should use only globally-accepted filename characters.

:25 checks to make sure the password doesn’t contain any characters you want to forbid in passwords. Valid passwords return true, while invalid ones return false, so again we need to use {{ staypal:input_valid }} to check for invalidity.

:29-31 use the tag pair {{ staypal:forbidden_chars }} to return the forbidden characters present in the user’s proposed password. If the proposed password contains a space (and you forbid the space character, like in this example), StayPal will, by default, return SPACE. You can, however, change that with the space parameter.

IPN Template

With just about everything else in place, we can now set up a functioning IPN template.

<!-- The user signed up for a subscription. -->
{{ if get_post:txn_type == "subscr_signup" }}
  {{ staypal:create_member roles='starman|kook' }}
<!-- The user cancelled their subscription. -->
{{ elseif get_post:txn_type == 'subscr_cancel' }}
  {{ staypal:edit_field value="cancelled" }}
<!-- The user's subscription expired. -->
{{ elseif get_post:txn_type == 'subscr_eot' }}
  {{ staypal:edit_field value="expired" }}
<!-- The user's payment failed. -->
{{ elseif get_post:txn_type == 'subscr_failed' }}
  {{ staypal:edit_field value="suspended" }}
{{ else }}
  {{ redirect url="/404" }}
{{ endif }}

Whenever anything happens with PayPal subscriptions, PayPal sends a bunch of POST variables to the notification URL. We can check the value of txn_type to figure out what we should do.

Once again, comments in the code above explain each conditional.

When someone signs up for an account, we need to create a new member with {{ staypal:create_member }} as seen on :3. You can set whatever roles you want with the roles parameter, which by default uses subscriber. If you want to use a non-pipe character for the delimiter in the custom parameter for {{ staypal:encrypt }}, you need to change the delimiter parameter here.

On :6, :9, and :12, we use {{ staypal:edit_field }} to edit the roles. By default {{ staypal:edit_field }} appends the value of the value parameter to the roles field. You can use the field parameter to edit a different field, though it works only with array fields. You can also set the mode parameter to replace or prepend if you want to clear out existing values or prepend to them instead.

☑ Requirement #1
☑ Requirement #2
☐ Requirement #3

Protected Content Templates

Lastly, we can show specific content only to members with the appropriate role with {{ staypal:member_has_role }}. Just set role to the appropriate role.

<!-- Starmen only! -->
{{ if { staypal:member_has_role role="starman" } }}

  {{ content }}

{{ else }}

  <p>Sorry, you need to <a href="/subscribe">subscribe</a> in order to view this content.</p>

{{ endif }}

☑ Requirement #1
☑ Requirement #2
☑ Requirement #3

That takes care of all of our requirements. Let me know if you have any questions or comments by leaving a comment below, or you can hit me up on Twitter @_cpb.

comments powered by Disqus