Skip to content

Stripe Payments

PinTeach uses Stripe Connect (Standard accounts). Each teacher connects their own Stripe account. Money goes directly to the teacher — PinTeach never holds funds.

  1. Teacher clicks “Connect Stripe” in settings
  2. Redirect to Stripe OAuth consent
  3. Stripe redirects back with auth code
  4. Backend stores stripeAccountId on teacher record
  5. Teacher can now accept payments
1. Student selects paid trial
2. Create Session(hold) + holdExpiresAt = now + 15min
3. Create Stripe Checkout Session (on teacher's connected account)
4. Student completes payment
5. Webhook: checkout.session.completed → Session(scheduled)
1. Student selects service
2. Magic link auth (if new)
3. Accept legal documents
4. Redirect to Stripe Checkout
5. Webhook: checkout.session.completed → Enrollment(active), credit GRANT
Webhook: invoice.paid → Auto-renew subscription enrollment
Webhook: customer.subscription.deleted → Cancel enrollment

Endpoint: POST /api/webhooks/stripe

Signature verified via stripe-signature header.

EventAction
checkout.session.completedActivate enrollment / confirm payment
invoice.paidAuto-renew subscription
customer.subscription.deletedCancel enrollment
charge.refundedApply refund
charge.dispute.createdRecord dispute

All webhook processing is idempotent via webhook_events.stripeEventId:

const [inserted] = await db.insert(webhookEvents).values({
stripeEventId: event.id,
type: event.type,
}).onConflictDoNothing().returning();
if (!inserted) return; // Already processed

Stripe API calls are protected by a circuit breaker:

  • Config: 3 failures in 30s → circuit opens
  • Recovery: 15s half-open period
  • Fallback: throws SERVICE_UNAVAILABLE (503)

Only infrastructure errors (429, 5xx, timeouts) trip the circuit — not business errors.

Discount codes are server-side, NOT Stripe Coupons. PinTeach adjusts the price before creating the Stripe Checkout Session.

  • discount_codes table with soft-delete (deletedAt)
  • discount_code_uses tracks redemptions per student per enrollment
  • enrollments.discountCodeId + discountAmount record the applied discount
TypeValue RangeDescription
percentage0–100Percentage off the service price
fixed_amountcentsFixed amount off (e.g., 500 = $5.00)

Optional fields: currency (for fixed amounts), applicableServiceIds (empty array = all services).

DiscountCodeService.validate() checks:

  1. Code exists, is active, and not soft-deleted
  2. Date range (validFrom / validUntil)
  3. Global usage limit (maxUses vs usesCount)
  4. Per-student usage limit (maxUsesPerStudent)
  5. Applicable services (applicableServiceIds)
1. Student enters code at checkout
2. POST /:slug/validate-discount (rate-limited 10/min)
3. Frontend shows adjusted price
4. On enrollment: server re-validates + adjusts unit_amount
5. Stripe Checkout Session created with discounted price
6. Webhook confirms → discount_code_uses recorded

POST /api/public/:slug/validate-discount — rate-limited to 10 requests per minute. Returns discount details or validation error.

/teacher/payments provides:

  • Payment history (filterable, paginated)
  • Revenue KPIs
  • CSV export
  • Refund initiation