Skip to main content

Pewpros Documentation

Everything in one place: guides for running the day to day, and the full JSON API reference for building on top of it. Start with the guides if you run the business, or jump to the API if you are integrating with it.

Events Guide

Events

Events are anything people sign up for: a class, a workshop, a seminar, or a meetup. Every event is built from three pieces:

Piece What it is
Event The class itself: its title, description, settings, and policies. Create it once and reuse it every time it runs.
Session One date the event runs on. A session can cover several days in a row (a multi-day class). Add as many sessions as you like.
Ticket type What people buy: a name, a price, and how many are available. Every event needs at least one, even a free event.

Events live under Events in the Classes section of your admin sidebar. Published events appear in three places on your site:

  • Your homepage: the next few upcoming classes, with a link to view all.
  • Upcoming Events (/events): every upcoming event, with search, filters by type, price, and category, and a list or calendar view.
  • The event's own page (/events/<slug>): the description, schedule, tickets, venue, and the Register button.
An event shows in the upcoming lists only while it is published, its visibility is Public, and it has at least one future session open for registration. (Prepay events, and Contact-Only events set to show in the Upcoming Events grid, are listed without sessions.) An event with no future sessions can still appear in the Full curriculum section of the Upcoming Events page, with a Request a Date button, while Accept date requests is on.

Creating an Event

  1. Go to Events and click New Event.
  2. Fill in the form (explained below) and click Save Event.
  3. Open the event and add its dates with Add Session (see Dates and Sessions).
  4. Add at least one ticket with Add Ticket Type (see Tickets, Pricing, and Add-ons).
  5. Click Publish. New events start as drafts, and an event cannot be published until it has a ticket type.

To run a similar class, open an existing event and click Duplicate instead of starting from scratch.

Training Type

Choice When to use it
Scheduled (Regular) The usual choice. You add dates, and people pick one when they sign up.
Prepay (No Date Selection) People pay first and arrange a date with you afterwards. There are no sessions to pick. Use Instructions shown after payment to tell them how to book their date; it appears after checkout and in their confirmation email. Date, venue, capacity, registration, and reschedule and refund policy settings are hidden for this type.
Contact-Only (Schedule by Request) No online signup. The event page shows how to call or email you. While Accept date requests is on, it is listed in the Full curriculum section below your upcoming events; tick Also show this event in the Upcoming Events grid to list it there too.

The basics

  • Title and Description: what people see on the event page.
  • Slug: the end of the event's web address. It is filled in from the title. Changing it later breaks any links you have already shared.
  • Instructor and Show instructor on event page: the instructor also receives students' reschedule requests for this event.
  • Event Type: In-Person, Virtual (Zoom), or Hybrid (see Venues and Online Events).
  • Duration: Single Day or Multi Day. This decides whether each session is one day or several (see Dates and Sessions).
  • Recurrence: One-Time or Recurring. This is a label for sorting and filtering your events list; it does not create dates for you.
  • Timezone: leave it on your site's default unless the class is held in a different time zone.
  • Featured Image: upload an image (PNG, JPG, GIF, SVG, or WebP, up to 5MB) or paste an image address into Featured Image URL.
  • Visibility: Public is listed everywhere. Unlisted is hidden from the lists but works for anyone with the link. Private is hidden completely, and its page cannot be opened.
  • Categories: tick any that apply. Categories become filters on the Upcoming Events page.

Other settings on the form

  • Show on community calendar: also lists the event's dates on your community calendar.
  • Auto-enroll in community on purchase: adds everyone who signs up to your community.
  • Disable Stage Timer push notifications: if you run the event with the Stage Timer, registered students get a "class is starting" notification in the app the first time you start it. Tick this to turn that off.
  • SEO Settings: the SEO Title and SEO Description search engines show. Leave them blank to use the event's own title and description.

The remaining settings are covered in the sections below: capacity, registration, access, waivers, policies, discounts, and emails.

Dates and Sessions

Open the event and click Add Session. The Sessions tab lists them under Upcoming and Past.

  • Single-day events: enter the Date, Start Time, and End Time. Optional: a Label, a Capacity Override, a different venue, and a Zoom meeting.
  • Multi-day events: the button opens Add Multi-Day Session with one row per day, each with its own times and, if needed, its own venue. Use Fill Consecutive Days to add a run of days at once. A multi-day session needs at least two days; use Add Day to add another later. People sign up once and are expected on every day.
  • Recurring classes: add each date as its own session. Dates are never created automatically.

Each session has buttons to Edit, Add Student, Move All Attendees, Close Registration, Cancel, and Delete. Each day of a multi-day session can be edited or cancelled on its own.

Cancelling a date

Cancelling a session, a single day, or a whole event does not email or refund anyone, and the attendees stay on the cancelled session. Deal with them first: use Move All Attendees to move them to another date (it can email them), or cancel and refund them from the Registrations tab. Then cancel the session.
  • Cancel keeps the session on record, marked cancelled. Delete removes it permanently.
  • To retire a whole event, use Cancel Event on the event page, or Delete on the Events list (which cancels it). An event with active registrations cannot be deleted. Once cancelled, it can be removed with Permanently Delete.

USCCA Classes

Most events are native: you create them here. If you teach USCCA classes, your USCCA classes can instead be synced from your USCCA instructor dashboard, so you only enter them once, at USCCA.

Connecting your USCCA login

Go to Site Settings → Integrations → USCCA Class Sync and enter your USCCA Instructor Dashboard Email and Password. This is the login for the instructor dashboard (training.usconcealedcarry.com), not the partner page login. The login is checked when you save, and nothing is saved if USCCA rejects it.

What syncs

About every 15 minutes, your classes are copied from USCCA: titles, descriptions, dates (including multi-day classes), price, capacity, location, and the people who registered through USCCA. Syncing is one way. Nothing you change here is sent back to USCCA.

Publishing a synced class

Synced classes are listed under USCCA Classes in the Classes section of your sidebar. A class is not on your site until it is published as an event:

  • Publish as Event publishes one class, and View Event opens it. Turning it off unpublishes the event (it goes back to draft).
  • Auto-publish synced events publishes new classes automatically. Classes with no future date are skipped, and classes you unpublished are left alone.
  • Sync Now pulls changes from USCCA right away.

Synced events carry a USCCA Sync badge on your Events list.

Where people sign up

  • Use native checkout off (the default, "USCCA portal"): each date's Register button sends people to that date's USCCA checkout page. They pay USCCA.
  • Use native checkout on: people sign up and pay on your site, at the USCCA price, through your own payment processor.

People who registered through USCCA appear on the event page under External / USCCA Signups. They count toward capacity and appear on your roster and sign-in sheet.

What you can edit

The title, price, registration settings, and dates follow USCCA. If you change them here, the next sync puts them back, so change them at USCCA instead. The image, categories, waivers, venue, and other settings are yours to edit.

Turning Publish as Event off and back on rebuilds the event's sessions and tickets from USCCA, so any ticket types you added are lost. Saving a new USCCA email or password in Site Settings clears the list of synced classes and pulls it again.

Tickets, Pricing, and Add-ons

On the event page, click Add Ticket Type. Each ticket type has:

  • Name and Price.
  • Admits (people per ticket): for a couple or bring-a-friend ticket, set 2. One purchase then takes two seats and collects details for both people.
  • Quantity Available: how many of this ticket can be sold. Leave it blank to use the event's capacity.
  • Sale Starts and Sale Ends: when this ticket can be bought. Useful for early-bird pricing.
  • Sort Order: the order tickets appear in at signup, lowest number first.
  • Tax Category: used when your payment processor calculates sales tax.
  • Free events: add a ticket type priced at 0. Free signups skip the payment step entirely.
  • Several ticket types (for example General and Student): the buyer picks one at signup.

Add-ons

Add-ons are extras bought with a ticket, such as course materials or a rental. Click Add Add-on and set the Name, Description, Price, Max Quantity Per Attendee, and Sort Order. Tick Required if every attendee must buy it. Add-ons cannot be edited after saving; delete and add it again to change one.

Discounts and member pricing

  • Allow promo codes on checkout: shows a promo code box at signup. On by default.
  • Enable automatic volume discounts: add tiers with Min Qty and Discount %, for example 5 or more people get 10% off.
  • There is no separate member price on a ticket. To give members a better price, use a promo code (a membership level can apply one automatically), or make a members-only event (see Registration and Access).

Capacity and Sold Out

The number of seats on a session comes from the first of these that is set:

  1. The session's Capacity Override.
  2. The event's Max Capacity.
  3. The venue's capacity.
  4. Otherwise, unlimited.
  • A ticket's Quantity Available can also limit sales. The session card warns you when a ticket is the real limit.
  • Once a session has a capacity, its card has buttons to add or remove a seat. Capacity cannot go below the number of people already registered.
  • On your site, a session shows Only N spots left! when it is nearly full and Sold Out when it is full. Sold-out dates are shown crossed out; tick Hide sold-out sessions to remove them from view instead.
  • You can still add someone to a full session yourself with Add Student.
  • Enable Waitlist: people on the waitlist appear on the Registrations tab as waitlisted. Move them in with Promote. Nobody is promoted automatically, and promoting does not collect payment, so arrange payment with them yourself.

Registration and Access

When people can sign up

  • Registration Opens (optional): before this time the event page says when registration opens. If blank, registration opens as soon as the event is published.
  • Registration Closes (optional): if blank, registration closes when the event starts.
  • Close Registration on a session stops signups for that date only. A closed session is hidden from your site unless you tick Show on public page as "Registration Closed". Reopen Registration undoes it.
  • A ticket's Sale Starts and Sale Ends limit when that one ticket can be bought.

Who can sign up

  • Access Type: Open (Anyone) or Members Only. For members only, pick a Required Membership Level, or leave it on Any linked level.
  • People do not need an account to sign up. An account is created for each attendee from their email address, so returning students see all their classes in one place.

Sending signups to another website

Set Registration Type to External (Link to Outside Site) and enter the External Registration URL. The Register buttons then open that address in a new tab. Nothing is collected on your site, so the confirmation and reminder emails in this guide do not apply. Leave it on Internal to take signups here.

Date requests

With Accept date requests on (the default), the event is listed in the Full curriculum section of your Upcoming Events page, and visitors can click Request a Date when nothing is scheduled. Requests are emailed to your company email address and listed under Date Requests in your sidebar, where you can mark them New, Contacted, or Closed. A request does not create a session; reply to the person and add the date yourself. Turn this off for one-time events you do not plan to repeat.

Venues and Online Events

  • Venues: add your locations once under Venues in the Classes section of your sidebar, then pick one as the event's Default Venue. A session, or a single day of a multi-day session, can use a different venue.
  • Second location and Travel notes: for a class that moves between two places, such as a classroom and then an outdoor site. The event page shows a Getting there section with the distance, drive time, and your notes.
  • Online events: set Event Type to Virtual (Zoom) or Hybrid. Connect your Zoom account under Site Settings (Zoom Integration), and each new session gets a Zoom meeting automatically. You can also paste a Zoom Meeting ID and Zoom Join URL into a session yourself.
  • The join link is only shown to people who signed up: in their confirmation and reminder emails, on their registration page, and as a Join Meeting button on the event page when they are signed in.

Waivers

Under Waivers on the event form, tick each waiver attendees should sign, and set each one to Required or Optional. Only active waivers that can be signed online are listed.

  • Nothing is signed during checkout. After signing up, each attendee is emailed their own signing link.
  • Required waivers are tracked at check-in and included when you send reminders to sign. Optional waivers (a photo release, for example) are offered and recorded, but nobody is chased for them.
  • Checking in someone who has not signed asks you to confirm first.
Attaching a waiver after people have signed up does not send them anything. On the Registrations tab, use Email all unsigned or Text all unsigned, or send one person a link with Email sign link or Text sign link. The same buttons are on the check-in screen.

Payments

  • Connect your payment processor (such as Stripe, Fortis, or Authorize.Net) under Site Settings → Payments. Which processor takes event payments is set when your site is set up; contact support to change it.
  • Paid signups only work once that processor is connected. Until then, people who try to pay see an error and are not registered. Free events are not affected. After connecting, make one test signup to be sure.
  • People pay on the processor's secure payment page, then return to your site to see their confirmation.
  • You get a New Event Sale email and app notification for every paid signup.

Managing Attendees

The Registrations tab on the event page lists everyone signed up, with their ticket, add-ons, session, status, waiver, payment, and attendance. Search by name, email, or phone. The Registrations page in your sidebar lists signups across all events.

Signing someone up yourself

Click Add Student, pick an existing student or enter a new one, and choose the session. Then record how they paid under Payment Method (Cash, Check, Complimentary, and so on), with an optional Payment Reference and the Amount Paid (cents), so 5000 is 50.00. Add-on prices are not added for you.

Add Student only records a payment. It never charges a card. The student still gets their confirmation email and waiver links.

Everyday changes

To do this Do this
Resend a confirmation email Resend confirmation on the attendee's row. Each attendee can be sent at most 3 in a day.
Move someone to another date Move on their row, Move to another session for several you have selected, or Move All Attendees on a session. Add an optional reason and leave the email notice ticked to tell them. Their payment and ticket move with them.
Fix a misspelled name Open Check-In, click Correct on their card, and choose Fix the spelling of this person's name.
Fix a mistyped email or swap in a different person Open Check-In, click Correct, and choose A different person is attending. Enter the right email, and tick the option to email them the confirmation. The original buyer stays the purchaser.
Cancel and refund Cancel on their row. See below.
Handle a reschedule request Students who are past the cutoff or over the limit send a request instead. Review them under Reschedule Requests in your sidebar.

Cancellations and refunds

Click Cancel on the attendee's row. The window shows what they paid, what has already been refunded, and the refund your cancellation policy suggests.

  • Paid online through Stripe or Authorize.Net: enter a Refund amount (a partial refund is fine) and click Cancel & refund, or Cancel without refund. If the refund fails, the registration is left in place so you can try again.
  • Paid any other way: cancel the registration here, and give the refund yourself (in cash, or in your payment processor).
  • Cancelling frees the seat but does not email the attendee, so let them know.

Check-in and rosters

  • Click Check-In on the event page for the check-in screen. Search for a person and click Check In (or Undo Check-In). From there you can also mark No Call No Show, Reschedule someone, Park them (set them aside to place in a session later), and add Notes. The screen also counts waivers signed and still missing.
  • You can also tick Attended on the Registrations tab.
  • Under Attendance Roster, each session has Print (a sign-in sheet), Export CSV, and QR sheet (QR codes people can scan to register and sign waivers).

Reschedule and Refund Policies

These settings are on the event form (they are not shown for Prepay events). Leave a number blank to skip that rule.

Setting What it does
Allow Reschedule Lets students move themselves to another date from their registration page.
Cutoff Days Students can move themselves until this many days before their session (1 if blank). After that, they can send you a request instead.
Free Reschedule Days and Late Fee (cents) A student moving with fewer than Free Reschedule Days to go is told a late fee may apply. The fee is in cents, so 2500 is 25.00.
Reschedule limit (lifetime) A student who has rescheduled this many times is flagged on the check-in screen, and has to send you a request to move again.
No Call No Show limit (lifetime) A student who has missed this many classes without notice is flagged on the check-in screen. A warning only; nobody is blocked.
Full Refund Days Cancelling at least this many days before the session earns a full refund.
Partial Refund Days and Partial Refund % Cancelling at least this many days before earns this percentage back.
No Refund After Days Cancelling with fewer than this many days to go earns no refund.

The refund rules are shown to students as your cancellation policy on their registration page, and they decide the suggested refund when you cancel someone. Students cannot cancel on the website themselves; they contact you, and you cancel and refund from the Registrations tab.

Emails and Notifications

Email Who gets it When Your settings
Registration confirmation Each attendee When they sign up Custom Confirmation Email Message adds a note to the standard email. Confirmation Email Override replaces it completely, and a single session can have its own override.
Waiver signing link Each attendee When they sign up Sent when the event has waivers attached.
24-hour and 1-hour reminders Everyone registered Before each session Off until you tick Send reminder emails to registered students. Edit the subject and body in the 24-Hour Reminder and 1-Hour Reminder boxes for this event, or leave them as they are to use your site-wide Event Reminder template in Message Templates, if that template is turned on.
Session moved Attendees you move When you move them Leave the email notice ticked when moving.
Feedback request Attendees Within a day after the final session Off until you tick Enable post-event feedback collection. Add a Custom Feedback Email Message if you like. Ratings appear on the event's Feedback tab.
Thank-you email Everyone registered, only checked-in attendees, or nobody, set in Who gets the thank-you email (your site default unless you change it) Hours after the final session ends Uses your Post-Class Follow-Up settings in Message Templates unless you choose otherwise here. Write a subject and body for this class under Thank-You Email Content, and use Send test email to preview it.
Payment confirmation The buyer After a Prepay purchase Includes your Instructions shown after payment.
New Event Sale Owners Every paid signup Free signups do not send this email.

The confirmation email includes the date and time, location (or the online join link), ticket, add-to-calendar links, a link to view or manage the registration, and, for in-person events, a digital ticket for Apple Wallet. The boxes on the form list the merge fields you can use, such as the student's name and the event date.

Events: Good to Know

  • No future dates means nothing to sign up for. When a scheduled event's last session passes, it drops out of the upcoming classes on your homepage and Upcoming Events page (it stays in Full curriculum, for date requests, while Accept date requests is on). Add new sessions to bring it back.
  • An event cannot be published without a ticket type. For a free event, add one priced at 0.
  • Cancelling a session or event never notifies or refunds anyone. Move or cancel the attendees first.
  • Mistyped emails: if a student never got their confirmation, check the email on their registration. Fix it with Correct on the check-in screen, then resend the confirmation.
  • Each email address belongs to one account. Signing someone up with an email you already have adds the registration to their existing account.
  • Late Fee and Amount Paid are entered in cents: 2500 means 25.00.
  • USCCA password changes stop the sync. If you change your USCCA instructor password, synced classes quietly stop updating until you enter the new password in Site Settings. A stale Synced date on the USCCA Classes page is the sign.

For Students: Signing Up for a Class

This part is written for students. Send it to anyone who needs help signing up.

Finding a class

Open the Upcoming Events page from the site menu, or look at the upcoming classes on the homepage. Search, filter by type, price, or category, or switch to the calendar view. Click a class to see its description, dates, location, and price.

Signing up and paying

  1. Click Register Now (or Register next to a date).
  2. Pick a date and a ticket if there is more than one.
  3. Choose how many tickets (up to 10), then enter each attendee's name, email, and phone. Each attendee needs their own email address.
  4. Add any extras, and enter a promo code if you have one.
  5. Click Proceed to Payment and pay on the secure payment page. For a free class, click Complete Registration instead.

You do not need an account first. One is created for you from your email address.

Waivers

If the class needs a waiver, nothing is signed during checkout. Each attendee is emailed their own signing link afterwards. Please sign before the class.

Your confirmation

You see a confirmation on screen, and each attendee gets a confirmation email with:

  • The date, time, and location, or the link to join an online class.
  • Your ticket, and links to add the class to your calendar.
  • A View or manage your registration link.
  • For in-person classes, a ticket you can save to Apple Wallet.

You may also get reminder emails the day before and an hour before the class. For an online class, use the Join Meeting link in your email, or on the class page while signed in.

Changing or cancelling

  • Open the View or manage your registration link in your confirmation email, or sign in and go to your Dashboard → Upcoming Events → Manage / Reschedule.
  • If the class allows it, pick a new date there. If it is too close to the class, you can send a reschedule request instead.
  • The cancellation policy is shown on that page. To cancel or ask about a refund, contact the business directly.
  • No confirmation email? Check your spam folder, then contact the business: your email address may have been mistyped.

Groups Guide

Groups

A group is a block of seats for an organization, such as a church, a company, or a school. Everyone who takes a seat automatically gets whatever you attach to the group: a membership, courses, and events.

Use one group per organization. The seat count is how many people that organization is covered for, and the organization's own admin can manage who fills those seats, so you are not adding and removing people for them.

Paid Online vs Paid Offline

The first decision is how the organization pays, because it changes who bills, who can add seats, and what happens at renewal.

  Paid online Paid offline (check, cash, invoice)
Group price Set a price per seat Leave the price blank (or 0)
Who collects payment The site, at checkout You, outside the site
How they sign up They buy through the group's purchase link, which creates their own group You create their group and invite their people
Renewal Automatic if the price is monthly or yearly Manual: collect the payment and update the seat count
More seats Per seat: they buy them from their group page They pay you, then you raise the seat count
Who can change the seat count Nobody edits it directly; more seats are bought Only you
With offline payment the site has no idea whether a renewal was paid. Keep your own record of when each organization is due.

Setting Up a Group

Groups live under Groups in your admin sidebar (in the Classes section). Click New Group and fill in:

  • Name: the organization's name.
  • Total Seats: how many people the organization is covered for.
  • Pricing Model: choose Per Seat for online payment, so the organization pays for each seat and buys more as needed.
  • Price and Billing Interval: leave the price blank for offline payment. For online payment, set the price and choose One-time, Monthly, or Yearly.
  • Leader: required. Pick an existing account, such as yourself or the organization's contact. On a group you set up, this is the name on file and does not give that person any control. Use the group's Admin role for that (see Who Manages What).
  • Membership Level: the membership everyone in the group gets.

After saving, open the group to attach Courses and Events. Members are enrolled and registered automatically.

Getting people in

  • Invitations tab: enter emails, separated by commas. Each person gets an email, signs in with that same email address, and accepts. Invitations expire after 30 days. Someone who has never set a password can request an emailed sign-in link from the sign-in page.
  • Join Link: a shareable link anyone with an account can use to join while seats are available. Share it only with the organization.
  • Online payment: copy the group's Purchase Link and send it to the organization's contact. Their purchase creates their own group, with them as its admin.

Who Manages What

You manage every group from Groups in your admin sidebar: seats, price, content, invitations, members, and roles.

The organization manages its own group by signing in and opening My Groups. What they can do depends on their role in the group, which you set on the group's Members tab:

Role Can do
Member Uses what the group grants. No management.
Leader Add, invite, and remove members. Edit the group's description.
Admin Everything a Leader can, plus buy more seats on paid per-seat groups.
Primary Admin Everything an Admin can. This is usually the person who bought the group.

Members can be added instantly from My Groups with Add Members: existing accounts are seated straight away, and new emails get an account created for them. Nobody can go past the seat count.

Common Group Scenarios

What happens What to do
The organization adds someone Their admin adds them from My Groups (or you add or invite them). They get the group's membership right away.
They need more seats Online (per seat): they buy seats from My Groups. Offline: they pay you, then you raise Total Seats on the group.
Someone leaves Their admin (or you) removes them from the group. The membership the group gave them ends.
Renewal Online: renews automatically. Offline: collect the payment, then update Total Seats if the number changed. Nothing else to do.
They don't renew or cancel Remove their members from the group, which ends their memberships.

Groups: Good to Know

  • Group memberships do not expire on their own. Access ends when someone is removed from the group.
  • Every email belongs to one account. To change someone's membership, work with their existing account rather than creating a new one.

Appointments Guide

Appointments

Appointments let customers book one-on-one time with a staff member: lessons, consultations, or calls. Each staff member gets their own public booking page, and customers pick a type of appointment, a day, and a time.

It lives under Appointments in your admin sidebar: Booking Types (what can be booked), Staff Availability (when), and Appointments (what has been booked).

Setting Up a Booking Page

  • Your profile: under Settings → Profile, set your Booking URL Slug, bio, and Appointment Booking Timezone. Your page is at /appointments/<your-slug>.
  • Booking Types: click New Booking Type and set the name, duration, price (0 for free), location (Zoom, In Person, or Phone), and how it is paid. Optional settings cover start-time spacing, setup and turnaround time, minimum notice, how far ahead people can book, and whether customers can reschedule themselves.
  • Staff Availability: tick the days you take appointments and set the hours. Use Date Overrides for a day off or different hours on a specific date.

Your booking page is live once you have a slug, at least one active booking type, and hours. The Appointments page shows what is still missing until then. Owners can set up booking types and hours for other staff using the Staff member picker on those pages.

Payments

  • Online only: the customer pays when booking.
  • Counter only: the booking is confirmed and the customer pays on arrival. Use Mark paid or Comp on the Appointments page.
  • Both: the customer chooses.
  • Free appointments never ask for payment.

Refunds for cancelled paid appointments are made in your payment processor.

Calendars and Zoom

  • Google Calendar: each staff member connects their own Google account from the Appointments page. Busy times on their calendar block those times, and new bookings are added to it with the customer invited.
  • Additional conflict calendar: another Google calendar that is only checked for busy times.
  • Zoom: connect your Zoom account in Site Settings, and appointments with the Zoom location get a meeting link automatically.

Appointment Notifications

Email Who gets it
Booking confirmed The customer
New booking The staff member who was booked
Appointment moved The customer
Appointment cancelled by the customer The customer and the staff member

Appointments: Good to Know

  • Each appointment is one staff member with one customer. For bookings that need a lane or room too, a set group size, a waiver, or members-only access, use Bookable Resources and Bundles.
  • The Appointments page lists your own appointments. The Bookings page lists every booking across staff, lanes, and bundles.
  • Staff Availability has one time window per day.

Range Guide

Range Lane Booking

Customers reserve lanes on your site's Range Booking page, and your staff run the day from the Reservation Board. Everything is set up under Range in your admin sidebar, mostly on the Lanes page, which has five tabs: Lanes, Hours & Closures, Booking, Rate Options, and Add-ons.

Settings apply to the location picked in the sidebar's Current location switcher, so each location can be set up differently. The public Range Booking link appears once Show Range Booking is on in Site Settings and a location has active lanes and opening hours.

Lanes, Hours, and Closures

Lanes

  • Add each lane with its number, an optional name, a Type (Pistol, Long gun, Archery, Other), and Max shooters.
  • Use Duplicate to copy a lane, or Bulk edit to change several at once.
  • Once you have Rate Options (below), those set the price. The hourly prices on a lane are only a fallback for locations with no Rate Options.

Hours & Closures

  • Operating Hours: the weekly schedule. Add a second window on a day to create two sittings.
  • Date-specific hours: different hours, or closed all day, on a particular date. A date entered here replaces that day's weekly hours.
  • Closures: close the whole location or one lane for a stretch of time. If existing reservations fall inside it, you are shown them with a Cancel button for each.

Check-in waiver

On the Lanes tab, choose the waiver shooters must sign before staff can check them in. Customers can sign it ahead of time from the confirmation screen after booking.

Rate Options and Pricing

A Rate Option is something a customer can book, such as "1 Hour Lane" or "30 Minute Lane". Each one has its own length and prices:

Field What it does
Walk-up $ Price for non-members.
Default member $ Price for anyone with an active membership.
Extra shooter $ / Extra (member) $ Added for each shooter after the first.
Member tier prices Optional per-plan prices. Leave a plan's Member $ blank to use the default member price, or enter 0 to make the first shooter free for that plan.
Free / window Free sessions per plan, resetting each billing period or monthly, or Unlimited.
Payment Pay at counter, pay online, or both. Inherits the location setting unless changed.
Lane types Limit the option to certain lane types.
Who can book this option Same as the location, or its own rule (see Who Can Book).

Members are priced automatically when they are signed in with an active membership: a non-member sees the walk-up price, and a member sees their plan's price. Add-ons (on the Add-ons tab) are optional extras such as targets or eye protection that customers add by quantity.

Prices on Rate Options and Add-ons are entered in dollars. A booking that totals $0 is always reserved without payment.

Who Can Book

On the Booking tab, the Booking Policy section controls online bookings:

  • Require an account to book online: when ticked, customers must sign in. When unticked, guests can book with a name and phone number (and an email when paying online).
  • Who can book online: Anyone signed in (no membership check; guests can still book unless Require an account is ticked), Any active membership, Specific membership programs, or Specific membership tiers. "Active" means the membership's status is active or trialing.
  • Max upcoming per person and Book up to (days ahead).
  • Daily lane-time limit per tier: the most minutes a member on each plan can book in one day.

A Rate Option can override "Who can book online" with its own rule, for example a members-only range that sells one option to the public. The Booking Settings section on the same tab covers slot length, buffers before and after, minimum notice, the longest booking allowed, how lanes are auto-assigned, and whether customers can cancel or reschedule themselves.

These rules apply to online bookings only. Staff bookings made at the counter from the Reservation Board are not limited by them.

Running the Range Day to Day

The Reservation Board shows each lane's bookings for the day. From a booking you can:

  • Check in the party (once the waiver is signed), or Capture waiver to have them sign on the spot.
  • Mark paid, Mark unpaid, or Comp the booking.
  • Reschedule, mark a No-show, or Cancel. The customer is emailed when you reschedule or cancel.
  • Block lane to close a lane for the whole day.

Use New Reservation for walk-ins and phone bookings. Search for a member to apply their pricing and any free sessions they have left. An owner can override the waiver requirement at check-in.

When a paid booking is cancelled, it is added to Cancellation Follow-ups so you can rebook the customer or refund them. Refunds are made in your payment processor.

Range Notifications

Email Who gets it When
Booking confirmation Customer When they book online
Reminder Customer Online bookings, the number of hours before set in Booking Settings (default 24)
Reservation moved Customer Whenever it is rescheduled
Reservation cancelled Customer Whenever it is cancelled
New booking, reschedule, and paid cancellation alerts Every owner on your account Turned on or off together with Email staff on paid cancellations & reschedules in Booking Settings

Bookings your staff make at the counter do not send the customer a confirmation or reminder.

Bundles Guide

Bookable Resources and Bundles

A resource is anything that can be booked: an instructor, a lane, a room, a simulator, a venue. A bundle books several resources together for one time slot at one price, such as "Private Lesson + Lane". Customers find bundles under Sessions on your Services page.

There are three ways to take bookings, and you can use any of them:

Use When
Range lane booking Customers renting a lane on their own
Appointments One-on-one time on a staff member's own calendar
Bundles One slot that needs several things at once, at one flat price

Booking Setup (under Appointments in the sidebar) shows which of these are ready and what is still missing.

Bookable Resources

Under Bookable Resources, add each resource with:

  • Type and Max capacity: 1 means one booking at a time; more means several groups can share it.
  • Staff member (instructors): links the resource to a staff member so their Google Calendar blocks busy times.
  • Linked lane (lanes): ties it to a range lane, so it follows the range's hours and appears on the Reservation Board when booked.
  • Optional requirements: a membership, a signed waiver, or age verification.
  • Sell standalone: untick this for resources that should only be booked inside a bundle.

After saving a resource, edit it to set its Availability (weekly hours) and any Date-specific hours. With no hours of its own, a resource uses its location's range hours. If you start a bundle from Instructor + lane and no lane resource exists yet, a Use my range lanes button (it shows your lane count) adds your range lanes as resources in one step.

The Setup column shows whether each resource is ready. Click Why? to see what is missing, such as hours.

Creating a Bundle

Under Booking Bundles, click New Bundle, or Instructor + lane for a ready-made starting point. Set:

  • Name, Card blurb, Description, and an optional image.
  • Duration (minutes) and Price in cents (2500 = $25.00). The price is the same for every customer.
  • How is this paid? At the counter, or card at booking.
  • Minimum notice, and whether a membership is required.
  • Resources: tick each resource the bundle needs. Tick Pool on interchangeable resources, such as several instructors, when any one of them will do.

A time is offered when every resource in the bundle (and at least one in each pool) is free at the same start time, so give bundled resources hours that line up.

What Customers See

  • Customers pick a date and a time, enter their details, and then pay by card or reserve to pay at the counter, depending on the bundle's How is this paid? setting. They do not need an account first: one is created for them.
  • If the bundle includes a lane, a lane reservation is created and shows on your Reservation Board.
  • The customer is emailed a confirmation, and every owner on your account gets a New booking email. The booking is also added to linked staff members' Google Calendars.
  • The Bookings page lists bundle bookings alongside appointments and lane reservations.
Bundles have one price for everyone. For member and per-plan pricing on lanes, use Range Rate Options.

Wholesale Distributors Guide

Wholesale Distributors

Connect your wholesale distributor account and PewPros keeps a private copy of their catalog, prices, and stock levels for you. From there you choose what to publish to your storefront, and PewPros drafts and places purchase orders when stock runs low.

Two distributors are supported today. You can run both at once, and the day-to-day experience is the same for either; what differs is how each one delivers data and accepts orders.

  Sports South RSR Group
What you sign up for A dealer account plus web-service access A dealer account plus file-transfer (FTP) access
Order confirmation Immediate: you get an order number straight away Delayed: confirmation arrives in a later file
Dealer cost refreshed Every 30 minutes, with stock Once a day, with the full catalog
Serial numbers on receipt Yes, from the invoice Not supplied: enter them when you receive
You need your own account with the distributor first. PewPros does not resell or broker; it connects to the account you already hold, and your pricing, terms, and shipping are whatever that distributor gives you.

Connecting a Distributor

Go to Admin → Distributors and add an account. What you enter depends on which distributor you are connecting.

What you will need

  • Sports South: your customer number, username, password, and the source identifier they assign you.
  • RSR Group: your dealer (etailer) number and your FTP username and password.
  • A vendor record to link the account to, so purchase orders and received stock land against the right supplier in your books.

Then, in order

  1. Test connection: confirms your credentials work before anything else runs.
  2. Sync now: pulls the catalog in. The first pull of a large catalog takes a while and continues in the background.
  3. Refresh on-hand: updates stock levels and, for Sports South, current pricing.
  4. Turn on auto-sync so it keeps itself current without you asking.
Credentials are stored encrypted and are never shown back to you in full after saving. If a distributor resets your password, update it here or syncing will start failing silently in the background.

Catalog, Pricing & Stock

Synced items land in a private staging area, not your storefront. Nothing is visible to customers until you choose to publish it. Browse what came in under Admin → Distributors → Catalog, where you can search by SKU, UPC, description, or manufacturer.

The three prices

  • Your cost: what the distributor charges you. Drives margin and the reorder suggestions.
  • MSRP: the manufacturer's suggested retail price. Used as the default sell price when you publish an item.
  • MAP: minimum advertised price, where the manufacturer sets one. Pricing below it can breach your dealer agreement.

How current it is

Stock levels refresh every 30 minutes. The full catalog (descriptions, images, categories, specifications) refreshes daily. Sports South sends updated cost and MSRP with each stock refresh; RSR sends quantities only, so RSR pricing updates on the daily catalog run.

Stock figures are the distributor's warehouse, not a reservation. An item showing available can still sell out before your order reaches them.

Selling Distributor Products

Publishing is deliberate and reversible. From the catalog screen, use Import to store to turn selected items into real products, priced at MSRP with the distributor's images attached. You can filter to in-stock items, or to items that have a photo, so you are not publishing blank listings.

Re-importing later refreshes the items you already published rather than creating duplicates. Products you have hidden stay hidden, so a re-sync never quietly puts something back on sale that you took down.

Publishing an item does not mean you hold it. Stock shown is the distributor's, so decide whether you are drop-shipping or stocking before you list a large catalog.

Ordering & Receiving

You do not have to watch stock yourself. Set a minimum and a maximum for any product under Manage Min/Max, and when stock falls to the minimum PewPros drafts a purchase order to bring you back to the maximum.

The screens

  • Smart Order: what needs reordering now, and from which distributor, based on your rules and recent sales.
  • Smart Buys: items worth buying on margin rather than because you are short.
  • Purchase Orders: every draft, submitted, and received order.

After you submit

A draft is yours to edit until you submit it; nothing is sent to a distributor without you doing so. Once submitted, PewPros watches for confirmation and tracking numbers and attaches them to the order. When the shipment arrives, receive it against the purchase order and stock is added to your inventory.

Receive what actually turned up, not what you ordered. Distributors regularly ship partial orders, and receiving accurately is what keeps your stock counts and your reorder suggestions honest.

Firearms & FFL Rules

Serialized firearms carry extra requirements, and PewPros enforces them rather than leaving them to memory.

  • Firearms are flagged automatically from the distributor's own data, so they are treated as serialized without you tagging them.
  • A current copy of your FFL must be on file with the distributor before they will ship a firearm. An order can be accepted and then held if it is missing or expired.
  • Received firearms go into your bound book with their serial numbers. Sports South supplies serials with the invoice; for RSR you enter them as you receive, because the feed does not carry them.
  • Shipping to a customer means shipping to their FFL, not their home. The receiving dealer completes the transfer and the background check.
These checks are a safety net, not legal advice. Your FFL obligations are yours, and state rules vary, particularly on magazines, features, and what may ship where.

Want the mechanics in more depth (every sync, file, and hand-off, with diagrams)? See how the RSR Group and Sports South integrations work .

API Reference

Introduction

Welcome to the developer API. Every endpoint listed here is served under /api/v1 on your domain. Requests and responses use JSON, and authentication is performed with a Bearer token.

If you prefer an interactive view, jump over to the Swagger UI where every route is listed with a live "Try it out" panel. A raw OpenAPI 3 spec is available at /api/docs/openapi.json for importing into Postman, Insomnia, or your code generator of choice.

Authentication

Most endpoints require a Bearer token. Generate one from your account settings, then send it on every request in the Authorization header.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/events" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          

Tokens are long-lived by default. Treat them like passwords: never commit them to source control, rotate them when a team member leaves, and scope them to the smallest set of permissions you need.

401 Missing or invalid token

            {
  "error": "unauthorized"
}

          
A handful of endpoints (for example, GET /api/v1/instance and the public blog routes) do not require authentication. The rest return 401 without a valid token.

Conventions

All requests with a body must set Content-Type: application/json. Field names are snake_case. Timestamps are ISO 8601 in UTC (for example, 2026-06-15T09:00:00Z). Money values are expressed in cents as integers to avoid floating point rounding.

Typical POST request

            curl -X POST "https://your-domain.example.com/api/v1/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "first_name": "Jane",
    "last_name": "Doe"
  }'

          

Collection endpoints accept page and per_page query parameters for pagination. Where a resource has a human-friendly slug (events, courses) you can fetch it either by numeric id or by slug.

Partner API

The Partner API is a secret-key, server-to-server surface for facility-management integrators (POS, CRM, and booking systems). It exposes the facility entities a partner needs (customers and membership tiers, range bookings, bookable resources, scheduling and rosters, and orders) under a single versioned base path, /api/v1/partner . Every endpoint is browsable with a live "Try it out" panel in the Swagger UI under the partner tag.

Authentication

Partner endpoints authenticate with a secret X-Partner-Key header (format sk_partner_...), generated and rotated by an admin in Site Settings → Integrations. This is a server-to-server secret. Unlike the public funnel key (pk_...), it must never be embedded in client-delivered HTML, and the public key is rejected here. The partner API allows 600 requests per minute per source IP, enough to fully back-fill a multi-thousand-record tenant across every list endpoint in a single run. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers; a 429 adds Retry-After (seconds) so you can back off and resume from your last cursor cleanly.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/partner/customers" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"

          
200 Lean customer payload with tier + waiver

            {
  "data": [
    {
      "id": 1024,
      "first_name": "Pat",
      "last_name": "Member",
      "email": "pat@example.com",
      "phone": "+13135550100",
      "status": "active",
      "membership": {"tier_id": 4, "tier_name": "Gold", "tier_slug": "gold"},
      "waiver": {
        "status": "signed",
        "signed_at": "2026-03-01T00:00:00Z",
        "expires_at": "2027-03-01T00:00:00Z"
      },
      "inserted_at": "2026-01-12T14:03:00Z",
      "updated_at": "2026-05-15T17:13:44Z"
    }
  ],
  "meta": {"page": 1, "per_page": 50, "total": 1}
}

          
A missing or invalid key returns 401 on every partner route. The key is compared in constant time.

Pagination & incremental sync

Every list endpoint accepts page and per_page (default 50, max 100) and returns a meta block with page, per_page, and total. Pass updated_since (ISO 8601) to fetch only rows changed at or after that time. Results are ordered oldest-change-first, so you advance a cursor to the last updated_at you saw and page forward without missing or re-processing a row. Money is always integer cents.

Incremental sync (cursor pattern)

            # Page 1: everything changed since your last cursor.
curl -X GET "https://your-domain.example.com/api/v1/partner/memberships?updated_since=2026-06-01T00:00:00Z&per_page=100" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"

# Remember the largest updated_at you saw, then page forward with it.
curl -X GET "https://your-domain.example.com/api/v1/partner/memberships?updated_since=2026-06-14T09:12:33Z&per_page=100" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"

          

Pagination and incremental sync compose: hold an updated_since cursor steady and walk page forward until a page returns fewer than per_page rows. Each response's meta block (page, per_page, total) tells you where you are. After each run, persist the largest updated_at you saw as the next run's cursor, so a sync only ever processes customers that actually changed, never the whole book.

Incremental sync recipe (changed-only, paginated)

            # Incremental customer sync: pull ONLY changed customers, 100 at a time.
#
#  - cursor  = the largest `updated_at` you have processed so far
#              (store it between runs; start with a far-past date on first run)
#  - results come back oldest-change-first, so the last row of the last page
#    is your new cursor

cursor="2026-06-01T00:00:00Z"   # load from your last run; far-past on first run
page=1

while true; do
  resp=$(curl -s \
    -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY" \
    "https://your-domain.example.com/api/v1/partner/customers?updated_since=${cursor}&per_page=100&page=${page}")

  rows=$(echo "$resp" | jq '.data | length')
  [ "$rows" -eq 0 ] && break        # no more changed customers -> done

  # ... upsert each row into your POS:
  #   - membership.tier_slug -> your discount tier
  #   - waiver.status / waiver.signed_at / waiver.expires_at
  echo "$resp" | jq -c '.data[]' | while read -r customer; do
    : # your_upsert "$customer"
  done

  [ "$rows" -lt 100 ] && break       # short page -> last page, stop
  page=$((page + 1))
done

# Persist the largest updated_at you saw as the next run's cursor.

          
This is offset pagination, so do not change the cursor mid-walk; keep updated_since fixed for the whole loop and only advance it once the loop finishes. For a one-time full backfill, page promptly (or run it during a quiet window) so the offsets stay stable while you read.

Customers & memberships

GET /api/v1/partner/customers

List customers

Lean PII plus the customer's discount tier and waiver state inline. Supports search and updated_since.

GET /api/v1/partner/customers/:id

Retrieve a customer

A single customer by id.

GET /api/v1/partner/membership-levels

List membership tiers

All membership levels/tiers (id, name, slug, price_cents) to map to your POS discount rules.

GET /api/v1/partner/membership-levels/:id

Retrieve a membership tier

A single membership level.

GET /api/v1/partner/memberships

List memberships

Memberships with member identity, status, and tier block. Filter by status or membership_level_id.

GET /api/v1/partner/memberships/:id

Retrieve a membership

A single membership by id.

Range lanes & reservations

GET /api/v1/partner/range/lanes

List lanes

Active range lanes. Filter by location_id or active.

GET /api/v1/partner/range/lanes/:id/availability

Lane availability

Open start times for a lane. Query: date (required, YYYY-MM-DD), duration (minutes, default 60).

Check availability

            curl -X GET "https://your-domain.example.com/api/v1/partner/range/lanes/12/availability?date=2026-08-01&duration=60" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"

          
200 Open start times

            {
  "data": {
    "lane_id": 12,
    "date": "2026-08-01",
    "duration_minutes": 60,
    "slots": [
      "2026-08-01T13:00:00Z",
      "2026-08-01T13:30:00Z",
      "2026-08-01T14:00:00Z"
    ]
  }
}

          
GET /api/v1/partner/range/reservations

List reservations

Reservations. Filters: lane_id, location_id, status, from, to, updated_since.

GET /api/v1/partner/range/reservations/:id

Retrieve a reservation

A single reservation by id.

POST /api/v1/partner/range/reservations

Create a reservation

Books a lane on behalf of a guest. Requires lane_id, starts_at, ends_at, and guest_name + guest_phone. Priced server-side.

Create a reservation

            curl -X POST "https://your-domain.example.com/api/v1/partner/range/reservations" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lane_id": 12,
    "starts_at": "2026-08-01T13:00:00Z",
    "ends_at": "2026-08-01T14:00:00Z",
    "guest_name": "Pat Guest",
    "guest_phone": "+13135550100",
    "shooter_count": 2
  }'

          
201 Reservation created (server-priced)

            {
  "data": {
    "id": 6,
    "status": "reserved",
    "source": "counter",
    "lane_id": 12,
    "starts_at": "2026-08-01T13:00:00Z",
    "ends_at": "2026-08-01T14:00:00Z",
    "shooter_count": 2,
    "price_cents": 2500,
    "customer": {"name": "Pat Guest", "phone": "+13135550100", "email": null, "user_id": null},
    "cancellation_token": "r0c8X1...",
    "inserted_at": "2026-06-25T19:40:00Z",
    "updated_at": "2026-06-25T19:40:00Z"
  }
}

          
POST /api/v1/partner/range/reservations/:id/cancel

Cancel a reservation

Cancels a reservation by id.

Cancel a reservation

            curl -X POST "https://your-domain.example.com/api/v1/partner/range/reservations/6/cancel" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"

          
Partner bookings have no platform user, so they are guest bookings and are priced at walk-up rates server-side, so never send a price. They default to source: "counter" (which bypasses online-only policy gates) while still enforcing hours, closures, capacity, turnover, and overlap. Pass source: "online" to opt into the stricter gates.

Bookable resources & bookings

GET /api/v1/partner/resources

List resources

Bookable resources (bays, rooms, simulators). Filter by location_id or active.

GET /api/v1/partner/resources/:id

Retrieve a resource

A single resource by id.

GET /api/v1/partner/resources/:id/availability

Resource availability

Capacity-aware slots. Query: date (required), duration, party_size.

GET /api/v1/partner/bookings

List bookings

Resource bookings. Filters: status, from, to, updated_since.

GET /api/v1/partner/bookings/:id

Retrieve a booking

A single booking by id.

POST /api/v1/partner/bookings

Create a booking

Books a resource. Requires resource_id, starts_at, ends_at. Priced server-side; members-only/waiver/age gates and capacity/conflict checks are enforced.

Create a booking

            curl -X POST "https://your-domain.example.com/api/v1/partner/bookings" \
  -H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_id": 7,
    "starts_at": "2026-08-01T13:00:00Z",
    "ends_at": "2026-08-01T14:00:00Z",
    "party_size": 2,
    "customer_name": "Pat Guest",
    "customer_email": "pat@example.com"
  }'

          
POST /api/v1/partner/bookings/:id/cancel

Cancel a booking

Cancels a booking by id.

Locations, scheduling & orders

GET /api/v1/partner/locations

List locations

Locations with address and range scheduling config (slot/turnover minutes, advance window).

GET /api/v1/partner/locations/:id

Retrieve a location

A single location by id.

GET /api/v1/partner/events

List events

Events. Filters: status, visibility, updated_since.

GET /api/v1/partner/events/:id

Retrieve an event

A single event by id.

GET /api/v1/partner/events/:id/roster

Event roster

Attendees and their check-in state. Query: event_instance_id to scope to one session.

GET /api/v1/partner/orders

List orders

Orders with line items (cents). Filters: status, channel, updated_since.

GET /api/v1/partner/orders/:id

Retrieve an order

A single order by id.

Events

Events represent scheduled sessions that users can register for. Each event has ticket types, dates, and an optional venue.

GET /api/v1/events

List events

Returns upcoming and past events, sorted by start time.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/events" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
200 List of events

            {
  "data": [
    {
      "id": 42,
      "slug": "annual-retreat",
      "title": "Annual Retreat",
      "starts_at": "2026-06-15T09:00:00Z",
      "ends_at": "2026-06-15T17:00:00Z",
      "location": "123 Main St",
      "ticket_price_cents": 9900
    }
  ]
}

          
GET /api/v1/events/:slug

Retrieve an event

Fetch a single event by its slug, including ticket types and add-ons.

POST /api/v1/events/:slug/register

Register for an event

Registers the authenticated user for the given event.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/events/annual-retreat/register" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ticket_type_id": 7, "quantity": 1}'

          

Courses & Lessons

Courses contain sections and lessons. Once a user is enrolled, they can track lesson progress and complete the course.

GET /api/v1/courses

List courses

Returns all courses the authenticated user can see.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/courses" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
GET /api/v1/courses/:slug

Retrieve a course

Fetch a single course by slug, including its sections.

POST /api/v1/courses/:slug/enroll

Enroll in a course

Enrolls the authenticated user in the given course.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/courses/intro-workshop/enroll" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
POST /api/v1/lessons/:id/complete

Mark a lesson complete

Marks the given lesson as completed for the authenticated user.

Memberships

Memberships give users access to tiered benefits and, optionally, courses gated behind a membership level. Use these endpoints to check the current user's status.

GET /api/v1/membership

Current membership

Returns the authenticated user's active membership, if any.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/membership" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
200 Active membership

            {
  "data": {
    "id": "membership-uuid",
    "level": "Gold",
    "status": "active",
    "current_period_end": "2026-12-31T23:59:59Z"
  }
}

          
GET /api/v1/memberships

List membership history

Returns all past and present memberships for the authenticated user.

Membership Activation

When a club moves onto Pewpros from a legacy membership system, existing members are imported but their payment cards are not: card details stay in the old provider's vault and cannot be transferred. Each member therefore re-enters a card once, at /activate, and their subscription is anchored to the renewal date they already had. This section is the staff reference for that process.

Two public pages drive it. /activate is for members we already hold an email address for: they enter their membership number (the number on their physical card, never a credit card number) plus first and last name. /claim is for members with a card but no email on file: they supply an email at the same time and their account is created before they continue to payment. Both pages throttle by IP and by membership number, and both return deliberately vague errors so the number range cannot be probed.

Do not begin outreach until billing on the legacy system is confirmed off. A member who adds a card here while the old system is still charging them pays twice, to two companies, for the same month.

A matching name and number does not always complete activation. Three further conditions are checked, and each refuses rather than taking money on an assumption:

  • The renewal date has passed, or is missing. The imported next-payment date is captured once, at import, and goes stale as weeks pass. Rather than charge against a date we no longer trust, activation stops and an operator sets the real date. No card is taken and no money moves.
  • The membership plan could not be read. Members whose plan was blank in the export sit on a $0 placeholder plan. They are paying customers whose tier is simply unknown, not free members, so they cannot activate until a real plan is set.
  • The member already has a paid membership. Someone who has since bought a current plan is told there is nothing to activate, which stops a second subscription being started on top of the one they already pay.
  • The plan is genuinely free. A complimentary or volunteer tier is never asked for a payment card, because there is nothing to bill.
A stale renewal date is never repaired by guessing. Rolling it forward by one billing interval assumes the old system is still charging that member, and spot checks found that assumption false every time: some members had already converted, others had left. Guessing would hand out free terms, so the date is set from the member's real record instead.

The two blocking refusals read almost identically, so check which one names the renewal date and which names the membership plan: they need different fixes. The renewal-date refusal appears as a notice in the corner of the screen rather than inside the form, so a member may describe it as "a red box that popped up".

Red notice reading: We need to confirm your renewal date before you can activate online. Please contact us and we'll finish setting up your membership. Notice on the activation form reading: We need to confirm your membership plan before you can activate online. Please contact us and we'll finish setting up your membership.
The renewal-date refusal (top) and the unresolved-plan refusal (bottom) as the member sees them.

Operators work from three admin screens. Activation Links holds the shareable link and splits the awaiting-activation population into members who are ready to convert, members awaiting a plan, and members on free plans; only the first is an outreach list, and a single combined total is misleading. Legacy Claims is the no-email queue, where staff can attach an email on a member's behalf. Memberships surfaces a Card # and a Next Bill column, both badged red when they would block activation, and its Billing Anchor field is where a blocked member's real next payment date is set.

Members drift out of the ready pool over time: on a monthly cohort a few anchors fall into the past every day, and each one then needs a date set by hand. Work through the ready list promptly rather than letting it age.
The three awaiting-activation counts: ready to activate, awaiting a membership plan, and on free plans, each with an explanation of what the number means.
The three-way split on Activation Links. Only the first number is an outreach list.
Memberships list with Card # and Next Bill columns. One row shows a red 'none' card number, another a red past-dated Next Bill, and the rest grey future dates.
On the Memberships list, a red badge in either column is a member who will be turned away until someone fixes that record.

A blocked member is unblocked by setting Next Billing Date under Billing Anchor on their record. The field tells you which way it went.

Billing Anchor field with a future Next Billing Date and a grey hint reading 'No charge until Sep 1, 2026.' Billing Anchor field with a past Next Billing Date and a red warning reading 'This date has passed, so this member is blocked from activating. Set their real next payment date to unblock them.'
A future date defers the first charge (top). A past or blank date blocks the member until it is corrected (bottom).

The full staff-facing version of this guide is maintained in the Pewpros repository at docs/memberships/activation-staff-guide.md.

Groups

Groups let an organization buy seats in bulk and assign them to members (for example, a company training account). Group admins can invite new members, manage seats, and track course progress for their roster.

GET /api/v1/groups

List groups

Returns the groups the authenticated user belongs to or administers.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/groups" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
POST /api/v1/groups/:group_id/invitations

Invite a member

Sends an invitation to join the group to an email address.

GET /api/v1/groups/:group_id/seats

Seat usage

Shows how many seats are used and how many remain.

Community

The community API powers the social feed: posts, comments, likes, categories, and leaderboards. Posts are scoped to categories and respect block/report rules.

GET /api/v1/community/feed

Community feed

Returns a paginated feed of community posts.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/community/feed?page=1&per_page=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
POST /api/v1/community/posts

Create a post

Creates a new community post in the specified category.

POST /api/v1/community/posts/:id/like

Like or unlike a post

Toggles the authenticated user's like on the given post.

Contacts (CRM)

The contacts API is the CRM layer: create, update, tag, and note contacts; move them in and out of lists; and read their activity history.

GET /api/v1/contacts

List contacts

Returns a paginated list of contacts.

POST /api/v1/contacts

Create a contact

Creates a new contact. Email is required; other fields are optional.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "phone": "+15551234567"
  }'

          
POST /api/v1/contacts/:id/tags

Add tags to a contact

Attaches one or more tags to the given contact.

GET /api/v1/contacts/:contact_id/notes

List contact notes

Returns notes attached to the given contact, newest first.

Push Notifications

The push notifications API lets your mobile app register device tokens and send notifications to users. Device tokens are bound to the currently authenticated user and can be registered for either iOS (APNs) or Android (FCM).

POST /api/v1/devices

Register a device token

Stores a device token for the authenticated user so they can receive push notifications.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/devices" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_token": "APNS_OR_FCM_DEVICE_TOKEN",
    "platform": "ios",
    "environment": "production",
    "bundle_id": "com.example.yourapp"
  }'

          
201 Device registered

            {
  "data": {
    "id": "device-uuid",
    "platform": "ios",
    "environment": "production",
    "active": true
  }
}

          
DELETE /api/v1/devices

Deactivate a device token

Marks a previously registered device token inactive so it stops receiving notifications.

GET /api/v1/push/devices

List your devices

Returns the authenticated user's registered device tokens. Tokens are masked in the response.

POST /api/v1/push/test

Send a test notification

Sends a test push to every device registered to the authenticated user.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/push/test" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Hello from the API",
    "body": "This is a test notification."
  }'

          
POST /api/v1/push/send

Send a notification to a user

Sends a push notification to a specific user by email. Requires an owner, admin, or instructor role.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/push/send" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "title": "Your order is ready",
    "body": "Stop by when you get a chance.",
    "data": {"type": "order_update", "order_id": "ORDER-12345"}
  }'

          
The environment field on a registered device must match the APNs environment your build is signed for: sandbox for TestFlight and development builds, production for App Store releases. A mismatch will silently drop the notification.

Live Activities

Live Activities are the iOS feature that shows real-time updates on the lock screen and in the Dynamic Island. The API lets you start an activity for a user, advance it through named steps, and end it when the work is done. Each activity is described as an ordered list of steps (for example, ["Received", "Processing", "Shipped", "Delivered"]).

Two authentication styles are supported: your mobile app uses a Bearer token to register update tokens for a running activity, while external callers (automations, webhooks, CRMs) use an X-Admin-Key header to start, advance, and end activities by user email.

Mobile app endpoints

POST /api/v1/live-activities/push-to-start/register

Register a push-to-start token

Stores the push-to-start token so the server can wake the device and start a new activity.

POST /api/v1/live-activities/register

Register an activity update token

Stores the per-activity push token that iOS issues after the activity is live, so the server can send incremental updates.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/live-activities/register" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "activity_id": "activity-uuid",
    "reference_id": "ORDER-12345",
    "push_token": "IOS_ACTIVITY_PUSH_TOKEN",
    "environment": "production"
  }'

          

External caller endpoints

POST /api/v1/push/live-activity/start

Start a Live Activity

Starts a new Live Activity for a user identified by email. Returns the activity id that you will use for subsequent advance and end calls.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/push/live-activity/start" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "activity_type": "order_processing",
    "reference_id": "ORDER-12345",
    "title": "Order #12345",
    "subtitle": "Tracking your order",
    "steps": ["Received", "Processing", "Shipped", "Delivered"]
  }'

          
201 Activity started

            {
  "success": true,
  "data": {
    "id": "activity-uuid",
    "activity_type": "order_processing",
    "reference_id": "ORDER-12345",
    "title": "Order #12345",
    "subtitle": "Tracking your order",
    "steps": ["Received", "Processing", "Shipped", "Delivered"],
    "current_step_index": 0,
    "current_step_name": "Received",
    "total_steps": 4,
    "status": "active",
    "created_at": "2026-06-15T09:00:00Z",
    "updated_at": "2026-06-15T09:00:00Z"
  }
}

          
POST /api/v1/push/live-activity/:id/advance

Advance to the next step

Advances the activity to the next step in its steps array. Accepts an optional reference_id body parameter as an alternative to the path id.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/push/live-activity/activity-uuid/advance" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

          
POST /api/v1/push/live-activity/:id/end

End a Live Activity

Ends the activity and stops future updates. Accepts an optional reference_id body parameter as an alternative to the path id, plus an optional status (completed, cancelled, or failed).

Example request

            curl -X POST "https://your-domain.example.com/api/v1/push/live-activity/activity-uuid/end" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'

          
GET /api/v1/push/live-activities

List Live Activities

Returns all Live Activities. Filter with the status and activity_type query parameters.

GET /api/v1/push/live-activities/:id

Retrieve a Live Activity

Fetches a single Live Activity by id, including its current step and status.

Once an activity is ended, iOS will not accept further updates for it. Always start a new activity (with a fresh reference_id) rather than trying to resurrect a completed one.

Approvals

Approvals let automation steps pause and wait for a human to approve or deny them before continuing. Two authentication styles are supported: your mobile app uses a Bearer token to list and respond to approvals, while external callers (automations, webhooks, CRMs) use an X-Admin-Key header to send interactive approval push notifications.

Mobile app endpoints

GET /api/v1/approvals

List approvals

Returns approvals assigned to the authenticated user. Use the status query parameter to filter by pending, approved, or denied.

Example request

            curl -X GET "https://your-domain.example.com/api/v1/approvals?status=pending" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
200 Pending approvals

            {
  "approvals": [
    {
      "id": "approval-uuid",
      "status": "pending",
      "requested_at": "2026-06-15T09:00:00Z",
      "responded_at": null,
      "expires_at": "2026-06-16T09:00:00Z",
      "run_id": "run-uuid",
      "step_id": "step-uuid",
      "context": {
        "summary": "Approve outbound message",
        "details": "This automation step requires human review before continuing."
      }
    }
  ]
}

          
POST /api/v1/approvals/:id/approve

Approve a request

Approves the approval request. The paused automation run resumes on the next scheduler tick.

Example request

            curl -X POST "https://your-domain.example.com/api/v1/approvals/approval-uuid/approve" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

          
POST /api/v1/approvals/:id/deny

Deny a request

Denies the approval request and signals the automation run to branch accordingly.

External caller endpoints

POST /api/v1/admin/push/send-approval

Send an interactive approval notification

Sends a rich push notification to a user by email with inline approve/reject action buttons. The iOS app renders the actions via the Notification Service Extension and persists them to the widget store. Authenticated via X-Admin-Key header.

For simple cases, pass convenience parameters and the endpoint builds the action buttons for you. For full control, pass a custom approval_actions array instead.

Parameter Required Description
email Yes Email of the user to notify.
title No Notification title. Defaults to "Approval Required".
body No Notification body text.
subtitle No Secondary text shown in the approval card.
details No Additional context rendered in the expanded notification view.
approval_id No Your own ID for this approval. A UUID is generated if omitted. Used to build the default approve/reject URLs.
approve_url No Override the approve action endpoint. Defaults to /api/v1/approvals/:id/approve.
reject_url No Override the reject action endpoint. Defaults to /api/v1/approvals/:id/deny.
approve_label No Label for the approve button. Defaults to "Approve".
reject_label No Label for the reject button. Defaults to "Reject".
auth_type No Authentication method the app uses when calling action endpoints. Defaults to "app_token".
approval_mode No How the approval is rendered. Defaults to "direct".
plugin_name No Source system name shown in the notification. Defaults to "PewPros".
expires_at No ISO 8601 timestamp after which the approval is no longer actionable.
approval_actions No Full custom actions array. When provided, all convenience params (approve_url, reject_url, labels, auth_type) are ignored.
Convenience params example

            curl -X POST "https://your-domain.example.com/api/v1/admin/push/send-approval" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "title": "New Instructor Application",
    "body": "John Smith has applied to become an instructor.",
    "subtitle": "Review and approve or reject this application.",
    "approval_id": "instr-app-42",
    "approve_label": "Accept",
    "reject_label": "Decline"
  }'

          
200 Notification sent

            {
  "success": true,
  "sent_to": "user@example.com",
  "result": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "data": {
    "type": "interactive_approval",
    "notification_type": "approval",
    "id": "approval_instr-app-42",
    "plugin_name": "PewPros",
    "category": "approval",
    "approval_required": true,
    "approval_mode": "direct",
    "item_title": "New Instructor Application",
    "item_subtitle": "Review and approve or reject this application.",
    "item_details": "",
    "status": "pending",
    "timestamp": "2026-06-15T09:00:00Z",
    "approval_actions": [
      {
        "action_id": "approve",
        "label": "Accept",
        "api_endpoint": "https://your-domain.example.com/api/v1/approvals/instr-app-42/approve",
        "method": "POST",
        "confirm": false,
        "auth_type": "app_token"
      },
      {
        "action_id": "reject",
        "label": "Decline",
        "api_endpoint": "https://your-domain.example.com/api/v1/approvals/instr-app-42/deny",
        "method": "POST",
        "confirm": true,
        "auth_type": "app_token"
      }
    ]
  }
}

          
Full custom actions example

            curl -X POST "https://your-domain.example.com/api/v1/admin/push/send-approval" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "title": "Approve Payout",
    "body": "A $500.00 payout is ready to send.",
    "approval_id": "payout-99",
    "approval_actions": [
      {
        "action_id": "approve",
        "label": "Send Payout",
        "api_endpoint": "https://hooks.example.com/payout/99/approve",
        "method": "POST",
        "confirm": false,
        "auth_type": "webhook_hmac"
      },
      {
        "action_id": "reject",
        "label": "Hold",
        "api_endpoint": "https://hooks.example.com/payout/99/hold",
        "method": "POST",
        "confirm": true,
        "auth_type": "webhook_hmac"
      }
    ]
  }'

          
Approvals can expire. Once expires_at passes, the approval is no longer actionable and the automation run will follow its configured timeout branch.

Automations

Automations are event-driven workflows that run automatically when a trigger fires. You can build automations visually in the admin panel or create them via the MCP tools. Each automation has a trigger type, an optional trigger config for filtering, and a definition containing the workflow nodes and edges.

Supported trigger types include membership_created, course_enrolled, event_booked, payment_received, webhook_received, schedule, and more. Automations must be activated before they respond to events.

Workflow nodes represent individual steps such as sending an email, adding a CRM tag, creating a delay, or branching based on a condition. The visual builder at /admin/automations/:id/build lets you drag, connect, and configure nodes.

Automation lifecycle

Status Description
draft Newly created. Will not respond to trigger events.
active Listening for trigger events and creating runs automatically.
paused Temporarily stopped. Existing runs complete but no new runs are created.
archived Retired. Hidden from the active list.

Approval gates

Any automation node can be configured to require approval before executing. When a run reaches an approval node, it pauses and sends a push notification to the admin. The run resumes when the approval is granted via the mobile app, the web UI, or the /api/v1/approvals/:id/approve endpoint (see the Approvals section).

Webhook Triggers

Automations with trigger type webhook_received get a unique, unguessable webhook URL. Any external system can fire the automation by sending a POST request to that URL. The URL uses a UUID path segment that is auto-generated when the automation is created.

POST /webhooks/automation/:uuid

Fire a webhook automation

Sends a JSON payload to trigger all active automations matching this webhook path. Returns a correlation ID for tracing.

Example request

            curl -X POST "https://your-domain.example.com/webhooks/automation/dd132cc3-824e-4047-8010-7c6154e6f69b" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "event_type": "signup",
    "name": "Jane Doe"
  }'

          
200 Webhook accepted

            {
  "status": "ok",
  "path": "dd132cc3-824e-4047-8010-7c6154e6f69b",
  "correlation_id": "a3bbeddb-5c15-46ab-8df7-a3415cf69def"
}

          

Finding the webhook URL

The webhook URL is displayed in three places in the admin panel:

  • The automation Show page (with a Copy button and sample cURL)
  • The Edit form (read-only field with Copy button)
  • The Builder header bar

It is also returned by the MCP tools automation_get, automation_create, and automation_list in the webhook_url field.

Payload mappings

By default, the incoming JSON body is nested under trigger.body in the automation context. Payload mappings let you extract nested fields and promote them to top-level trigger variables so they can be referenced as {{trigger.user_email}} in downstream nodes.

Each mapping has a name (the variable name to create) and a source (a dot-path into the trigger data). For example, body.email extracts the email field from the webhook body.

Trigger config with mappings

            {
  "trigger_config": {
    "path": "dd132cc3-824e-4047-8010-7c6154e6f69b",
    "payload_mappings": [
      {"name": "user_email", "source": "body.email"},
      {"name": "action", "source": "body.event_type"},
      {"name": "user_name", "source": "body.name"}
    ]
  }
}

          

With the mappings above, a POST with email in the body produces this trigger data:

Resulting trigger data

            {
  "path": "dd132cc3-...",
  "body": {
    "email": "user@example.com",
    "event_type": "signup",
    "name": "Jane Doe"
  },
  "headers": {
    "content-type": "application/json",
    "x-custom-header": "value"
  },
  "received_at": "2026-04-06T04:13:55Z",
  "correlation_id": "a3bbeddb-...",
  "user_email": "user@example.com",
  "action": "signup"
}

          

Headers

All x-* prefixed headers, plus content-type and user-agent, are captured in trigger.headers. You can map header values with a source path like headers.x-api-key.

Deduplication

Duplicate payloads (identical JSON body within a short window) are automatically detected and return a "status": "deduplicated" response. The automation does not fire a second time. Each delivery is logged with a correlation ID for debugging.

Webhook URLs are secret. Treat them like API keys: do not expose them in client-side code or public repositories. If a URL is compromised, edit the automation and the system will preserve the existing UUID path, or you can delete and recreate the automation to get a new URL.

MCP Tools

The platform exposes an MCP (Model Context Protocol) server that lets AI agents interact with every feature programmatically. The MCP endpoint is at /mcp/v1 and uses the same Bearer token as the REST API.

MCP connection

            POST /mcp/v1 HTTP/1.1
Host: your-domain.example.com
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

          

Available tool categories

Category Tools Description
CRM 14 Contacts, tags, lists, notes, activities
Automation 15 Create, list, trigger, manage runs, approvals, templates, node types
Events 6 List, get, register, update capacity, cancel
Courses 3 List, get details, enroll students
Messaging 3 Conversations, messages, send SMS
Agency 5 Multi-tenant provisioning, snapshots, locations
Other 15 Groups, community, appointments, certifications, shop, reporting

Automation MCP tools

The automation tools let AI agents create and manage workflows programmatically. Webhook automations return a webhook_url field in all responses.

Tool Description
automation_list List automations with filters
automation_get Get full automation with definition
automation_create Create from scratch or from template
automation_update Update name, trigger, or definition
automation_delete Delete an automation
automation_activate Set status to active
automation_deactivate Set status to paused
automation_trigger Manually trigger a run
automation_list_runs List runs with status filter
automation_get_run Get run details with all steps
automation_cancel_run Cancel a running automation
automation_list_templates Browse pre-built templates
automation_list_node_types List available node types
automation_list_approvals List pending approval requests
automation_respond_approval Approve or deny an approval
For the full parameter reference for each MCP tool, see the Interactive API docs or the full MCP specification .

Errors

Errors follow a consistent JSON shape. The HTTP status code indicates the category of failure; the body provides additional detail.

404 Resource not found

            {
  "errors": [
    {
      "message": "Resource not found",
      "code": "not_found"
    }
  ]
}

          
Status Meaning
200 OK. The request succeeded.
201 Created. The resource was created successfully.
400 Bad request. The request body or parameters were invalid.
401 Unauthorized. Missing or invalid Bearer token.
403 Forbidden. Your token does not have access to this resource.
404 Not found. The resource does not exist.
422 Unprocessable. Validation failed; see the errors array.
429 Too many requests. Slow down and retry after a short delay.
500 Server error. Please try again later.

Next steps

Ready to build? Head to the interactive Swagger UI to browse every endpoint and try calls against your own account. If you'd rather work locally, download the spec from /api/docs/openapi.json and import it into your API client or code generator.