Skip to main content
Hayon uses Stripe as its payment processor for all subscription billing. The integration covers checkout, the customer billing portal, subscription cancellation, status retrieval, and webhook-driven lifecycle events.

Configuration

The Stripe integration requires three environment variables on the backend:
VariableDescription
STRIPE_SECRET_KEYYour Stripe secret key (starts with sk_)
STRIPE_PRO_PRICE_IDThe Stripe Price ID for the Pro plan (starts with price_)
STRIPE_WEBHOOK_SECRETThe webhook signing secret from your Stripe dashboard (starts with whsec_)
Use separate Stripe accounts or restricted keys for development and production. Stripe test mode keys (starting with sk_test_) work with test card numbers like 4242 4242 4242 4242.

API endpoints

All payment endpoints are mounted at /api/payments.
MethodPathAuth requiredDescription
POST/api/payments/create-checkoutYesStart a Stripe Checkout session
POST/api/payments/billing-portalYesOpen the Stripe Billing Portal
POST/api/payments/cancelYesSchedule subscription cancellation
GET/api/payments/statusYesGet current subscription status
POST/api/payments/webhookNo (raw body)Receive Stripe webhook events

Creating a checkout session

To start the subscription checkout flow, send a POST request to /api/payments/create-checkout. No request body is required — the user’s ID and email are read from the JWT token.
curl -X POST http://localhost:5000/api/payments/create-checkout \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json"
Response:
{
  "message": "Checkout session created",
  "data": {
    "url": "https://checkout.stripe.com/c/pay/cs_test_..."
  }
}
Redirect the user to the returned url. Stripe handles everything from there. On success, Stripe redirects to {FRONTEND_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}. On cancellation, the user lands on {FRONTEND_URL}/payment/cancel.
The checkout session is created with allow_promotion_codes: true, so users can enter valid Stripe promotion codes at checkout.

Managing subscription via billing portal

The Stripe Billing Portal lets Pro users update their payment method, download invoices, and manage their subscription directly through a Stripe-hosted interface.
curl -X POST http://localhost:5000/api/payments/billing-portal \
  -H "Authorization: Bearer <token>"
Response:
{
  "message": "Billing portal session created",
  "data": {
    "url": "https://billing.stripe.com/session/..."
  }
}
Redirect the user to the returned url. After they finish, Stripe returns them to {FRONTEND_URL}/settings.
This endpoint returns a 400 error if the user does not have a stripeCustomerId on their account. This means they have never completed a checkout session. Direct them to /api/payments/create-checkout instead.

Canceling a subscription

Cancellation in Hayon is always deferred to the end of the current billing period. The user retains Pro access until then.
curl -X POST http://localhost:5000/api/payments/cancel \
  -H "Authorization: Bearer <token>"
Successful response:
{
  "message": "Subscription will be cancelled at the end of the billing period"
}
This call sets cancel_at_period_end = true in Stripe and syncs the cancelAtPeriodEnd flag to the user’s record in MongoDB. When the period ends, Stripe fires customer.subscription.deleted and Hayon automatically downgrades the user to the Free plan. Error cases:
ConditionStatusMessage
User is on the Free plan400No active Pro subscription to cancel
No Stripe subscription ID on record400No Stripe subscription ID found
Already scheduled for cancellation400Subscription is already scheduled for cancellation

Checking subscription status

curl http://localhost:5000/api/payments/status \
  -H "Authorization: Bearer <token>"
Response:
{
  "message": "Subscription status",
  "data": {
    "subscription": {
      "plan": "pro",
      "status": "active",
      "stripeCustomerId": "cus_...",
      "stripeSubscriptionId": "sub_...",
      "currentPeriodStart": "2025-03-01T00:00:00.000Z",
      "currentPeriodEnd": "2025-04-01T00:00:00.000Z",
      "cancelAtPeriodEnd": false
    },
    "usage": {
      "postsCreated": 12,
      "captionGenerations": 5
    },
    "limits": {
      "maxPosts": 60,
      "maxCaptionGenerations": 30
    },
    "plan": "pro"
  }
}
Possible values for subscription.status: active, cancelled, pastDue.

Webhook handling

Stripe sends lifecycle events to POST /api/payments/webhook. This endpoint is unauthenticated and does not go through the global express.json() middleware.

Raw body requirement

The webhook route uses express.raw({ type: "application/json" }) instead of express.json(). This is required because Stripe signature verification (stripe.webhooks.constructEvent) must receive the raw, unparsed request body as a Buffer. If the body is parsed by express.json() first, signature verification will fail with a 400 error.
This is configured in payment.routes.ts:
paymentRouter.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  handleWebhook
);
The handler reads the stripe-signature header and verifies the event against your STRIPE_WEBHOOK_SECRET before processing.

Handled webhook events

Fired when the user completes a checkout session and payment is confirmed. Hayon:
  1. Reads the userId from the session’s metadata field.
  2. Retrieves the full subscription object from Stripe to get billing period dates.
  3. Upgrades the user to the Pro plan in MongoDB, storing stripeCustomerId, stripeSubscriptionId, currentPeriodStart, and currentPeriodEnd.
Fired on every successful invoice payment. The first invoice (reason: subscription_create) is skipped because it is already handled by checkout.session.completed. For subsequent renewals, Hayon:
  1. Looks up the user by stripeCustomerId.
  2. Fetches updated billing period dates from Stripe.
  3. Resets usage counters and updates currentPeriodStart / currentPeriodEnd in MongoDB.
Fired when a renewal invoice fails (e.g., expired or declined card). Hayon marks the user’s subscription status as pastDue. The user’s account stays on Pro but they will be prompted to update their payment method via the billing portal.
Fired after a subscription fully expires — typically after a cancel_at_period_end period ends or Stripe terminates a past-due subscription. Hayon downgrades the user to the Free plan and clears the Stripe subscription reference.
Fired whenever a subscription is updated — most commonly when cancel_at_period_end is toggled (e.g., via the billing portal). Hayon syncs the cancelAtPeriodEnd flag in MongoDB to match Stripe’s record.

Testing webhooks locally

Use the Stripe CLI to forward events to your local server:
stripe listen --forward-to http://localhost:5000/api/payments/webhook
The CLI will print a webhook signing secret (whsec_...). Set this as STRIPE_WEBHOOK_SECRET in your .env for local development.
Trigger specific events during testing with stripe trigger checkout.session.completed or stripe trigger customer.subscription.deleted.

Build docs developers (and LLMs) love