EduHub integrates with Stripe to handle course enrollment payments. The integration supports course base pricing and add-on pricing based on Formbricks survey responses, providing a seamless payment experience for users.
- Checkout Sessions: Secure Stripe Checkout sessions for course enrollment payments
- Add-on Pricing: Dynamic pricing based on Formbricks survey responses
- Webhook Processing: Automatic enrollment status updates when payments complete
- Payment Tracking: Full payment history and status tracking in the database
- Multi-environment Support: Separate configurations for development, staging, and production
createStripeCheckout/index.js
- Creates Stripe Checkout sessions for course enrollments
- Handles base course pricing and selected add-ons
- Returns checkout URL for redirect
createStripeAddonPrices/index.js
- Creates Stripe products and prices for validated add-ons
- Manages product/price lifecycle
pages/api/webhooks/stripe.ts
- Next.js API route for receiving Stripe webhooks
- Verifies webhook signatures
- Updates enrollment records via GraphQL mutations
components/pages/CourseContent/Registration/hooks/useRegistrationHandler.ts
- Handles registration flow including Stripe checkout
- Creates checkout session via GraphQL mutation
- Redirects user to Stripe Checkout
CourseEnrollment table:
stripeCheckoutSessionId: Stripe Checkout Session IDstripePaymentIntentId: Stripe Payment Intent IDpaymentStatus: Enum (PENDING,COMPLETED,FAILED)paymentAmount: Payment amount in centspaymentCurrency: Currency code (e.g.,eur,usd)
Course table:
basePrice: Base course price in centscurrency: Currency codestripeProductId: Stripe Product IDstripePriceId: Stripe Price ID
CourseAddonMapping table:
validatedPrice: Add-on price in centsstripeProductId: Stripe Product IDstripePriceId: Stripe Price ID
- Create a Stripe account at stripe.com
- Navigate to Developers → API Keys
- Copy your Secret Key (starts with
sk_test_...for test mode) - Copy your Publishable Key (starts with
pk_test_...for test mode)
Stripe prints the seller block and the invoice number from account settings; no code passes them. Configure each account once:
- Settings → Business → Public details: support address
(Fraunhoferstr. 13, 24118 Kiel) and support email
rechnungen@opencampus.sh. The legal address under Unternehmensdaten is not printed on invoices. - Settings → Billing → Invoices: default account tax ID
eu_vatDE322443015, and invoice number prefixEDUHUB(FastBill and Billomat also issue opencampus invoices, so the prefix keeps the sequences apart).
The code adds the rest of what §14 UStG requires:
- Buyer address: Checkout requires a billing address and offers an
optional tax ID field (
buildPaymentMethodConfig); "Kauf auf Rechnung" collects both in the StuJo order form. - Leistungszeitraum / Leistungsdatum as an invoice custom field
(
buildServicePeriodField): the exact posting runtime on "Kauf auf Rechnung" invoices, "N Tage ab Rechnungsdatum" on Checkout job posting invoices (the posting goes live when Stripe dates the invoice, and the field is fixed before payment), the program'slectureStart–lectureEndfor courses (the purchase day when unset). - German invoices: customers get
preferred_locales: ['de']. metadata.sourceon sessions, payments and invoices:stujo,courses,eventsordegrees(buildCourseSource;eduhubfor an unknown program type).
-
Install Stripe CLI (for local webhook testing):
# macOS brew install stripe/stripe-cli/stripe # Linux # Download from https://stripe.com/docs/stripe-cli
-
Authenticate Stripe CLI:
stripe login
-
Start local webhook listener:
stripe listen --forward-to localhost:5000/api/webhooks/stripe
This will output a webhook signing secret (starts with
whsec_...). You can use this temporarily, but it's recommended to leaveSTRIPE_WEBHOOK_SECRETempty in development and use the CLI instead. -
Configure
.envfile:STRIPE_SECRET_KEY=sk_test_your_test_secret_key STRIPE_WEBHOOK_SECRET= # Leave empty, use Stripe CLI instead STRIPE_PUBLISHABLE_KEY=pk_test_your_test_publishable_key
Note: Staging and production environments use Terraform for configuration, not .env files.
-
Create Webhook Endpoint in Stripe Sandbox:
- Go to Stripe Dashboard → Developers → Webhooks
- Click "+ Add endpoint" or "+ Ziel hinzufügen"
- Enter your staging webhook URL:
https://staging.your-domain.com/api/webhooks/stripe - Select events to listen for:
checkout.session.completedcheckout.session.expiredcheckout.session.async_payment_succeededcheckout.session.async_payment_failedpayment_intent.payment_failedinvoice.finalizedinvoice.paidinvoice.overdueinvoice.voided
- Click Add endpoint
invoice.finalizedis required for StuJo job postings. Their confirmation mail carries the invoice PDF, and Stripe finalizesinvoice_creationinvoices asynchronously, so the PDF often does not exist yet atcheckout.session.completed. Without this event the mail falls through to thesend_pending_job_posting_mailscron sweep and arrives up to ~30 minutes late. Nothing fails loudly if you forget it, so check it here.invoice.paid,invoice.overdueandinvoice.voideddrive StuJo "Kauf auf Rechnung". Organizations an admin approved (Organization.allowInvoicePayment, toggled on the job board settings page) get a Stripe invoice paid by EU bank transfer instead of Checkout. Stripe assigns each customer a virtual IBAN and matches incoming transfers itself;invoice.paidthen flips ourInvoicerow toPAID,invoice.overduemarks itOVERDUEand notifiesSTUJO_ADMIN_EMAIL. Without these events the row staysISSUEDalthough the money arrived. This also requires Settings → Payment methods → Bank transfers to be activated on the account (sandbox and live); otherwise issuing the invoice fails withINVOICE_PAYMENT_FAILED. -
Get Webhook Signing Secret:
- Click on your newly created webhook endpoint
- Copy the Signing secret (starts with
whsec_...)
-
Configure via Terraform:
- Update Terraform variables with your Stripe test keys and webhook secret
- Secrets are stored in Google Secret Manager (see
infrastructure/application/01_secrets.tf) - Cloud functions automatically load secrets from Secret Manager (see
infrastructure/application/06_cloud-functions.tf) - Frontend environment variables are set via Terraform (see
infrastructure/application/07_edu.tf)
Note: Production uses Terraform for configuration, not .env files.
-
Switch to Live Mode in Stripe Dashboard
-
Create Webhook Endpoint:
- Go to Stripe Dashboard → Developers → Webhooks
- Click "+ Add endpoint"
- Enter your production webhook URL:
https://your-domain.com/api/webhooks/stripe - Select the same events as staging:
checkout.session.completedcheckout.session.expiredcheckout.session.async_payment_succeededcheckout.session.async_payment_failedpayment_intent.payment_failedinvoice.finalizedinvoice.paidinvoice.overdueinvoice.voided
- Click Add endpoint
-
Get Live Webhook Signing Secret:
- Copy the signing secret from your live webhook endpoint
-
Configure via Terraform:
- Update Terraform variables with your Stripe live keys (
sk_live_...,pk_live_...) and webhook secret - Secrets are stored in Google Secret Manager
- Apply Terraform changes to update the infrastructure
- Update Terraform variables with your Stripe live keys (
The docker-compose.yml file automatically loads Stripe environment variables from your .env file for local development:
frontend-nx:
environment:
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY:-}
node_functions:
environment:
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}Note: This configuration is only for local development. Staging and production environments use Terraform (see below).
Stripe secrets for staging and production are configured via Terraform:
-
Secret definitions:
infrastructure/application/01_secrets.tf- Defines Google Secret Manager secrets for
stripe_secret_keyandstripe_webhook_secret - Secrets are populated from Terraform variables
- Defines Google Secret Manager secrets for
-
Cloud function configuration:
infrastructure/application/06_cloud-functions.tf- Injects Stripe secrets into the
call_node_functioncloud function - Secrets are loaded from Secret Manager at runtime
- Injects Stripe secrets into the
-
Frontend configuration:
infrastructure/application/07_edu.tf- Sets
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYas an environment variable for the frontend service
- Sets
To configure Stripe for staging/production:
- Set Terraform variables for Stripe keys and webhook secrets
- Apply Terraform changes:
terraform apply - Secrets are automatically stored in Google Secret Manager
- Services load secrets from Secret Manager at runtime
-
checkout.session.completed- Triggered when a payment is successfully completed
- Updates enrollment:
paymentStatus = COMPLETED,status = CONFIRMED - Stores payment details (amount, currency, session ID, payment intent ID)
-
checkout.session.expired- Triggered when a checkout session expires without payment
- Updates enrollment:
paymentStatus = FAILED
-
checkout.session.async_payment_succeeded- Delayed payment method (SEPA direct debit) settled
- Course: enrollment confirmed. Job posting: invoice
ISSUED->PAID
-
checkout.session.async_payment_failed- Delayed payment failed after the fact
- Job posting: taken offline (
DRAFT), invoiceCANCELLED, employer notified
-
payment_intent.payment_failed- Triggered when a payment attempt fails
- Updates enrollment:
paymentStatus = FAILED
-
invoice.finalized- Stripe has assigned the invoice its document number and rendered the PDF
- Backfills
Invoice.stripeInvoiceNumber/stripeInvoicePdfUrl/stripeHostedInvoiceUrl - For job postings, releases the confirmation mail with the PDF attached if
it was not already sent at checkout time. Handled by
handleJobPostingInvoiceFinalizedinlib/stripeJobPosting.ts; thesend_pending_job_posting_mailscron is the backstop when this event never arrives.
All webhook requests are verified using Stripe's signature verification:
event = stripe.webhooks.constructEvent(
rawBody,
stripeSignature,
webhookSecret
);This ensures that webhook events are authentic and come from Stripe.
- User fills out course registration form (including Formbricks survey)
- User selects add-ons (if applicable)
- Frontend calls
createStripeCheckoutmutation - Backend creates Stripe Checkout session with:
- Base course price (if applicable)
- Selected add-on prices
- Metadata:
courseId,enrollmentId,formbricksResponseId
- User is redirected to Stripe Checkout
- User completes payment
- Stripe sends
checkout.session.completedwebhook - Webhook handler updates enrollment status
- Pending: Enrollment created, payment not yet completed
- Completed: Payment successful, enrollment confirmed
- Failed: Payment failed or expired
-
Start Stripe CLI listener:
stripe listen --forward-to localhost:5000/api/webhooks/stripe
-
Trigger test events:
# Simulate successful payment stripe trigger checkout.session.completed # Simulate expired session stripe trigger checkout.session.expired # Simulate failed payment stripe trigger payment_intent.payment_failed
-
View webhook logs:
stripe logs tail
Use Stripe test cards for testing:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - Requires authentication:
4000 0025 0000 3155
See Stripe Testing Documentation for more test cards.
- Check webhook endpoint URL is correct and publicly accessible
- Verify webhook secret matches the one in Stripe Dashboard
- Check webhook logs in Stripe Dashboard → Developers → Webhooks → [Your endpoint] → Logs
- For local development: Ensure Stripe CLI is running and forwarding correctly
- Check webhook handler logs for errors
- Verify enrollment ID is present in checkout session metadata
- Check GraphQL mutation is executing successfully
- Verify Hasura permissions allow enrollment updates
- Re-authenticate:
stripe login - Check Stripe CLI version:
stripe --version - Restart listener: Stop and restart
stripe listen
- Never commit secrets:
- Local development: Stripe keys are in
.env(gitignored) - Staging/Production: Stripe keys are stored in Google Secret Manager via Terraform
- Local development: Stripe keys are in
- Use test keys for development: Never use live keys in development or staging
- Webhook signature verification: Always verify webhook signatures to ensure events are from Stripe
- HTTPS required: Production webhooks must use HTTPS (enforced by Stripe)
- Secret rotation: Rotate webhook secrets if compromised
- Environment separation: Use separate Stripe accounts or test/live modes for different environments
- Stripe Checkout Documentation
- Stripe Webhooks Guide
- Stripe CLI Documentation
- Formbricks Integration - For add-on pricing based on survey responses
| Variable | Description | Required | Example |
|---|---|---|---|
STRIPE_SECRET_KEY |
Stripe API secret key (test mode) | Yes | sk_test_... |
STRIPE_WEBHOOK_SECRET |
Webhook signing secret | No* | whsec_... |
STRIPE_PUBLISHABLE_KEY |
Frontend publishable key (test mode) | Yes | pk_test_... |
* Not required for local development when using Stripe CLI
Stripe configuration for staging and production is managed via Terraform:
- Secrets are stored in Google Secret Manager
- Configured in
infrastructure/application/01_secrets.tf - Injected into services via
infrastructure/application/06_cloud-functions.tfand07_edu.tf - Use Terraform variables to set values, not
.envfiles