Recspert Partner Integration Specification

Audience: Provider integration engineers building API endpoints for Recspert to consume.

Purpose: Documents the API surface Recspert needs from a partner provider system in order to integrate inventory, registrations, and transaction lifecycle.

Status: v1.1 — informed by Recspert's existing partner integrations as of 2026-05-20. Optional capabilities are marked.


Table of Contents

  1. Integration Model Overview
  2. Authentication
  3. Conventions
  4. Catalog APIs (Inventory)
  5. User & Member APIs
  6. Pricing & Checkout APIs
  7. Registration APIs (C & P)
  8. Package APIs (K)
  9. Membership APIs
  10. Roster & Reporting
  11. Transaction Lifecycle & Source Tracking
  12. Webhooks (Optional but Strongly Preferred)
  13. Error Handling
  14. Rate Limits & SLAs
  15. Sandbox / Test Environment
  16. Onboarding Checklist
  17. Appendix A — End-to-End Flow Walkthroughs
  18. Appendix B — Field Reference Quick-Lookup
  19. Appendix C — Glossary

1. Integration Model Overview

Recspert is a discovery and registration aggregator. We pull a provider's full activity inventory into our search index, render it on web and mobile, and pass user registrations back to the provider's source-of-truth system in real time. The provider remains the system of record for activities, registrations, packages, and payment.

We classify all activity inventory into three entity types:

Type Code Description Examples
Class C A single-occurrence drop-in instance, usually group exercise Yoga at 6am Tuesday
Program P A multi-session enrollment with a defined start/end date 6-week swim lessons, summer camp week
Package K A pre-purchased bundle of redemptions used to attend classes or schedule individual appointments 10-class pass, 5-pack of personal training

A complete integration supports all three. Partial integrations (e.g. C+P only, no K) are viable but limit user value.

Integration phases

Phase What works What's missing
Catalog-only (read) Discovery, search, share No registration on Recspert — users deep-link out
Registration (read + write C/P) Discovery + in-app class/program registration No package purchase, no Wallet
Packages (read + write C/P/K) Full activity + package commerce Optional advanced features (reservations, audit)
Advanced Reservations, capacity, membership sales

This spec describes the Packages tier. Catalog-only is a valid subset that requires only Section 4.


2. Authentication

Recspert authenticates to your APIs server-to-server.

Required: OAuth 2.0 client credentials grant

POST {auth_base}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=...
&client_secret=...
&scope=catalog.read registrations.write packages.write users.write

Response:

{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600
}

All subsequent API calls carry Authorization: Bearer {access_token}.

Scopes Recspert will request

Scope Purpose
catalog.read List clubs, locations, categories, programs, classes, packages
users.read Look up users for linkage and payment-method listing
users.write Create users (guest + member)
registrations.read Query registration status, roster
registrations.write Create, cancel registrations
packages.read List a user's owned packages
packages.write Buy packages on behalf of a user
payment.read List payment methods on file
payment.write (Optional) Add new payment methods
memberships.write (Optional) Sell memberships

Token lifecycle expectations

Alternative: API key

Acceptable for catalog-read-only integrations, not for write paths.


3. Conventions

IDs

Dates & times

Currency

Pagination

Locale


4. Catalog APIs (Inventory)

These endpoints feed Recspert's search index. Recspert polls them on a schedule and caches results (cadence configurable per partner; webhook-driven sync per Section 12 is preferred where supported).

4.1 List clubs

GET /clubs

A "club" is a top-level partner organization (e.g. a Y branch, a fitness studio chain, a campus rec department). One credential set may grant access to one or many clubs.

Response:

{
  "clubs": [
    {
      "id": "12345",
      "name": "Cleveland State University Recreation Center",
      "shortName": "CSU Rec",
      "websiteUrl": "https://www.csurec.com",
      "imageUrl": "https://...",
      "phone": "+1-216-555-0100",
      "email": "rec@csu.edu",
      "address": {
        "street1": "2420 Chester Ave",
        "city": "Cleveland",
        "state": "OH",
        "zip": "44115",
        "country": "US"
      },
      "active": true
    }
  ]
}

4.2 List locations / facilities

GET /clubs/{clubId}/locations

Locations are the physical facilities within a club where activities happen. A program lists one location; a class lists one location and timezone.

Response:

{
  "locations": [
    {
      "id": "loc-456",
      "clubId": "12345",
      "name": "Main Pool",
      "address": { /* same shape as club */ },
      "geo": { "lat": 41.5017, "lng": -81.6792 },
      "timezone": "America/New_York",
      "imageUrl": "https://..."
    }
  ]
}

4.3 List categories

GET /clubs/{clubId}/categories

Categories are how partners group activities (Aquatics, Group Fitness, Personal Training, Youth Sports, etc.). Recspert uses these for filter chips.

Response:

{
  "categories": [
    { "id": "cat-1", "name": "Aquatics", "parentId": null },
    { "id": "cat-2", "name": "Swim Lessons", "parentId": "cat-1" }
  ]
}

4.4 List classes (entityType C)

GET /clubs/{clubId}/classes?from=2026-05-19&to=2026-08-19

Returns scheduled occurrences — one row per class instance. Recurring classes expand into N rows. Recspert calls this with a forward-looking date window.

Response (per class instance):

{
  "classes": [
    {
      "id": "class-789",
      "clubId": "12345",
      "name": "Sunrise Yoga",
      "description": "All-levels yoga to start your day.",
      "categoryIds": ["cat-3"],
      "locationId": "loc-456",
      "instructors": [
        { "id": "instr-1", "name": "Jane Doe", "imageUrl": "https://..." }
      ],
      "startTimestamp": "2026-05-20T10:00:00Z",
      "endTimestamp": "2026-05-20T11:00:00Z",
      "startTimeOfDay": "06:00",
      "endTimeOfDay": "07:00",
      "capacity": 20,
      "availableSpots": 14,
      "status": "open",
      "registrationOpensAt": "2026-05-13T04:00:00Z",
      "registrationClosesAt": "2026-05-20T09:45:00Z",
      "pricing": {
        "memberFee": "0.00",
        "nonMemberFee": "15.00",
        "currency": "USD"
      },
      "imageUrl": "https://...",
      "tags": ["all-levels", "morning"]
    }
  ],
  "nextCursor": null
}

Required fields: id, clubId, name, locationId, startTimestamp, endTimestamp, status, pricing.

4.5 List programs (entityType P)

GET /clubs/{clubId}/programs?from=2026-05-19&to=2026-12-31

Programs are multi-session enrollments. Recspert ingests the program-level record plus its child sessions.

Response:

{
  "programs": [
    {
      "id": "prog-321",
      "clubId": "12345",
      "name": "Learn-to-Swim Level 2 (Tues/Thurs)",
      "description": "...",
      "categoryIds": ["cat-2"],
      "locationId": "loc-456",
      "instructors": [ /* same shape as class */ ],
      "startDate": "2026-06-02",
      "endDate": "2026-07-09",
      "registrationOpensAt": "2026-05-01T04:00:00Z",
      "registrationClosesAt": "2026-06-01T23:59:00Z",
      "pricing": { /* same shape as class */ },
      "capacity": 12,
      "availableSpots": 8,
      "sessions": [
        {
          "id": "sess-1",
          "startTimestamp": "2026-06-02T22:00:00Z",
          "endTimestamp": "2026-06-02T22:45:00Z"
        }
      ],
      "ageRange": { "min": 6, "max": 8 },
      "tags": ["youth", "swim"]
    }
  ]
}

4.6 List packages (entityType K)

GET /clubs/{clubId}/packages

Packages are bundles of redemptions a user pre-buys. They link to one or more activities (Group Ex) or are appointment-based (PT, semi-private).

Response:

{
  "packages": [
    {
      "id": "pkg-654",
      "clubId": "12345",
      "name": "10-Class Group Ex Pass",
      "description": "...",
      "packageType": 1,
      "packageTypeName": "Group Exercise",
      "rates": [
        {
          "id": "rate-1",
          "sessionCount": 1,
          "price": "25.00",
          "currency": "USD"
        },
        {
          "id": "rate-2",
          "sessionCount": 10,
          "price": "200.00",
          "currency": "USD"
        }
      ],
      "memberPrice": "200.00",
      "nonMemberPrice": "250.00",
      "currency": "USD",
      "canExpire": true,
      "expirationPolicy": "12 months after purchase",
      "includedActivities": [
        {
          "seasonName": "Season 2026",
          "activities": ["Sunrise Yoga", "HIIT Bootcamp", "Spin Class"]
        }
      ],
      "imageUrl": "https://..."
    }
  ]
}

packageType values:

includedActivities is required for packageType=1 so users can confirm coverage before purchase.

4.7 List instructors

GET /clubs/{clubId}/instructors

Recspert renders instructor cards on program/class detail pages.

Response:

{
  "instructors": [
    {
      "id": "instr-1",
      "name": "Jane Doe",
      "bio": "...",
      "imageUrl": "https://...",
      "certifications": ["RYT-200", "CPR"]
    }
  ]
}

4.8 Get program details (optional, for deeper enrichment)

GET /clubs/{clubId}/programs/{programId}

Returns the full program record including waiver content, custom field definitions, prerequisites.


5. User & Member APIs

Recspert maintains its own user accounts. When a Recspert user registers at your club for the first time, we create a corresponding user record on your system and store the partnerUserId linkage on our member record.

5.1 Search for an existing user

POST /clubs/{clubId}/users/search

{
  "email": "user@example.com",
  "firstName": "Sam",
  "lastName": "Smith",
  "dateOfBirth": "1990-04-15"
}

Recspert calls this before creating a new user, to avoid duplicates when a Recspert user already has an account at the club from a prior in-person visit.

Field semantics: email is the primary lookup. firstName, lastName, and dateOfBirth are optional search criteria sent when available. Recspert does not collect dateOfBirth from all users (it is not required at Recspert account creation), so the search endpoint must support email-only matches as well as combined-criteria matches. Partners should not reject the search request when only email is provided.

Response:

{
  "users": [
    {
      "id": "user-9999",
      "clubId": "12345",
      "email": "user@example.com",
      "firstName": "Sam",
      "lastName": "Smith",
      "isMember": true,
      "membershipExpiresAt": "2026-12-31",
      "dependents": [
        { "id": "user-9998", "firstName": "Kid", "relationship": "child" }
      ]
    }
  ]
}

5.2 Create a guest user (no membership)

POST /clubs/{clubId}/users

{
  "email": "user@example.com",
  "firstName": "Sam",
  "lastName": "Smith",
  "dateOfBirth": "1990-04-15",
  "phone": "+1-555-555-5555",
  "address": { /* standard address */ },
  "emergencyContact": { "name": "...", "phone": "..." }
}

Required fields: email, firstName, lastName.

Optional fields: dateOfBirth, phone, address, emergencyContact. Recspert sends these when the user has provided them, but does not require them at Recspert signup. Partners must accept user-creation requests when these fields are absent. Partners may legitimately reject downstream actions that depend on the missing data (e.g., registration for an age-gated program where dateOfBirth is required) at the checkout/preview step — see Section 6 — but the user record itself must be creatable without them.

Response:

{ "id": "user-9999", "created": true }

5.3 Add a dependent

POST /clubs/{clubId}/users/{userId}/dependents
{ "firstName": "Kid", "lastName": "Smith", "dateOfBirth": "2018-03-01", "relationship": "child" }

Field semantics: Same optionality as Section 5.2 — firstName and lastName required; dateOfBirth and relationship recommended but optional. Partners may require dateOfBirth at registration time for dependent eligibility checks (e.g., age-gated youth programs).

5.4 List payment methods on file

GET /clubs/{clubId}/users/{userId}/payment-methods

Response:

{
  "paymentMethods": [
    {
      "id": "pm-111",
      "type": "card",
      "brand": "visa",
      "last4": "4242",
      "expMonth": 12,
      "expYear": 2028,
      "isDefault": true
    }
  ]
}

5.5 Add a payment method (optional)

POST /clubs/{clubId}/users/{userId}/payment-methods

{ /* PCI-safe tokenized card representation from your payment processor */ }

If you don't expose this, Recspert can only use cards added by the user directly on your portal.


6. Pricing & Checkout APIs

The preview / checkout endpoint is the single most important write-path endpoint. It validates a proposed registration before money moves and returns a confirmed line-item breakdown.

6.1 Preview a class registration

POST /clubs/{clubId}/classes/{classId}/checkout

{
  "classDate": "2026-05-20",
  "userIds": ["user-9999", "user-9998"]
}

Response:

{
  "checkout": {
    "registrants": [
      {
        "userId": "user-9999",
        "name": "Sam Smith",
        "isMember": true,
        "price": "0.00",
        "discounts": [],
        "fees": [],
        "waiversRequired": ["waiver-abc"],
        "customFieldsRequired": ["cf-1"],
        "eligible": true
      }
    ],
    "subtotal": "0.00",
    "tax": "0.00",
    "total": "0.00",
    "currency": "USD",
    "requiredPayment": false,
    "warnings": []
  }
}

Critical fields:

6.2 Preview a program registration

POST /clubs/{clubId}/programs/{programId}/checkout
{ "startDate": "2026-06-02", "userIds": [...] }

Same shape. Program previews include all sessions in scope.

6.3 Preview a package purchase

POST /clubs/{clubId}/packages/{packageId}/checkout

{
  "userId": "user-9999",
  "rateId": "rate-2"
}

Response:

{
  "checkout": {
    "packageId": "pkg-654",
    "packageName": "10-Class Group Ex Pass",
    "rateId": "rate-2",
    "sessionCount": 10,
    "price": "200.00",
    "tax": "0.00",
    "total": "200.00",
    "currency": "USD"
  }
}

7. Registration APIs (C & P)

7.1 Create a class registration

POST /clubs/{clubId}/classes/{classId}/registrations

{
  "classDate": "2026-05-20",
  "userIds": ["user-9999"],
  "paymentMethodId": "pm-111",
  "waiverSignatures": [
    { "waiverId": "waiver-abc", "signedAt": "2026-05-19T18:00:00Z", "signatureBase64": "..." }
  ],
  "customFieldValues": [
    { "fieldId": "cf-1", "value": "Vegetarian" }
  ]
}

Response:

{
  "registration": {
    "id": "reg-555",
    "classId": "class-789",
    "classDate": "2026-05-20",
    "status": "confirmed",
    "participantList": [
      { "userId": "user-9999", "name": "Sam Smith", "status": "confirmed" }
    ],
    "total": "0.00",
    "paymentMethodId": "pm-111",
    "createdAt": "2026-05-19T18:00:00Z"
  }
}

Failure modes:

7.2 Create a program registration

POST /clubs/{clubId}/programs/{programId}/registrations
{ /* same shape; startDate instead of classDate */ }

7.3 Cancel a registration

DELETE /clubs/{clubId}/registrations/{registrationId}

7.4 Get user's upcoming registrations

GET /clubs/{clubId}/users/{userId}/registrations?from=2026-05-19

8. Package APIs (K)

8.1 Buy a package

POST /clubs/{clubId}/users/{userId}/packages

{
  "packageId": "pkg-654",
  "rateId": "rate-2",
  "startDate": "2026-05-19",
  "paymentMethodId": "pm-111"
}

Critical: Reject the request with 422 if paymentMethodId is missing. At least one existing partner API exhibits the failure mode of accepting package buys without a payment method and silently creating an unpaid house-charge against the user's account. This has caused real operational issues. Do not replicate this behavior.

Response:

{
  "packageInstance": {
    "id": "pkginst-777",
    "packageId": "pkg-654",
    "userId": "user-9999",
    "sessionsTotal": 10,
    "sessionsRemaining": 10,
    "purchasedAt": "2026-05-19T18:00:00Z",
    "expiresAt": "2027-05-19",
    "total": "200.00",
    "paymentMethodId": "pm-111",
    "status": "active"
  }
}

8.2 List a user's packages

GET /clubs/{clubId}/users/{userId}/packages

Response:

{
  "packages": [
    {
      "id": "pkginst-777",
      "packageId": "pkg-654",
      "name": "10-Class Group Ex Pass",
      "sessionsTotal": 10,
      "sessionsRemaining": 7,
      "purchasedAt": "2026-05-19T18:00:00Z",
      "expiresAt": "2027-05-19",
      "status": "active",
      "redemptions": [
        { "registrationId": "reg-555", "redeemedAt": "2026-05-20T10:00:00Z" }
      ]
    }
  ]
}

8.3 Apply a package to a class registration

Two acceptable patterns; pick one and document it:

Pattern A (preferred): Auto-apply. When a class registration is created and the user owns an eligible package, the package is automatically debited. Surface this in the registration response (packageApplied: { packageInstanceId, sessionsDebited }).

Pattern B: Explicit. Recspert sends redemption: { packageInstanceId } in the registration body.

Existing partner integrations use Pattern A (with an opt-in keyword on the registration request to redeem a package). Pattern A is simpler to integrate and is preferred.


9. Membership APIs (Optional)

Lets Recspert sell memberships at signup, not just registrations. Skip if memberships are sold only in person.

GET /clubs/{clubId}/membership-groups
POST /clubs/{clubId}/users/{userId}/memberships

Full payload shape covers agreements, signature collection, and dependent-included pricing. Recspert can share its reference implementation against existing partner APIs during integration onboarding.


10. Roster & Reporting

10.1 Class / program roster

GET /clubs/{clubId}/classes/{classId}/roster?date=2026-05-20
GET /clubs/{clubId}/programs/{programId}/roster

Returns participants by user ID. Recspert uses this to render "Who's coming" features and check-in tools.

10.2 Provider stats / audit (optional)

GET /clubs/{clubId}/reports/registrations?from=...&to=...&source=recspert

Lets either party reconcile Recspert-facilitated registrations against the partner's records. Useful for partnership reporting and dispute resolution. Requirement level depends on the commercial model agreed between Recspert and the partner (see Section 11).


11. Transaction Lifecycle & Source Tracking

This section covers two related but distinct capabilities: transaction lifecycle visibility (required for any write integration) and source tracking (recommended; specific contractual requirement depends on the commercial model). Recspert supports multiple commercial models and the integration spec is intentionally model-agnostic.

11.1 Transaction Lifecycle Visibility — REQUIRED

Required for any partner integration that includes registration or package writes. Recspert needs to know, with reasonable confidence and timeliness, the lifecycle state of every transaction it facilitates.

11.1.1 Synchronous confirmation on write

Every write endpoint (registration create, package buy, cancellation) must return a synchronous response confirming the operation's outcome and returning the canonical record. Recspert does not commit downstream actions (charging a service fee, issuing a Wallet pass, sending a confirmation email) until this confirmation is received. See Section 7 and Section 8 for required response shapes.

11.1.2 Cancellation / refund notification

Recspert must be able to learn that a previously confirmed registration has been cancelled or refunded. Acceptable mechanisms:

Required to enable Recspert to issue corresponding refunds, remove items from My Schedule, and update Wallet passes.

11.1.3 Registration lookup by reference

Recspert must be able to retrieve a registration record by its partner-issued ID or by a Recspert-supplied reference. Required for user-support escalations, dispute handling (where Recspert needs to evidence to a payment processor that a registration occurred), and reconciliation.

GET /clubs/{clubId}/registrations/{registrationId}

Recspert supports multiple commercial models with its partners:

Source tracking is contractually required only when the commercial model depends on it (revenue share, hybrid arrangements with revenue-share components). Under a pure platform-fee model, source tracking is strongly recommended but not contractually required, because Recspert's fee is collected at point-of-transaction and does not depend on partner-side reconciliation.

Independent of the commercial model, source tracking provides operational value to both parties:

POST /clubs/{clubId}/classes/{classId}/registrations
{
  /* ... */
  "source": "recspert",
  "sourceMetadata": {
    "recspertMemberId": "rsm-12345",
    "recspertReferralCode": "..."
  }
}

The source should appear on:

GET /clubs/{clubId}/reports/registrations?source=recspert

Enables independent reconciliation by either party.

11.2.4 Optional: referral identifier per Recspert user

If the partner system can store a recspertMemberId on the user record (in addition to per-registration), even better — it preserves attribution across the user's lifetime at the partner, not just per-transaction.

11.3 Commercial Model Decision

The choice between platform-fee, revenue-share, or hybrid is made in the commercial agreement between Recspert and the partner, not in this integration spec. If the agreed model is revenue-share or hybrid, all 11.2 items become required rather than recommended, and the report endpoint in Section 10 is the contractual reconciliation surface.


12. Webhooks (Optional but Strongly Preferred)

Polling for catalog changes is wasteful and laggy. Webhooks let us stay in sync within seconds rather than hours.

Events Recspert subscribes to

Event Trigger Payload
class.created New class instance added Full class object
class.updated Capacity, time, instructor, or status change Full class object
class.cancelled Class cancelled { classId, classDate, reason }
program.created / .updated / .cancelled Same for programs Full program object
package.created / .updated Catalog package changes Full package object
registration.created New registration (any source) Registration record incl. source
registration.cancelled Cancellation { registrationId, reason }
user.updated Membership status change User record

Delivery requirements


13. Error Handling

Status codes

Code Meaning
200 Success, body has data
201 Created (on POST)
400 Malformed request (Recspert bug — log it)
401 Auth failed (Recspert will refresh token + retry once)
403 Auth OK but not authorized for this resource
404 Entity not found
409 Conflict (class full, duplicate registration, etc.)
422 Validation error (missing waiver, bad date, etc.)
429 Rate limited (see Section 14)
5xx Server error (Recspert will retry idempotent reads)

Error body shape

{
  "error": {
    "code": "CLASS_FULL",
    "message": "Class is at capacity",
    "details": { "classId": "class-789", "capacity": 20, "registered": 20 },
    "userMessage": "Sorry, this class just filled up. Try a later date."
  }
}

14. Rate Limits & SLAs

Expected request volume from Recspert

429 behavior

Return:

HTTP/1.1 429 Too Many Requests
Retry-After: 30

Recspert respects Retry-After and backs off.

SLA expectations

Catalog endpoints under heavier rate limits are tolerable. Write endpoints (registrations, package buys) under heavy throttling translate directly to checkout failures Recspert users see.


15. Sandbox / Test Environment

Required. Recspert cannot integrate against production for QA.

Sandbox requirements

Acceptable equivalents

15.2 Recspert Sandbox

Recspert provides a development environment for partner integration testing.

Sandbox endpoints:

Resource URL
Web app https://dev.recspert.com
API base https://dev-apiv2.recspertservices.com

Sandbox access is provisioned manually during onboarding. During the first 2–4 weeks of an integration, expect coordination with the Recspert team for the following:

Item How provided
Partner API key (for inbound catalog/registration sync) Issued per integration, scoped to your test club
Test club ID + seeded test users Created on request before integration kickoff
Test card numbers Stripe test card list provided; standard 4242 4242 4242 4242 always succeeds
Data reset / fixture re-seed Support request to sgill@recspert.com — 1 business day turnaround
Webhook delivery target for outbound events Recspert-supplied URL specific to your integration
Rate limit headroom Dedicated bucket so your load testing does not impact other dev users

Self-service sandbox tooling — partner portal for credential management, ingestion log viewer, automated nightly resets, synthetic fixture seeding — is on the roadmap but not yet shipped. Plan on direct support contact during your initial integration.

What the Recspert sandbox does NOT provide:

Production cutover happens only after Section 16's onboarding checklist is complete. Production credentials are issued separately and never shared via the same channel as sandbox credentials.


16. Onboarding Checklist

Hand this back to Recspert when each item is done.

Phase 1 — Read

Phase 2 — User & Checkout

Phase 3 — Write

Phase 4 — Attribution & Webhooks

Phase 5 — Production


Appendix A — End-to-End Flow Walkthroughs

The flows below show the exact sequence of calls Recspert makes against a partner API during the most common end-user actions. Use these as integration test scenarios — if all four flows pass, you have a working integration.

A.1 Catalog ingestion (Recspert pulls partner inventory)

Runs on a schedule per partner (cadence configurable; webhook-driven sync per Section 12 reduces or eliminates polling). One sync per club.

sequenceDiagram participant R as Recspert (ingestion) participant P as Partner API R->>P: POST /oauth/token (client_credentials) P-->>R: { access_token, expires_in: 3600 } R->>P: GET /clubs P-->>R: clubs[] loop For each club R->>P: GET /clubs/{clubId}/locations P-->>R: locations[] R->>P: GET /clubs/{clubId}/categories P-->>R: categories[] R->>P: GET /clubs/{clubId}/instructors P-->>R: instructors[] R->>P: GET /clubs/{clubId}/classes?from=...&to=... P-->>R: classes[] R->>P: GET /clubs/{clubId}/programs?from=...&to=... P-->>R: programs[] (incl. sessions) R->>P: GET /clubs/{clubId}/packages P-->>R: packages[] end Note over R: Index into search;<br/>de-duplicate against prior sync

Failure handling:

A.2 Class registration (user clicks Register)

End-to-end latency target: under 5 seconds wall-clock.

sequenceDiagram participant U as User (browser) participant R as Recspert API participant P as Partner API U->>R: POST /registrations/preview { classId, classDate, userIds } R->>P: POST /clubs/{clubId}/users/search { email, dob, lastName } P-->>R: users[] (existing or empty) alt User does not exist at partner R->>P: POST /clubs/{clubId}/users { full profile } P-->>R: { id: "user-9999" } end R->>P: POST /clubs/{clubId}/classes/{classId}/checkout { classDate, userIds } P-->>R: checkout { eligible, total, waivers, customFields } R-->>U: preview { total, waivers, customFields, requiredPayment } Note over U: User signs waivers,<br/>fills custom fields,<br/>selects payment method U->>R: POST /registrations { ...selections, paymentMethodId } R->>P: POST /clubs/{clubId}/classes/{classId}/registrations { full body, source: "recspert" } P-->>R: registration { id, status: "confirmed", participantList } R->>R: Write memberTransaction row R-->>U: success { registrationId, participantList }

Critical failure cases:

Case Partner response Recspert behavior
Class fills between preview and registration 409 CLASS_FULL Surface partner's userMessage; suggest alternate sessions
Payment declined 402 PAYMENT_DECLINED with decline reason Surface decline reason; user picks different card
Waiver missing 422 WAIVER_REQUIRED with waiverId Re-fetch waiver, prompt user to sign, retry
Partner timeout (> 10s) (no response) Recspert aborts and shows "Provider unavailable, try again." NEVER retries a write without user confirmation.

A.3 Package purchase (user clicks Buy Package)

sequenceDiagram participant U as User (browser) participant R as Recspert API participant S as Stripe participant P as Partner API U->>R: GET /packages/preview/{packageId}?rateId=X R->>P: POST /clubs/{clubId}/packages/{packageId}/checkout { userId, rateId } P-->>R: checkout { price, total } R->>R: Compute Recspert platform fee (if provider opted in) R-->>U: preview { price, platformFee, total } opt Platform fee applies U->>R: POST /platform-fee/setup { amountCents, programName } R->>S: paymentIntents.create() S-->>R: { clientSecret, paymentIntentId } R-->>U: { clientSecret, existingCard } U->>S: confirm payment (Stripe Elements) S-->>U: { paymentIntent.id, status: succeeded } end U->>R: POST /packages/buy { userId, rateId, paymentMethodId, platformFeePaymentIntentId? } Note over R: Backend REJECTS if paymentMethodId missing (422) R->>P: POST /clubs/{clubId}/users/{userId}/packages { packageId, rateId, startDate, paymentMethodId, source: "recspert" } P-->>R: packageInstance { id, sessionsRemaining, expiresAt } R->>R: Write memberTransaction (links Stripe intent to partner purchase) R-->>U: success { packageInstanceId }

Critical: Partner MUST reject the POST /packages call with 422 if paymentMethodId is missing. See Section 8.1 for the rationale — a known partner failure mode in this scenario silently creates unpaid house-charges that surface as operational and user-trust problems downstream.

A.4 Webhook delivery (partner notifies Recspert of catalog change)

sequenceDiagram participant P as Partner system participant R as Recspert webhook receiver participant DB as Recspert DB P->>P: Class capacity changes (e.g. someone cancels) P->>R: POST /webhooks/{partnerId} { event: "class.updated", payload, idempotencyKey } Note right of P: X-Webhook-Signature: sha256={hmac of body} R->>R: Verify HMAC signature with shared secret alt Invalid signature R-->>P: 401 Unauthorized end R->>R: Check idempotencyKey against processed-events log alt Already processed R-->>P: 200 OK (no-op) end R->>DB: Update class record R->>R: Invalidate search index cache for this class R->>R: Record idempotencyKey in processed-events log R-->>P: 200 OK Note over P: On 5xx or timeout:<br/>retry with exponential backoff,<br/>min 3 retries over 1 hour

Recspert webhook receiver expectations:


Appendix B — Field Reference Quick-Lookup

Required vs. optional fields per entity. For full schemas see the section noted in the rightmost column.

Club (Section 4.1)

Field Type Req? Notes
id string Opaque, stable
name string Display name
shortName string Used in dense UI
address object Standard postal address
geo.lat / geo.lng number Falls back to address geocoding if absent
timezone string IANA, e.g. America/New_York
imageUrl string Recspert uses generic placeholder if absent
phone / email string For contact section
websiteUrl string Linked in detail page
active boolean Inactive clubs are excluded from search

Location (Section 4.2)

Field Type Req? Notes
id string Opaque, stable
clubId string Parent club
name string "Main Pool", "Studio B"
address object Used for map + driving directions
geo.lat / geo.lng number Geocoded from address if absent
timezone string Critical: each location may differ from club
imageUrl string

Class — entityType C (Section 4.4)

Field Type Req? Notes
id string Opaque, stable across instance lifetime
clubId string
name string Display title
description string Markdown or plain
locationId string Drives timezone
categoryIds string[] At least one required for filtering
startTimestamp ISO-8601 With timezone offset
endTimestamp ISO-8601 With timezone offset
startTimeOfDay / endTimeOfDay string HH:MM Local time, redundant with timestamps but resilient
status enum open, full, cancelled, closed
capacity integer Hidden if absent
availableSpots integer Hidden if absent; required for waitlist UX
registrationOpensAt ISO-8601 Used for "Register opens..." countdown
registrationClosesAt ISO-8601
pricing.memberFee string/cents
pricing.nonMemberFee string/cents Equal to memberFee if no distinction
pricing.currency string Always "USD" today
instructors[] object[] Shown on detail page
imageUrl string Falls back to category default
tags string[] For "All Levels", "Morning", etc.

Program — entityType P (Section 4.5)

Same fields as Class except:

Field Type Req? Notes
startDate / endDate ISO-8601 date Replaces startTimestamp/endTimestamp for the program-level record
sessions[] object[] Each session has id, startTimestamp, endTimestamp; at least one required
ageRange.min / .max integer For youth program filtering

Package — entityType K (Section 4.6)

Field Type Req? Notes
id string
clubId string
name string
description string
packageType integer 1 = Group Ex, 2 = Individual/PT
packageTypeName string Display label for packageType
rates[] object[] Multi-tier packages list here; single-rate packages use top-level memberPrice
rates[].id string (when rates present)
rates[].sessionCount integer
rates[].price string/cents
memberPrice string/cents Top-level fallback / single-rate price
nonMemberPrice string/cents
canExpire boolean
expirationPolicy string Human-readable, e.g. "12 months after purchase"
includedActivities[] object[] ✅ for packageType=1 Each entry: seasonName, activities: string[]
imageUrl string

User (Section 5)

Field Type Req? Notes
id string Opaque, stable
clubId string
email string Used for duplicate detection
firstName / lastName string
dateOfBirth ISO-8601 date Optional in user record; conditionally required at registration for age-gated programs (enforce at checkout/preview, not at user create)
phone string E.164 preferred
address object Required by some partners for membership sales
isMember boolean Affects pricing tier
membershipExpiresAt ISO-8601 date Required if isMember and membership has an expiration
dependents[] object[] For family registrations

Checkout / Preview (Section 6)

Field Type Req? Notes
registrants[] object[] One per submitted userId
registrants[].userId string
registrants[].eligible boolean Hard gate on registration
registrants[].reason string ✅ if eligible=false Machine-readable reason code
registrants[].userMessage string ✅ if eligible=false Displayed to end user
registrants[].price string/cents Per-registrant line item
registrants[].waiversRequired string[] Waiver IDs
registrants[].customFieldsRequired string[] Custom field IDs
subtotal / tax / total string/cents
currency string
requiredPayment boolean Drives whether Recspert prompts for payment method
warnings[] string[] Non-blocking notices for the user

Registration response (Section 7)

Field Type Req? Notes
id string Used by Recspert for cancellation, roster lookup
status enum confirmed, waitlisted, pending
participantList[] object[] Per-user confirmation
total string/cents Final amount charged
paymentMethodId string Echoes what was sent
packageApplied object If Pattern A redemption applied
createdAt ISO-8601

Package Instance (Section 8)

Field Type Req? Notes
id string Distinct from the catalog packageId
packageId string Reference back to the catalog
userId string Owner
sessionsTotal integer
sessionsRemaining integer Used by Recspert to render "X of Y remaining"
purchasedAt ISO-8601
expiresAt ISO-8601 date Required if canExpire=true on the catalog package
status enum active, expired, consumed, cancelled
redemptions[] object[] Per-redemption history

Appendix C — Glossary


Document version 1.0 — 2026-05-19. Maintainer: Stuart Gill, Recspert. Send corrections / questions to sgill@recspert.com.