> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Stripe Payments

> Accept credit card payments on your site with Stripe Checkout or Stripe Elements

Accept credit card payments, subscriptions, and one-time charges by wiring Stripe into your Hiveku project.

<Info>
  Before you start: create a [Stripe account](https://stripe.com) and grab your test keys from the Stripe Dashboard > Developers > API keys. You'll also need a product and price ID for whatever you're selling.
</Info>

## Three Paths

<Tabs>
  <Tab title="AI Chat (Fastest)">
    Ask the project AI to set it up:

    ```
    Add Stripe Checkout for a $29/month subscription. Use my Stripe 
    test keys from env vars STRIPE_SECRET_KEY and 
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. Wire up a /api/checkout route 
    and a Buy button on the pricing page.
    ```

    The AI adds env var references, installs the SDK, creates the API route, and updates the pricing page. Review before deploy.
  </Tab>

  <Tab title="Stripe SDK (Manual)">
    Follow the steps below to build the integration yourself. Gives you the most control.
  </Tab>

  <Tab title="Stripe Elements">
    For fully custom payment UIs (not Checkout hosted pages), use Stripe Elements. The env vars and webhook flow are the same; the client code uses `@stripe/react-stripe-js`. See [Stripe's Elements docs](https://stripe.com/docs/stripe-js) for the client components.
  </Tab>
</Tabs>

## Step 1: Add Env Vars

You need two keys, and you'll add a third once webhooks are set up.

<Steps>
  <Step title="Get your keys from Stripe">
    Stripe Dashboard > Developers > API keys. Start with **test mode**:

    * `sk_test_...` — secret key (server-side only)
    * `pk_test_...` — publishable key (client-side OK)
  </Step>

  <Step title="Add them as env vars in Hiveku">
    See [Configure Environment Variables](/how-tos/env-vars). Use these names:

    * `STRIPE_SECRET_KEY` — server-side only
    * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — client-side OK
  </Step>
</Steps>

<Warning>
  Never expose `STRIPE_SECRET_KEY` in client-side code. It must stay in server-only routes. The `NEXT_PUBLIC_` prefix on the publishable key is safe — that key is meant for the browser.
</Warning>

## Step 2: Install the SDK

In your [terminal](/how-tos/use-terminal):

```bash theme={null}
npm install stripe @stripe/stripe-js
```

## Step 3: Create a Checkout Session

Server-side route that creates a Stripe Checkout session:

```typescript theme={null}
// app/api/checkout/route.ts (Next.js App Router)
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const origin = req.headers.get('origin') || 'https://yoursite.com';

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription', // or 'payment' for one-time
    line_items: [
      { price: 'price_xxxxxxxxxxxxxx', quantity: 1 },
    ],
    success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${origin}/cancel`,
  });

  return Response.json({ url: session.url });
}
```

Get your `price_xxx` ID from Stripe Dashboard > Products > click your product > Pricing.

## Step 4: Redirect from the Buy Button

Client-side, when the user clicks "Buy":

```typescript theme={null}
'use client';

export function BuyButton() {
  async function handleBuy() {
    const res = await fetch('/api/checkout', { method: 'POST' });
    const { url } = await res.json();
    window.location.href = url;
  }

  return <button onClick={handleBuy}>Subscribe</button>;
}
```

That's enough to accept payments. To reliably know when a payment succeeds, you also need webhooks.

## Step 5: Set Up a Stripe Webhook

<Steps>
  <Step title="Open Stripe Dashboard > Webhooks">
    In Stripe, go to Developers > Webhooks and click **Add endpoint**.
  </Step>

  <Step title="Enter your endpoint URL">
    Use the Hiveku URL of your webhook route:

    ```
    https://yoursite.hiveku.com/api/stripe-webhook
    ```

    Or your custom domain if configured.
  </Step>

  <Step title="Select events to listen for">
    Common choices:

    * `checkout.session.completed` — one-time purchases and subscription starts
    * `payment_intent.succeeded` — any successful payment
    * `customer.subscription.updated` — plan changes, cancellations
    * `customer.subscription.deleted` — subscription ended
  </Step>

  <Step title="Copy the signing secret">
    Stripe shows a signing secret starting with `whsec_`. Copy it and add as env var `STRIPE_WEBHOOK_SECRET`.
  </Step>
</Steps>

## Step 6: Handle the Webhook

Server-side route that validates the signature and processes events:

```typescript theme={null}
// app/api/stripe-webhook/route.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature');
  if (!sig) return new Response('Missing signature', { status: 400 });

  // IMPORTANT: use raw body for signature verification
  const body = await req.text();

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return new Response(`Webhook error: ${(err as Error).message}`, { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // grant access, send receipt, update database...
      break;
    }
    case 'customer.subscription.updated': {
      const sub = event.data.object as Stripe.Subscription;
      // update user's plan/status in your DB
      break;
    }
  }

  return Response.json({ received: true });
}
```

<Warning>
  Verify against the raw request body (`await req.text()`), not the parsed JSON. Parsing changes whitespace and breaks the signature. This is the single most common Stripe webhook bug.
</Warning>

See the [serverless guide](/publishing/serverless-guide) for more on webhook patterns.

## Test It End-to-End

<Steps>
  <Step title="Use Stripe test cards">
    Checkout accepts these in test mode:

    * `4242 4242 4242 4242` — successful payment
    * `4000 0000 0000 0002` — generic decline
    * `4000 0000 0000 9995` — insufficient funds

    Any future expiry and any 3-digit CVC.
  </Step>

  <Step title="Complete a test checkout">
    Click your Buy button, enter the test card, submit.
  </Step>

  <Step title="Check Stripe Dashboard">
    Payments > Test data should show the successful payment. The webhook endpoint's **Recent attempts** log shows your endpoint returning 200.
  </Step>

  <Step title="Check your database or app">
    Whatever your webhook handler updates (grant access, add credits, etc.) should have happened.
  </Step>
</Steps>

## Go Live

When you're ready to accept real payments:

1. In Stripe Dashboard, toggle from **Test mode** to **Live mode**
2. Copy the live `sk_live_...` and `pk_live_...` keys
3. Update your Hiveku env vars (use different values per environment if you want staging on test, production on live)
4. Re-create the webhook in live mode — live webhooks are separate from test webhooks and have a new signing secret
5. Update `STRIPE_WEBHOOK_SECRET` to the live value
6. Do a small real-money test (then refund yourself)

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook signature verification fails">
    You're almost certainly parsing the body before verifying. Use `await req.text()` and pass that string to `constructEvent`. Also make sure `STRIPE_WEBHOOK_SECRET` is set correctly and matches the mode (test vs live) you're operating in.
  </Accordion>

  <Accordion title="&#x22;No such price: price_xxx&#x22;">
    Product IDs differ between test and live modes. A price created in test mode doesn't exist in live mode — you need to recreate products/prices in live mode, or use Stripe's Products API to copy them over.
  </Accordion>

  <Accordion title="Redirect URL invalid">
    `success_url` and `cancel_url` must be fully-qualified URLs (including `https://`). `/success` won't work — use `${origin}/success` or hardcode the full URL.
  </Accordion>

  <Accordion title="CORS errors on the Buy button fetch">
    Webhooks don't use CORS (Stripe calls server-to-server), but your `/api/checkout` fetch from the browser is subject to same-origin rules. If you're calling from a different domain, configure CORS on your API route. If same-domain, check that the route path is correct and the deployment succeeded.
  </Accordion>

  <Accordion title="Webhook fires but handler doesn't run">
    Check the deployment log for errors in the webhook handler. Common causes: typo in env var name, SDK version mismatch, an unhandled async throw. Also check that the event type you're testing is in your `switch` — Stripe logs the event type it sent, and you can see it in the Webhooks dashboard.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Environment Variables" icon="key" href="/how-tos/env-vars">
    Manage your Stripe keys per environment
  </Card>

  <Card title="Serverless Guide" icon="server" href="/publishing/serverless-guide">
    More on API routes and webhook patterns
  </Card>
</CardGroup>
