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.
Creating an Event
- Go to Events and click New Event.
- Fill in the form (explained below) and click Save Event.
- Open the event and add its dates with Add Session (see Dates and Sessions).
- Add at least one ticket with Add Ticket Type (see Tickets, Pricing, and Add-ons).
- 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
- 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.
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:
- The session's Capacity Override.
- The event's Max Capacity.
- The venue's capacity.
- 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.
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.
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
| 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
- Click Register Now (or Register next to a date).
- Pick a date and a ticket if there is more than one.
- Choose how many tickets (up to 10), then enter each attendee's name, email, and phone. Each attendee needs their own email address.
- Add any extras, and enter a promo code if you have one.
- 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 |
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
| 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.
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.
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
| 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.
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 |
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
- Test connection: confirms your credentials work before anything else runs.
- Sync now: pulls the catalog in. The first pull of a large catalog takes a while and continues in the background.
- Refresh on-hand: updates stock levels and, for Sports South, current pricing.
- Turn on auto-sync so it keeps itself current without you asking.
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.
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.
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.
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.
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.
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.
{
"error": "unauthorized"
}
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.
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.
curl -X GET "https://your-domain.example.com/api/v1/partner/customers" \
-H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"
{
"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}
}
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.
# 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 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.
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
/api/v1/partner/customers
List customers
Lean PII plus the customer's discount tier and waiver state inline. Supports search and updated_since.
/api/v1/partner/customers/:id
Retrieve a customer
A single customer by id.
/api/v1/partner/membership-levels
List membership tiers
All membership levels/tiers (id, name, slug, price_cents) to map to your POS discount rules.
/api/v1/partner/membership-levels/:id
Retrieve a membership tier
A single membership level.
/api/v1/partner/memberships
List memberships
Memberships with member identity, status, and tier block. Filter by status or membership_level_id.
/api/v1/partner/memberships/:id
Retrieve a membership
A single membership by id.
Range lanes & reservations
/api/v1/partner/range/lanes
List lanes
Active range lanes. Filter by location_id or active.
/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).
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"
{
"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"
]
}
}
/api/v1/partner/range/reservations
List reservations
Reservations. Filters: lane_id, location_id, status, from, to, updated_since.
/api/v1/partner/range/reservations/:id
Retrieve a reservation
A single reservation by id.
/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.
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
}'
{
"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"
}
}
/api/v1/partner/range/reservations/:id/cancel
Cancel a reservation
Cancels a reservation by id.
curl -X POST "https://your-domain.example.com/api/v1/partner/range/reservations/6/cancel" \
-H "X-Partner-Key: sk_partner_YOUR_SECRET_KEY"
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
/api/v1/partner/resources
List resources
Bookable resources (bays, rooms, simulators). Filter by location_id or active.
/api/v1/partner/resources/:id
Retrieve a resource
A single resource by id.
/api/v1/partner/resources/:id/availability
Resource availability
Capacity-aware slots. Query: date (required), duration, party_size.
/api/v1/partner/bookings
List bookings
Resource bookings. Filters: status, from, to, updated_since.
/api/v1/partner/bookings/:id
Retrieve a booking
A single booking by id.
/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.
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"
}'
/api/v1/partner/bookings/:id/cancel
Cancel a booking
Cancels a booking by id.
Locations, scheduling & orders
/api/v1/partner/locations
List locations
Locations with address and range scheduling config (slot/turnover minutes, advance window).
/api/v1/partner/locations/:id
Retrieve a location
A single location by id.
/api/v1/partner/events
List events
Events. Filters: status, visibility, updated_since.
/api/v1/partner/events/:id
Retrieve an event
A single event by id.
/api/v1/partner/events/:id/roster
Event roster
Attendees and their check-in state. Query: event_instance_id to scope to one session.
/api/v1/partner/orders
List orders
Orders with line items (cents). Filters: status, channel, updated_since.
/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.
/api/v1/events
List events
Returns upcoming and past events, sorted by start time.
curl -X GET "https://your-domain.example.com/api/v1/events" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"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
}
]
}
/api/v1/events/:slug
Retrieve an event
Fetch a single event by its slug, including ticket types and add-ons.
/api/v1/events/:slug/register
Register for an event
Registers the authenticated user for the given event.
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.
/api/v1/courses
List courses
Returns all courses the authenticated user can see.
curl -X GET "https://your-domain.example.com/api/v1/courses" \
-H "Authorization: Bearer YOUR_API_TOKEN"
/api/v1/courses/:slug
Retrieve a course
Fetch a single course by slug, including its sections.
/api/v1/courses/:slug/enroll
Enroll in a course
Enrolls the authenticated user in the given course.
curl -X POST "https://your-domain.example.com/api/v1/courses/intro-workshop/enroll" \
-H "Authorization: Bearer YOUR_API_TOKEN"
/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.
/api/v1/membership
Current membership
Returns the authenticated user's active membership, if any.
curl -X GET "https://your-domain.example.com/api/v1/membership" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"data": {
"id": "membership-uuid",
"level": "Gold",
"status": "active",
"current_period_end": "2026-12-31T23:59:59Z"
}
}
/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.
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.
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".
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.
A blocked member is unblocked by setting Next Billing Date under Billing Anchor on their record. The field tells you which way it went.
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.
/api/v1/groups
List groups
Returns the groups the authenticated user belongs to or administers.
curl -X GET "https://your-domain.example.com/api/v1/groups" \
-H "Authorization: Bearer YOUR_API_TOKEN"
/api/v1/groups/:group_id/invitations
Invite a member
Sends an invitation to join the group to an email address.
/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.
/api/v1/community/feed
Community feed
Returns a paginated feed of community posts.
curl -X GET "https://your-domain.example.com/api/v1/community/feed?page=1&per_page=20" \
-H "Authorization: Bearer YOUR_API_TOKEN"
/api/v1/community/posts
Create a post
Creates a new community post in the specified category.
/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.
/api/v1/contacts
List contacts
Returns a paginated list of contacts.
/api/v1/contacts
Create a contact
Creates a new contact. Email is required; other fields are optional.
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"
}'
/api/v1/contacts/:id/tags
Add tags to a contact
Attaches one or more tags to the given contact.
/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).
/api/v1/devices
Register a device token
Stores a device token for the authenticated user so they can receive push notifications.
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"
}'
{
"data": {
"id": "device-uuid",
"platform": "ios",
"environment": "production",
"active": true
}
}
/api/v1/devices
Deactivate a device token
Marks a previously registered device token inactive so it stops receiving notifications.
/api/v1/push/devices
List your devices
Returns the authenticated user's registered device tokens. Tokens are masked in the response.
/api/v1/push/test
Send a test notification
Sends a test push to every device registered to the authenticated user.
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."
}'
/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.
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"}
}'
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
/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.
/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.
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
/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.
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"]
}'
{
"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"
}
}
/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.
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 '{}'
/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).
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"}'
/api/v1/push/live-activities
List Live Activities
Returns all Live Activities. Filter with the status and activity_type query parameters.
/api/v1/push/live-activities/:id
Retrieve a Live Activity
Fetches a single Live Activity by id, including its current step and status.
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
/api/v1/approvals
List approvals
Returns approvals assigned to the authenticated user. Use the status query parameter to filter by pending, approved, or denied.
curl -X GET "https://your-domain.example.com/api/v1/approvals?status=pending" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"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."
}
}
]
}
/api/v1/approvals/:id/approve
Approve a request
Approves the approval request. The paused automation run resumes on the next scheduler tick.
curl -X POST "https://your-domain.example.com/api/v1/approvals/approval-uuid/approve" \
-H "Authorization: Bearer YOUR_API_TOKEN"
/api/v1/approvals/:id/deny
Deny a request
Denies the approval request and signals the automation run to branch accordingly.
External caller endpoints
/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 |
|---|---|---|
| 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. |
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"
}'
{
"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"
}
]
}
}
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"
}
]
}'
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.
/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.
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"
}'
{
"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": {
"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:
{
"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.
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.
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 |
Errors
Errors follow a consistent JSON shape. The HTTP status code indicates the category of failure; the body provides additional detail.
{
"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.