Skip to main content

What is a Checkout Session?

A checkout session is a secure, temporary link where your customers complete their payment. Each session:
  • Is unique and expires after a set time
  • Contains all the payment information
  • Redirects customers after successful/failed payment
  • Can be tracked via webhooks

Prerequisites

Before creating checkout sessions, you need:
  1. API Key - Get it from your dashboard
  2. Product ID - You’ll need this to create checkout sessions

API Versions

Loops offers three API versions with different levels of flexibility:
VersionParametersBest For
V1paymentLinkId onlyLegacy applications
V2paymentLinkId OR productIdMost Loops applications (recommended)
V3line_items (Stripe-style) OR productId (Loops-style)Stripe migrations & advanced use cases
We recommend V2 for most use cases - it’s simple, flexible, and fully featured. Use V3 if you’re migrating from Stripe or need line_items support.

Getting Your Product ID

You have two options to get your product ID:

Option 1: From Dashboard (Easiest)

  1. Go to your Loops Dashboard
  2. Navigate to Products
  3. Find the product you want to sell
  4. Click the three-dot menu (⋮) next to your product
  5. Click “Copy Product ID”
  6. Use this ID in your checkout session creation
The product ID will look like: prod_abc123xyz or pl_1234567890 (for payment links)
Copy product ID from dashboard

Option 2: Create Product via API

You can also create products programmatically and get the ID from the response:
import { Loops } from "@loops-fi/sdk";

const loops = new Loops({
  apiKey: process.env.LOOPS_API_KEY,
});

// Create a product
const product = await loops.products.create({
  name: "Premium Plan",
  description: "Access to all premium features",
  priceAmount: 2999, // $29.99 in cents
  priceCurrency: "usd",
});

console.log("Product ID:", product.id);
// Save this ID - you'll use it for checkout sessions
// product.id -> "prod_abc123xyz"
One-time setup: You typically create products once (either in dashboard or via API), then reuse the same product ID for multiple checkout sessions.

The Flow

Here’s the typical workflow:
  1. Create Product (one time)
    • Via dashboard OR via API
    • Get the product ID
  2. Create Checkout Sessions (many times)
    • Use the product ID from step 1
    • Create a new session for each customer/purchase
  3. Customer Pays
    • Redirect customer to the checkout URL
    • They complete payment
  4. Receive Webhook
    • Get notified when payment succeeds
    • Fulfill the order

Creating a Checkout Session

Now that you have your product ID, you can create checkout sessions.

Basic Example

import { Loops } from "@loops-fi/sdk";

const loops = new Loops({
  apiKey: process.env.LOOPS_API_KEY,
});

async function createCheckout() {
  const session = await loops.checkoutSessions.create({
    // Use the product ID you copied from dashboard or got from API
    productId: "prod_abc123xyz",
    externalCustomerId: "customer_123",
    metadata: {
      orderId: "order_456",
    },
  });

  console.log("Checkout URL:", session.url);
  return session.url;
}
V2 accepts both parameters: Since V2 supports both workflows:
  • productId (recommended) - Direct product checkout: prod_abc123xyz
  • paymentLinkId (alternative) - Pre-configured payment link: pl_1234567890
Both work identically in V2. We recommend productId for clarity!

Request Parameters

ParameterTypeRequiredDescription
productIdstringYes*Your product ID from dashboard or API (e.g., prod_abc123xyz)
paymentLinkIdstringYes*Alternative to productId - payment link ID (e.g., pl_1234567890)
externalCustomerIdstringNoYour internal customer identifier
metadataobjectNoCustom data (max 50 key-value pairs)
*Either productId or paymentLinkId is required (not both)

Response

{
  "id": "cs_abc123xyz",
  "url": "https://checkout.loops.fi/cs_abc123xyz",
  "productId": "prod_abc123xyz",
  "externalCustomerId": "customer_123",
  "status": "open",
  "metadata": {
    "orderId": "order_456"
  },
  "createdAt": "2025-01-15T10:30:00Z",
  "expiresAt": "2025-01-15T11:30:00Z"
}

Redirecting Your Customer

After creating a checkout session, redirect your customer to the url field:
import { redirect } from 'next/navigation';

async function checkoutAction() {
  'use server';

  const loops = new Loops({ apiKey: process.env.LOOPS_API_KEY });
  const session = await loops.checkoutSessions.create({
    productId: "prod_abc123xyz",
  });

  redirect(session.url);
}

Complete End-to-End Example

Here’s a full example showing the entire flow from product creation to checkout:
import { Loops } from "@loops-fi/sdk";

const loops = new Loops({
  apiKey: process.env.LOOPS_API_KEY,
});

// Step 1: Create a product (do this once)
async function setupProduct() {
  const product = await loops.products.create({
    name: "Premium Membership",
    description: "Get access to all premium features",
    priceAmount: 4999, // $49.99
    priceCurrency: "usd",
  });

  console.log("Product created:", product.id);
  // Save this ID in your database or config
  return product.id;
}

// Step 2: Create checkout sessions (do this for each purchase)
async function createCheckoutForCustomer(productId: string, userId: string) {
  const session = await loops.checkoutSessions.create({
    productId: productId, // Use the product ID from step 1
    externalCustomerId: userId,
    metadata: {
      userId: userId,
      plan: "premium",
      source: "website",
    },
  });

  console.log("Checkout URL:", session.url);
  // Redirect user to session.url
  return session.url;
}

// Example usage:
async function main() {
  // First time setup
  const productId = await setupProduct();
  // productId = "prod_abc123xyz"

  // Now use this productId for all your checkouts
  const checkoutUrl = await createCheckoutForCustomer(productId, "user_123");
  console.log("Send customer to:", checkoutUrl);
}
Important: Create your products once (either in dashboard or via API), then reuse the same product ID for all checkout sessions. Don’t create a new product for each checkout!

Using Metadata

Metadata allows you to attach custom data to a checkout session. This data is:
  • Returned in API responses
  • Included in webhook events
  • Visible in your dashboard

Example Use Cases

const session = await loops.checkoutSessions.create({
  productId: "prod_abc123xyz",
  externalCustomerId: "user_123",
  metadata: {
    orderId: "order_789",
    cartItems: JSON.stringify([
      { productId: "prod_1", quantity: 2 },
      { productId: "prod_2", quantity: 1 },
    ]),
    source: "mobile-app",
    promoCode: "SUMMER2025",
  },
});
Metadata Limits:
  • Maximum 50 keys
  • Keys must be strings
  • Values must be strings (use JSON.stringify() for objects/arrays)
  • Total size limit: 5KB

Listing Checkout Sessions

Retrieve all checkout sessions for your account:
const sessions = await loops.checkoutSessions.list({
  limit: 10,
  status: 'completed',
});

Query Parameters

ParameterTypeDescription
limitnumberNumber of sessions to return (1-100, default: 50)
offsetnumberPagination offset (default: 0)
statusstringFilter by status: open, completed, expired, failed
externalCustomerIdstringFilter by customer ID

Getting a Specific Session

Retrieve details of a single checkout session:
const session = await loops.checkoutSessions.get("cs_abc123xyz");

Session Statuses

StatusDescription
openSession is active and awaiting payment
completedPayment was successful
expiredSession expired without payment (default: 1 hour)
failedPayment failed

Handling Post-Payment

After a customer completes or cancels payment, they’ll be redirected based on your payment link configuration.

Success Redirect

Set a successUrl when creating your payment link:
// When creating the payment link
const paymentLink = await loops.paymentLinks.create({
  label: "My Product",
  productIds: ["prod_123"],
  successUrl: "https://yourdomain.com/success",
  cancelUrl: "https://yourdomain.com/cancel",
});

Query Parameters

Loops automatically appends query parameters to your redirect URLs so you can track the session:

Success URL Parameters

When a payment succeeds, customers are redirected to your successUrl with these parameters:
https://yourdomain.com/success?session_id=cs_abc123&status=completed
ParameterDescriptionExample
session_idThe checkout session IDcs_abc123xyz
statusSession statuscompleted

Cancel URL Parameters

When a customer cancels, they’re redirected to your cancelUrl with these parameters:
https://yourdomain.com/cancel?session_id=cs_abc123&status=canceled
ParameterDescriptionExample
session_idThe checkout session IDcs_abc123xyz
statusSession statuscanceled
You can also use the {CHECKOUT_SESSION_ID} placeholder in your URLs, which will be replaced with the actual session ID:
successUrl: "https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}"

Example Redirect Flow

Here’s how the redirect flow works:
  1. You configure the payment link:
    successUrl: "https://myapp.com/success"
    cancelUrl: "https://myapp.com/cancel"
    
  2. Customer completes payment → Redirected to:
    https://myapp.com/success?session_id=cs_abc123xyz&status=completed
    
  3. Customer cancels payment → Redirected to:
    https://myapp.com/cancel?session_id=cs_abc123xyz&status=canceled
    
If your URL already has query parameters, Loops will append the session parameters with &:
successUrl: "https://myapp.com/success?source=checkout"

→ Redirects to:
https://myapp.com/success?source=checkout&session_id=cs_abc123&status=completed

Handling the Redirect

// app/success/page.tsx
export default async function SuccessPage({
  searchParams,
}: {
  searchParams: { session_id: string; status: string };
}) {
  const { session_id: sessionId, status } = searchParams;

  // Fetch session details
  const loops = new Loops({ apiKey: process.env.LOOPS_API_KEY });
  const session = await loops.checkoutSessions.get(sessionId);

  return (
    <div>
      <h1>Payment Successful!</h1>
      <p>Status: {status}</p>
      <p>Order ID: {session.metadata.orderId}</p>
      <p>Amount: ${session.amount / 100}</p>
    </div>
  );
}
Don’t rely solely on redirects for order fulfillment! Always use webhooks to verify payment completion, as users can close the browser before being redirected.

Best Practices

1. Always Use HTTPS

Ensure your success and cancel URLs use HTTPS in production.

2. Implement Webhooks

Use webhooks as your primary method for detecting successful payments. Redirects should only be used for user experience. See our Webhooks Guide for implementation details.

3. Include Customer ID

Always include externalCustomerId to link sessions to your users:
const session = await loops.checkoutSessions.create({
  productId: "prod_abc123xyz",
  externalCustomerId: req.user.id, // Your user ID
  metadata: {
    email: req.user.email,
  },
});

4. Handle Errors Gracefully

try {
  const session = await loops.checkoutSessions.create({
    productId: "prod_abc123xyz",
  });

  return session.url;
} catch (error) {
  console.error('Failed to create checkout:', error);

  // Show user-friendly error message
  return { error: 'Unable to start checkout. Please try again.' };
}

5. Set Appropriate Metadata

Use metadata to store information you’ll need later:
metadata: {
  // ✅ Good - useful identifiers
  orderId: "123",
  userId: "user_456",

  // ❌ Bad - sensitive information
  creditCard: "1234-5678-9012-3456", // Never store PII
  password: "secret123",              // Never store credentials
}

Common Errors & FAQs

This is the most common question! You have two options:
  1. Copy from Dashboard: Products → Click ⋮ menu → “Copy Product ID”
  2. Create via API: Use the product creation endpoint and save the returned ID
See the Getting Your Product ID section above for detailed steps.
{
  "error": "Payment link not found"
}
Solution:
  • Verify the paymentLinkId or productId is correct (check for typos)
  • Make sure the product exists in your dashboard
  • Confirm you’re using the right API key (test vs live mode)

“Do I need to create a new product for each checkout?”

No! Create your products once, then reuse the same product ID for all checkout sessions.
// ❌ Wrong - Don't do this
async function checkout(userId: string) {
  // Creating a new product every time is wasteful
  const product = await loops.products.create({...});
  const session = await loops.checkoutSessions.create({
    productId: product.id
  });
}

// ✅ Correct - Reuse product ID
const PRODUCT_ID = "prod_abc123xyz"; // Created once

async function checkout(userId: string) {
  const session = await loops.checkoutSessions.create({
    productId: PRODUCT_ID // Reuse the same product
  });
}

Invalid Metadata

{
  "error": "Metadata exceeds maximum size"
}
Solution: Reduce metadata size to under 5KB.

Session Expired

If you try to access an expired session, you’ll get a 404. Sessions typically expire after 1 hour of inactivity.

Next Steps

I