API reference
Clubs on the Membership add-on can read and update their members, memberships, events, sign-ins and payments from their own website or tools like Zapier, send their join, renew, donate and contact forms from forms of their own, start payment pages for dues and donations, and hear about changes through webhooks. The dashboard and the API use the same rules, so anything done here is exactly what the dashboard would do.
Keys and permissions
An owner makes keys in the dashboard under Membership → API and webhooks, each with a name and only the permissions it needs. A key is shown once, then kept only in scrambled form; lose it and make another. Revoking a key stops it at once. Every change made with a key is written to the club's activity log, and the dashboard shows when each key was last used.
curl https://demo.passthepad.com/api/v1/members?status=current \
-H "Authorization: Bearer ptp_live_…"
ptp_live_…keys work on the club's real data.ptp_test_…keys read real data, but their writes are only checked: the answer shows what would have happened (with"livemode": falseand aPTP-Test-Mode: trueheader), and nothing is saved, emailed, sent to webhooks or charged. Ids in those answers don't exist.
| Permission | Allows |
|---|---|
members:read | See people: names, emails, tags and membership status |
members:write | Add and change people and their tags |
memberships:read | See levels and memberships |
memberships:write | Give, renew and cancel memberships |
forms:read | See forms and what was sent through them |
forms:write | Send forms: join, renew, donate and contact, as a person filling them in would |
events:read | See events and who registered for them: names and emails |
events:write | Add events |
signins:read | See who signed in |
payments:read | See payments and refunds |
payments:write | Start payment pages for dues and donations |
members:login | Sign members in: make sign-in links, check and refresh member tokens, and sign members out everywhere |
webhooks:manage | Add and remove webhook endpoints, for the events this key may also read |
Refunds and changes to the club's Stripe or PayPal connection are deliberately not in the API: they stay in the dashboard.
Requests
- Base address:
https://demo.passthepad.com/api/v1. Send JSON bodies withContent-Type: application/json; answers are JSON. - Money is in whole cents (
6500is $65.00), currency USD. Dates areYYYY-MM-DDin the club's time zone; times are UTC, like2026-03-31T18:30:00Z. - Limits: 600 requests a minute per key. Every answer has
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(a Unix time); past the limit you get429withRetry-After. - Pages: lists come newest first as
{"object": "list", "data": [...], "has_more": true, "next_cursor": "…"}. Ask for the next page withstarting_after=<next_cursor>;limitis 1 to 100 (25 when left out). - Retries: send an
Idempotency-Keyheader (any unique string up to 255 characters) with aPOST,PATCHorDELETE. Sending the same request again with the same key within 24 hours returns the first answer (withIdempotent-Replayed: true) instead of doing it twice. The same key with a different request is refused with422.
curl -X POST https://demo.passthepad.com/api/v1/memberships \
-H "Authorization: Bearer ptp_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: website-order-1042" \
-d '{"email": "pat@example.org", "first_name": "Pat", "last_name": "Lee", "level_id": 3}'
Errors
Anything but a 2xx answer has an error object saying what to fix:
{"error": {"type": "invalid_request", "code": "missing_param", "message": "email is required.", "param": "email"}}
| Status | Means |
|---|---|
| 400 | The request can't be read (invalid_json, invalid_param). |
| 401 | No key, a wrong key, or a revoked one (unauthorized). |
| 402 | The club's plan is read-only, so nothing new can be added (plan_read_only). |
| 403 | The key lacks the permission (missing_scope), or Membership isn't on for the club (membership_off). |
| 404 | No such address, or nothing with that id in this club. |
| 409 | It clashes with what's there (already_member, email_taken), it can't be done right now (form_unavailable, donations_off, no_processor, not_completed), or the same Idempotency-Key is still being handled. |
| 422 | Something sent isn't right (invalid, invalid_param, missing_param, idempotency_key_reused). A form sent with mistakes also lists every one in errors: {"error": {"code": "invalid", "message": "Enter your first name.", "param": "first_name", "errors": {"first_name": "Enter your first name.", "answers.q1a2b3c4d": "Please answer this."}}}. |
| 429 | Over 600 requests this minute (rate_limited). |
| 500 | Our fault. Try again; with an Idempotency-Key it's safe. |
Endpoints
Members
List people, newest first. Filters: q (name or email), email (exact), status (any = has or had a membership, none, current, active, grace, pending, lapsed, cancelled, expiring; everyone when left out), level_id, tag, attended_event_id, mailing_list (true or false), saved_filter_id (one of the Members list's saved filters), updated_since (includes membership changes).
One person.
Add a person: email (required), first_name, last_name, tags (a list), fields (answers to the club's profile fields, see PATCH). One record per email: if the email is already in the club, that person is returned with 200 (blank names filled in, tags added) instead of 201.
Change first_name, last_name, email (409 email_taken if someone else has it), tags (replaces them all), add_tags, remove_tags, fields: answers to the club's profile fields as an object keyed by field label or id, such as {"T-shirt size": "M", "Volunteer": true}. Only the fields named change; null clears one. A checkbox takes true or false, several choices a list, a date 2026-03-31, the rest a string; a choice must be one of the field's.
Signing members in
A one-time sign-in link for a member, from your own systems (a "Members area" button, your app, a support tool). to: site (the members-only sign-in: they press one button on our page and come back to return_url, one of the websites listed under Members-only pages, with a token; the default when members-only pages are on) or portal (the member portal). send_email: true emails it to them; false (the default) only returns it, for you to hand over, so keep it as safe as a password. Works once, for 30 minutes. 409 not_a_member for someone who never had a membership, 409 sso_off / portal_closed when that side is switched off.
Check a member token on your server: token, and audience (your website's origin, like https://yourclub.org) to make sure it was made for you. Answers 200 either way: valid with the member (id, name, email, status, level), issued_at and expires_at; or valid: false with an error (expired, signed_out, not_a_member, wrong_audience, bad_signature, malformed) and a message.
Keep a member signed in: send a token that still checks out (and audience), get a fresh one for the same website with a new expiry. Refresh before it expires; an expired, signed-out or forged token is 401 and the member signs in again.
Sign a member out everywhere: their member portal and member-page sessions end, sign-in links still out stop working, and every token made before now fails /sessions/verify with signed_out (in the browser script too). They can sign in again straight away. Noted on their timeline.
Levels
The club's membership levels and prices. include_archived=true adds archived ones.
One level.
Memberships
Filters: member_id, level_id, status (active, grace, pending, lapsed, cancelled).
One membership.
Give someone a membership paid elsewhere, or comped: level_id (required), member_id or email (with first_name, last_name; the person is found, or added when the key also has members:write), starts_on, expires_on (dates; one term from the start when left out), comped (true or false), note, send_welcome (true: on a free level, email them a link to set a password for the club's member portal, if the club has those emails on; nothing is emailed otherwise). 409 already_member if they have a current membership: renew it instead.
One more term from where the current one ends (from today if it lapsed), or to expires_on. Also comped, note.
Cancel now, or with "at_period_end": true at the end of the time paid for. Its automatic renewal is stopped either way. Nothing is refunded.
Events
Filters: from, to (dates), series_id. Each event says how many signed in.
Add a one-date event: name, date (required), start_time, end_time ("18:30", the club's time zone).
Who registered for the event (event tickets), newest first: registration objects with ticket_type, member_id, names, email, status (confirmed, pending, waitlist, offered, cancelled), price_cents, refunded_cents, checked_in and checked_in_at. Filter: status.
Sign-ins
Who signed in, newest first. Filters: event_id, member_id, from, to (dates in the club's time zone). via is tablet, phone or import (past attendance brought in from another system).
Payments
Payments and their refunds. Filters: member_id, status (paid, refunded, failed), purpose (dues, donation, ticket, store, other), from, to.
One payment with its refunds.
Forms
The club's join, renew, donate and contact forms, with everything a page of your own needs to ask the same things: questions, and per kind the levels offered (join) or the suggested amounts_cents, other_amount, min_cents and monthly_offered (donate). unavailable says why a form can't be sent right now (closed, no way to take payments, a read-only plan), or is null.
One form.
Send a form from your own page, exactly as someone filling in ours would: the same checks, the same emails to your team, the same timeline and webhook. email, first_name, last_name (not for renew), answers (an object: each question's id and its answer: a string, a list for several choices, true or false for a tickbox), mailing_list (where the form offers it; the person confirms by email). Join: level_id, auto_renew, confirm_by_email (false when left out: your website vouches for the person, so a free level is given at once or the payment page is made now; true emails them a link to finish, as our form does). Donate: amount_cents (a suggested amount, or any amount from min_cents when the form takes others), monthly. Renew: send_email (false when left out: nothing is emailed and the answer carries the renewal link; true emails it, as our form does). Contact: message. Paying kinds also take processor (needed only when the club has more than one), success_url and cancel_url (where the payer goes after the payment page, see Checkouts; the form's own thank-you page when left out). Answers 201 with the submission and next: action is pay (send them to url), done, check_email, already_member, renew (the renewal link in url), renews_automatically, nothing_to_renew or not_found, with a message in plain words. Mistakes are 422 naming the first field in param, with every one in errors; a closed form is 409 form_unavailable. The dashboard shows each one as sent through the API.
What was sent through a form, newest first. Filter: status (unconfirmed, received, awaiting_payment, paid, failed, refunded).
One submission.
Checkouts
Start a payment page without a form, for a club that takes payments on its own website. purpose: membership or donation; the person by member_id, or email (with first_name, last_name; for a membership someone new is added only when the key also has members:write; a donor is added once the gift is paid). Membership: level_id, auto_renew, action (join, renew or upgrade; worked out from their current membership when left out), with the same prices and rules as the dashboard. Donation: amount_cents, frequency (once, month, or year where the donation page offers it), fund, note, anonymous; the donation page must be on (409 donations_off), and gifts get its tax wording and year-end statements. Both take processor, send_email (also email the payer the link) and success_url / cancel_url: full https:// addresses on your website (up to 500 characters; {CHECKOUT_ID} in them becomes the checkout's id). The payer comes back through Pass the Pad, which checks the payment first, so GET /checkouts/{id} already says paid when your page asks. cancel_url is success_url when left out; without either they see our own thank-you page.
One payment page and how it stands: status open, paid, expired (closed unpaid, or replaced by a newer page) or cancelled (it couldn't be made). An open one is checked with the processor when asked.
Webhooks
The club's webhook endpoints.
Add one: url (https), events (a list, or ["*"] for all), description. The answer includes the signing secret, shown this once (a replay with the same Idempotency-Key leaves it out). A key can subscribe only to events it could read: member.* needs members:read, membership.* memberships:read, payment.* payments:read, registration.* events:read, signin.* signins:read and form.* forms:read; ["*"] means all of those. Revoking the key turns off the endpoints it added.
Stop sending to an endpoint.
Objects
Member
{"id": 812, "object": "member", "email": "pat@example.org", "first_name": "Pat", "last_name": "Lee",
"status": "active", "level": {"id": 3, "name": "Professional"}, "membership_id": 95,
"expires_on": "2027-03-31", "member_since": "2024-03-31", "tags": ["Board", "Volunteer"],
"fields": [{"id": 4, "label": "T-shirt size", "type": "choice", "value": "M"},
{"id": 7, "label": "Employer", "type": "text", "value": null}],
"mailing_list": true, "visits": 14, "first_seen_at": "2024-01-09T02:15:00Z",
"last_seen_at": "2026-10-09T01:58:00Z", "updated_at": "2026-10-12T16:20:00Z"}
fields lists every one of the club's profile fields with this person's answer: type is text, long_text, choice, multi_choice (the value is a list), date or checkbox (true or false); null when unanswered. member.updated is sent when answers change.
status is the person's best membership (active, grace, pending, lapsed, cancelled), or null for someone who has never been a member.
Membership
{"id": 95, "object": "membership", "member_id": 812, "level": {"id": 3, "name": "Professional"},
"status": "active", "price_cents": 6500, "currency": "USD", "term": "year", "comped": false,
"started_on": "2024-03-31", "expires_on": "2027-03-31", "grace_until": null, "auto_renew": true,
"cancel_at_period_end": false, "subscription": {"processor": "stripe", "status": "active", "amount_cents": 6500,
"interval": "year", "current_period_end": "2027-03-31T07:00:00Z", "cancel_at": null},
"people": [813], "source": "dashboard", "created_at": "…", "updated_at": "…"}
term is month, year, lifetime or free (free forever). expires_on is the renewal date; after it the membership has a grace period (grace_until), then lapses. people lists the others on a family or business membership.
Level, event, sign-in, payment
{"id": 3, "object": "level", "name": "Professional", "description": "…", "price_cents": 6500, "currency": "USD",
"term": "year", "auto_renew": true, "family_size": 1, "public": true, "archived": false}
{"id": 51, "object": "event", "name": "Middlers", "date": "2026-10-08", "starts_at": "2026-10-09T01:30:00Z",
"ends_at": null, "series_id": 4, "signins": 23}
{"id": 9012, "object": "signin", "event_id": 51, "member_id": 812, "first_name": "Pat", "last_name": "Lee",
"email": "pat@example.org", "mailing_list": true, "returning": true, "via": "tablet", "signed_in_at": "…"}
{"id": 77, "object": "payment", "amount_cents": 6500, "refunded_cents": 0, "currency": "USD", "status": "paid",
"processor": "stripe", "processor_ref": "pi_…", "description": "Professional membership – Your Club",
"purpose": "dues", "member_id": 812, "payer_name": "Pat Lee", "payer_email": "pat@example.org",
"receipt_number": 41, "test": false, "failure": null, "period_start": "…", "period_end": "…", "paid_at": "…",
"refunds": [{"id": "re_…", "amount_cents": 1000, "created_at": "…"}]}
Form, form submission, checkout
{"id": 4, "object": "form", "kind": "join", "name": "Join Your Club", "status": "open",
"url": "https://demo.passthepad.com/f/your-club/join", "embed_code": "<iframe …", "intro": "…", "button_label": "Join",
"thanks": "…", "offers_mailing_list": true, "mailing_list_label": "Email me news",
"questions": [{"id": "q1a2b3c4d", "label": "How did you hear about us?", "type": "choice", "required": true,
"options": ["A friend", "The web"], "help": ""}],
"unavailable": null, "processors": ["stripe"],
"levels": [{"id": 3, "name": "Professional", "description": "…", "price_cents": 6500, "term": "year", "auto_renew_offered": true}]}
{"id": 230, "object": "form_submission", "form_id": 4, "kind": "join", "status": "awaiting_payment",
"email": "pat@example.org", "first_name": "Pat", "last_name": "Lee", "message": null, "member_id": 812,
"membership_id": 95, "level_id": 3, "amount_cents": 6500, "interval": null, "auto_renew": true, "mailing_list": false,
"answers": [{"id": "q1a2b3c4d", "label": "How did you hear about us?", "type": "choice", "value": "A friend"}],
"outcome": "Waiting for payment: $65.00 a year, renewing automatically", "checkout_id": 1188, "payment_id": null,
"via": "api", "created": "2026-10-22T18:30:00Z",
"next": {"action": "pay", "url": "https://checkout.stripe.com/…", "message": "Send them to the payment page ($65.00 a year). …"}}
{"id": 1188, "object": "checkout", "purpose": "membership", "status": "open", "url": "https://checkout.stripe.com/…",
"amount_cents": 6500, "currency": "USD", "interval": "year", "description": "Professional membership – Your Club",
"processor": "stripe", "member_id": 812, "membership_id": 95, "action": "join", "payment_id": null, "created": "…"}
A submission's status is unconfirmed (waiting for the emailed link), received, awaiting_payment, paid, failed (the payment page expired unpaid) or refunded; via is form or api. next comes only in the answer to POST /forms/{id}/submissions. A donation checkout has donation_id instead of membership_id and action. Test keys answer with a stand-in url: no payment page is made.
Webhooks
Add an endpoint in the dashboard (or with POST /webhooks) and choose its events. We send each one as a JSON POST. Answer with any 2xx within 10 seconds; otherwise we try again after a minute, 5 minutes, half an hour, 2, 5 and 12 hours, then daily, for 3 days. The dashboard shows every delivery, its answer, and a button to send it again, and can send a test (ping).
| Event | When | data.object |
|---|---|---|
member.created | Someone was added to People (sign-in, dashboard, API, form or import) | member |
member.updated | A person's details, tags or level changed, or two records were merged | member |
membership.started | A membership began (given, comped, paid or at the door) | membership |
membership.renewed | A membership was renewed (paid, automatic or by hand) | membership |
membership.lapsed | A membership ran out after its grace period | membership |
membership.cancelled | A membership was cancelled | membership |
payment.succeeded | A payment went through | payment |
payment.failed | A payment failed (such as an auto-renewal the card declined) | payment |
payment.refunded | All or part of a payment was refunded | payment |
registration.created | Someone registered for an event | registration |
signin.created | Someone signed in at an event | signin |
form.submitted | Someone sent one of the club's forms (on the website or through the API) | form_submission |
POST /your/endpoint
PTP-Event: membership.renewed
PTP-Event-Id: evt_5c0d…
PTP-Signature: t=1792500000,v1=9f86d081884c7d65…
{"id": "evt_5c0d…", "object": "event", "type": "membership.renewed", "created": "2026-10-19T18:00:00Z",
"livemode": true, "data": {"object": {"id": 95, "object": "membership", …}}}
The same event can arrive more than once (a retry after your server answered too slowly): use its id to do things once. Events can arrive out of order; fetch the object from the API when the latest state matters.
Checking the signature
Each endpoint has its own secret (whsec_…), shown once when it's made. PTP-Signature holds the time t and v1, the hex HMAC-SHA256 of <t>.<raw body> with that secret. Check it against the raw body, before parsing it, and refuse anything older than 5 minutes.
// PHP
$header = $_SERVER['HTTP_PTP_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
preg_match('/t=(\d+)/', $header, $t);
preg_match('/v1=([0-9a-f]{64})/', $header, $v1);
$expected = hash_hmac('sha256', $t[1] . '.' . $body, $secret);
if (!$t || !$v1 || abs(time() - (int)$t[1]) > 300 || !hash_equals($expected, $v1[1])) {
http_response_code(400); exit;
}
// Node.js
const crypto = require("crypto");
const [, t] = header.match(/t=(\d+)/) || [];
const [, v1] = header.match(/v1=([0-9a-f]{64})/) || [];
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = t && v1 && Math.abs(Date.now() / 1000 - t) < 300 &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
Signing members in
Members sign in on Pass the Pad's own page (with their password or an emailed link), never on your site, so their passwords never pass through your website. Your site then gets a token that says who they are:
- Send them to sign in. Link to
https://demo.passthepad.com/p/<your-club>/sso?return=<a page on your site>, or make a one-time link withPOST /members/{id}/login-link(for example from your own "Members area" button, or emailed withsend_email). - They come back with a token in the address (
?ptp_token=…). The members-only script from Membership → Members-only pages picks it up and shows or hides yourdata-ptp-members-onlycontent by itself. - Check it on your server before showing anything private:
POST /sessions/verifywith the token and your site's origin asaudience. It also says their current status and level, so a member who lapsed since signing in is refused. - Keep them signed in with
POST /sessions/refreshbefore the token expires (the club sets how long tokens last, 15 minutes to a day). - Sign them out everywhere with
DELETE /members/{id}/sessions, for example when they report a lost phone, or when you remove someone.
To check whether someone is a member without signing them in, use GET /members?email=… (their status, level and expires_on), and hear about changes from the membership.* webhooks.
Styling an embedded form
Each form has its own design, set in the form builder (Membership → Forms → a form → Design), and GET /forms/{id} returns it as design. Where a form is embedded on your website with the snippet from the builder, your own stylesheet can override that design: set any of these custom properties on the iframe, and the form uses them. Only these values are read, and each one is checked (a color, a size, a font name, one of the choices), so nothing else from your page reaches the form.
iframe[data-ptp-form] {
--ptp-brand: #7a3b69; /* buttons and highlights */
--ptp-on-brand: #ffffff; /* words on the buttons (worked out from --ptp-brand if left out) */
--ptp-text: #2b2230; /* text */
--ptp-muted: #6b5e70; /* hints */
--ptp-bg: transparent; /* behind the form */
--ptp-surface: #ffffff; /* the form and its fields */
--ptp-line: #e7dccd; /* borders */
--ptp-radius: 18px; /* corners, 0 to 40px */
--ptp-font: "Lora", Georgia, serif;
--ptp-heading-font: "Fraunces", Georgia, serif;
--ptp-button: pill; /* filled, outline or pill */
--ptp-layout: flat; /* card, or flat to blend into your page */
}
The same settings work as attributes on the iframe, like data-ptp-brand="#7a3b69". Fonts from the builder's list load by themselves; any other font must be one people's computers already have. To build your own form instead, use POST /forms/{id}/submissions or POST /checkouts.
Use it with an AI agent
AI assistants like Claude and ChatGPT can use the API for you: follow up on renewals, find regulars who haven't joined, add a list of people, or write a webhook receiver. Give the agent a key with only the permissions the job needs, start it on a test key (ptp_test_…, where changes are checked and then rolled back), and read what it plans before letting it change anything.
Agents read a short plain-text guide to the API at https://demo.passthepad.com/developers/agents.txt. Paste one of these prompts, put your key in place of ptp_live_YOUR_KEY, and add your own details. To try them without a club of your own, use the live demo, where they come with a demo key filled in.
Renewal follow-upFinds memberships ending soon and drafts a personal note to each person, without sending anything.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). Find every member whose membership is ending in the next 30 days (GET /members?status=expiring). For each person, look up their membership level and how often they have come to events lately (GET /signins?member_id=). Then give me a table: name, level, end date, visits in the last 90 days. Below it, draft a short, warm renewal note for each person that mentions something true from their attendance. Don't change anything and don't send anything: drafts only.
Regulars who aren't members yetLooks at who keeps coming to events and suggests who to invite to join.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). Look at sign-ins from the last three months (GET /signins, passing each page's next_cursor as starting_after until has_more is false). Find people who signed in three or more times but have no current membership (check GET /members?email= or GET /memberships?member_id=). List them with how many times they came and which events, most frequent first, and suggest a friendly one-line invitation to join for each. Read only: don't change anything.
Add members from a spreadsheetAdds a list of people safely: a dry run first, then for real, never twice.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). I'll paste a list of people (name and email) below. Add each one to the club with POST /members. First do a dry run with a test key (ptp_test_...) if I give you one, and show me what would happen. When I say go, do it for real, sending an Idempotency-Key header made from the email address so a retry never adds anyone twice. Report who was added, who was already there, and any errors with the API's message. [paste the list here]
Tonight's event reportSummarises an event: who registered, who came, members and guests.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>").
Find tonight's event (GET /events), then get its registrations (GET /events/{id}/registrations) and its sign-ins (GET /signins?event_id=). Tell me: how many registered, how many came, how many were members and how many guests, and who registered but didn't come. Finish with three short bullet points I could share with the club's board. Read only.
Build a webhook receiverWrites a small server endpoint that checks signatures and handles each event once.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). Write a small web endpoint (in the language I name below; default Node.js with no frameworks) that receives Pass the Pad webhooks. It must: read the raw body; verify the PTP-Signature header (t=<time>,v1=<HMAC-SHA256 of "t.body" with my signing secret>) using a constant-time comparison and reject anything older than 5 minutes; answer 200 quickly; and skip any PTP-Event-Id it has already handled. For membership.started and membership.renewed, add or update the person in a CSV file; for membership.lapsed, mark them as lapsed. Include instructions for registering the endpoint with POST /webhooks and for testing it. Language: [Node.js]
Your own join formBuilds a small join page on the club's own website that takes payment through Pass the Pad, with the key kept on the server.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>").
Build a small join page for my club's own website, in the language I name below (default: PHP with no framework). It must call the API only from the server: the key never reaches the browser. The page lists the membership levels the club's join form offers (GET /forms, the form with kind "join"; or GET /levels for every level) with their prices, and asks for first name, last name, email and a level, plus the form's own questions. When it's sent, the server calls POST /forms/{id}/submissions (or POST /checkouts with purpose "membership") with success_url and cancel_url set to a return page on my site, such as https://example.org/joined?checkout={CHECKOUT_ID}, and an Idempotency-Key so a double click is one join. Then follow the answer: for next.action "pay" redirect to next.url; for "done" or "already_member" show next.message; for a 422, show error.errors next to each field. The return page reads the checkout id from its address, calls GET /checkouts/{id} on the server and says "Welcome, you're a member" when status is "paid", or offers to try again when it's still "open". Test it with a test key (ptp_test_...) first.
Language: [PHP]
A members-only area on your websiteBuilds sign-in for members on the club's own site: a sign-in button, a server-side token check and staying signed in.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). Add a members-only area to my club's website (tell me what my site is built with; default: a small Node.js server). It must: send visitors who aren't signed in to the club's sign-in page (https://demo.passthepad.com/p/<club>/sso?return=<the members page>), which brings them back with ?ptp_token=...; check every token on the server with POST /sessions/verify, passing my site's origin as audience, and only then show members-only pages; keep members signed in by calling POST /sessions/refresh before the token expires; and show a friendly message for each verify error (expired, signed_out, not_a_member). Keep the API key on the server only, never in the browser. Explain how to test it with a test key first.
Monthly dues summaryTotals the month's payments by purpose and spots failed renewals.
Use the Pass the Pad API for my club. The API guide is at https://demo.passthepad.com/developers/agents.txt: read it first. My API key is ptp_live_YOUR_KEY (send it as "Authorization: Bearer <key>"). Get all payments from the last calendar month (GET /payments, following next_cursor until has_more is false). Give me totals by purpose (dues, tickets, store, donations), the number of payments, any refunds, and a list of failed payments with the member's name so I can follow up. Note any test payments separately; they aren't real money. Read only.