Skip to main content

POST /api/stripe/create-checkout

Create a Stripe Checkout session for one-time payments or subscriptions. This endpoint is triggered by the ButtonCheckout component.

Request

priceId
string
required
Stripe Price ID for the product or subscription plan.
successUrl
string
required
URL to redirect to after successful payment. Must be a valid URI.
cancelUrl
string
required
URL to redirect to if user cancels the checkout. Must be a valid URI.
mode
string
required
Checkout mode. Either payment for one-time payments or subscription for recurring subscriptions.

Response

url
string
The Stripe Checkout session URL to redirect the user to.
error
string
Error message returned when request fails.

Code Examples

curl -X POST https://8space.app/api/stripe/create-checkout \
  -H "Content-Type: application/json" \
  -d '{
    "priceId": "price_1234567890",
    "successUrl": "https://8space.app/success",
    "cancelUrl": "https://8space.app/cancel",
    "mode": "subscription"
  }'

Request Validation

packages/landing/app/api/stripe/create-checkout/route.ts
export async function POST(req: NextRequest) {
  const body = await req.json();

  if (!body.priceId) {
    return NextResponse.json(
      { error: "Price ID is required" },
      { status: 400 }
    );
  } else if (!body.successUrl || !body.cancelUrl) {
    return NextResponse.json(
      { error: "Success and cancel URLs are required" },
      { status: 400 }
    );
  } else if (!body.mode) {
    return NextResponse.json(
      {
        error:
          "Mode is required (either 'payment' for one-time payments or 'subscription' for recurring subscription)",
      },
      { status: 400 }
    );
  }

  try {
    const supabase = await createClient();
    const {
      data: { user },
    } = await supabase.auth.getUser();

    const { priceId, mode, successUrl, cancelUrl } = body;

    const stripeSessionURL = await createCheckout({
      priceId,
      mode,
      successUrl,
      cancelUrl,
      clientReferenceId: user?.id,
      user: user
        ? { email: user.email, customerId: null }
        : null,
    });

    return NextResponse.json({ url: stripeSessionURL });
  } catch (e: any) {
    console.error(e);
    return NextResponse.json({ error: e?.message }, { status: 500 });
  }
}

Error Handling

400
error
Validation ErrorReturned when:
  • priceId is missing: "Price ID is required"
  • successUrl or cancelUrl is missing: "Success and cancel URLs are required"
  • mode is missing: "Mode is required (either 'payment' for one-time payments or 'subscription' for recurring subscription)"
500
error
Internal Server ErrorReturned when Stripe API call fails or unexpected server error occurs.
{
  "error": "Error message from Stripe or internal error"
}

Success Response

Status Code: 200 OK
{
  "url": "https://checkout.stripe.com/pay/cs_test_..."
}

POST /api/stripe/create-portal

Create a Stripe Customer Portal session for managing billing and subscriptions. This endpoint is triggered by the ButtonAccount component and requires an authenticated user.

Authentication

This endpoint requires a valid Supabase user session. Users must be signed in with a session cookie.

Request

returnUrl
string
required
URL to return to after the user completes their session in the billing portal. Must be a valid URI.

Response

url
string
The Stripe Customer Portal URL to redirect the user to.
error
string
Error message returned when request fails.

Code Examples

curl -X POST https://8space.app/api/stripe/create-portal \
  -H "Content-Type: application/json" \
  -H "Cookie: sb-access-token=..." \
  -d '{
    "returnUrl": "https://8space.app/account"
  }'

Request Validation

packages/landing/app/api/stripe/create-portal/route.ts
export async function POST(req: NextRequest) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (user) {
    try {
      const body = await req.json();

      if (!body.returnUrl) {
        return NextResponse.json(
          { error: "Return URL is required" },
          { status: 400 }
        );
      }

      // TODO: Look up customerId from your database (e.g., a Supabase 'customers' table)
      // For now, this is a placeholder
      const customerId: string | null = null;

      if (!customerId) {
        return NextResponse.json(
          {
            error:
              "You don't have a billing account yet. Make a purchase first.",
          },
          { status: 400 }
        );
      }

      const stripePortalUrl = await createCustomerPortal({
        customerId,
        returnUrl: body.returnUrl,
      });

      return NextResponse.json({
        url: stripePortalUrl,
      });
    } catch (e: any) {
      console.error(e);
      return NextResponse.json({ error: e?.message }, { status: 500 });
    }
  } else {
    return NextResponse.json({ error: "Not signed in" }, { status: 401 });
  }
}

Error Handling

400
error
Validation ErrorReturned when:
  • returnUrl is missing: "Return URL is required"
  • User has no Stripe customer account: "You don't have a billing account yet. Make a purchase first."
401
error
UnauthorizedReturned when user is not authenticated.
{
  "error": "Not signed in"
}
500
error
Internal Server ErrorReturned when Stripe API call fails or unexpected server error occurs.
{
  "error": "Error message from Stripe or internal error"
}

Success Response

Status Code: 200 OK
{
  "url": "https://billing.stripe.com/session/..."
}

Implementation Notes

  • The endpoint includes a TODO for looking up the Stripe customer ID from the database
  • Currently uses a placeholder customerId that is always null
  • Users must make a purchase first to create a Stripe customer account before accessing the portal
  • All authentication is handled via Supabase session cookies

Build docs developers (and LLMs) love