Putiikkipalvelu Docs
SDK

Checkout

Process payments with Stripe and Paytrail payment providers

Overview

The checkout resource handles payment processing with two supported providers:

  • Stripe - International card payments and digital wallets
  • Paytrail - Finnish payment methods (banks, mobile pay, cards)

Both methods validate cart contents, reserve inventory, create an order, and return payment URLs/providers.

Checkout Flow:

  1. Cart is validated (stock, prices)
  2. Order is created with PENDING status
  3. Inventory is reserved
  4. Payment session is created
  5. User completes payment
  6. Webhook updates order to PAID

Methods

checkout.stripe(params, options?)

Create a Stripe checkout session and get a redirect URL.

const { url } = await storefront.checkout.stripe({
  customerData: {
    first_name: "Matti",
    last_name: "Meikäläinen",
    email: "matti@example.fi",
    address: "Mannerheimintie 1",
    postal_code: "00100",
    city: "Helsinki",
    phone: "+358401234567"
  },
  shipmentMethod: {
    shipmentMethodId: "ship_123",
    pickupId: null
  },
  orderId: "order_abc123",
  successUrl: "https://mystore.com/payment/success/order_abc123",
  cancelUrl: "https://mystore.com/payment/cancel/order_abc123"
}, {
  cartId: "cart_xyz",     // For guest users
  sessionId: "sess_123"   // For logged-in users
});

// Redirect to Stripe
window.location.href = url;

Parameters:

ParamTypeRequiredDescription
params.customerDataCheckoutCustomerDataYesCustomer information
params.shipmentMethodCheckoutShipmentMethod | nullYesSelected shipping method
params.orderIdstringYesUnique order ID (generate with randomUUID())
params.successUrlstringYesRedirect URL on success
params.cancelUrlstringYesRedirect URL on cancel
options.cartIdstringNoCart ID for guest users
options.sessionIdstringNoSession ID for logged-in users

Returns: Promise<StripeCheckoutResponse>

interface StripeCheckoutResponse {
  url: string;  // Stripe checkout URL
}

Throws:

  • ValidationError - Empty cart, product not found, insufficient inventory

checkout.paytrail(params, options?)

Create a Paytrail checkout session and get payment providers.

const response = await storefront.checkout.paytrail({
  customerData: {
    first_name: "Matti",
    last_name: "Meikäläinen",
    email: "matti@example.fi",
    address: "Mannerheimintie 1",
    postal_code: "00100",
    city: "Helsinki",
    phone: "+358401234567"
  },
  shipmentMethod: {
    shipmentMethodId: "ship_123",
    pickupId: "loc_456"  // For pickup points
  },
  orderId: "order_abc123",
  successUrl: "https://mystore.com/payment/success/order_abc123",
  cancelUrl: "https://mystore.com/payment/cancel/order_abc123"
}, {
  cartId: "cart_xyz"
});

// Display payment providers grouped by type
const banks = response.providers.filter(p => p.group === "bank");
const mobile = response.providers.filter(p => p.group === "mobile");

Parameters:

Same as checkout.stripe().

Returns: Promise<PaytrailCheckoutResponse>

interface PaytrailCheckoutResponse {
  transactionId: string;      // Paytrail transaction ID
  href: string;               // Direct payment URL
  reference: string;          // Payment reference
  terms: string;              // Terms URL
  groups: PaytrailGroup[];    // Payment method groups
  providers: PaytrailProvider[];  // Available providers
  customProviders: Record<string, unknown>;
}

interface PaytrailGroup {
  id: string;     // "bank", "mobile", "creditcard"
  name: string;   // Display name
  icon: string;   // Icon URL
  svg: string;    // SVG icon
}

interface PaytrailProvider {
  id: string;           // Provider ID (e.g., "nordea")
  name: string;         // Display name
  group: string;        // Group ID
  icon: string;         // Icon URL
  svg: string;          // SVG icon
  url: string;          // Form submission URL
  parameters: Array<{   // Hidden form fields
    name: string;
    value: string;
  }>;
}

Throws:

  • ValidationError - Empty cart, product not found, insufficient inventory

Usage Examples

Stripe Checkout (Server Action)

// lib/actions/checkout.ts
"use server";

import { storefront } from "@/lib/storefront";
import { cookies } from "next/headers";
import { randomUUID } from "crypto";
import type { CheckoutCustomerData, CheckoutShipmentMethod } from "@putiikkipalvelu/storefront-sdk";

export async function createStripeCheckout(
  customerData: CheckoutCustomerData,
  shipmentMethod: CheckoutShipmentMethod | null
) {
  const cookieStore = await cookies();
  const cartId = cookieStore.get("cart-id")?.value;
  const sessionId = cookieStore.get("session-id")?.value;
  const orderId = randomUUID();

  const { url } = await storefront.checkout.stripe({
    customerData,
    shipmentMethod,
    orderId,
    successUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success/${orderId}`,
    cancelUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/cancel/${orderId}`,
  }, {
    cartId,
    sessionId,
  });

  return { url };
}

Paytrail Provider Selection

// components/PaytrailProviders.tsx
"use client";

import type { PaytrailCheckoutResponse } from "@putiikkipalvelu/storefront-sdk";

export function PaytrailProviders({ data }: { data: PaytrailCheckoutResponse }) {
  return (
    <div>
      {data.groups.map(group => (
        <div key={group.id}>
          <h3>{group.name}</h3>
          <div className="grid grid-cols-4 gap-4">
            {data.providers
              .filter(p => p.group === group.id)
              .map(provider => (
                <form
                  key={provider.id}
                  method="POST"
                  action={provider.url}
                >
                  {provider.parameters.map(param => (
                    <input
                      key={param.name}
                      type="hidden"
                      name={param.name}
                      value={param.value}
                    />
                  ))}
                  <button type="submit">
                    <img src={provider.svg} alt={provider.name} />
                  </button>
                </form>
              ))}
          </div>
        </div>
      ))}
    </div>
  );
}

Complete Checkout Flow

// app/checkout/page.tsx
import { storefront } from "@/lib/storefront";
import { cookies } from "next/headers";
import { randomUUID } from "crypto";

async function handleCheckout(formData: FormData) {
  "use server";

  const cookieStore = await cookies();
  const cartId = cookieStore.get("cart-id")?.value;
  const orderId = randomUUID();

  const customerData = {
    first_name: formData.get("firstName") as string,
    last_name: formData.get("lastName") as string,
    email: formData.get("email") as string,
    address: formData.get("address") as string,
    postal_code: formData.get("postalCode") as string,
    city: formData.get("city") as string,
    phone: formData.get("phone") as string,
  };

  const shipmentMethod = {
    shipmentMethodId: formData.get("shipmentMethodId") as string,
    pickupId: formData.get("pickupId") as string | null,
  };

  const provider = formData.get("provider") as string;

  if (provider === "stripe") {
    const { url } = await storefront.checkout.stripe({
      customerData,
      shipmentMethod,
      orderId,
      successUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success/${orderId}`,
      cancelUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/cancel/${orderId}`,
    }, { cartId });

    redirect(url);
  } else {
    const response = await storefront.checkout.paytrail({
      customerData,
      shipmentMethod,
      orderId,
      successUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success/${orderId}`,
      cancelUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/cancel/${orderId}`,
    }, { cartId });

    // Return Paytrail providers for selection
    return response;
  }
}

Error Handling

import { ValidationError, StorefrontError } from "@putiikkipalvelu/storefront-sdk";

try {
  const { url } = await storefront.checkout.stripe(params, options);
  redirect(url);
} catch (error) {
  if (error instanceof ValidationError) {
    // Cart/inventory issues
    if (error.message.includes("EMPTY_CART")) {
      return { error: "Your cart is empty" };
    }
    if (error.message.includes("INSUFFICIENT_INVENTORY")) {
      return { error: "Some items are out of stock" };
    }
    return { error: "Checkout validation failed" };
  }

  if (error instanceof StorefrontError) {
    console.error("Checkout failed:", error.message);
    return { error: "Payment processing failed" };
  }

  throw error;
}

TypeScript Types

// Customer data for checkout
interface CheckoutCustomerData {
  first_name: string;
  last_name: string;
  email: string;
  address: string;
  postal_code: string;  // 5-digit Finnish postal code
  city: string;
  phone: string;
}

// Shipment method selection
interface CheckoutShipmentMethod {
  shipmentMethodId: string;
  pickupId: string | null;  // For pickup points
}

// Checkout parameters
interface CheckoutParams {
  customerData: CheckoutCustomerData;
  shipmentMethod: CheckoutShipmentMethod | null;
  orderId: string;
  successUrl: string;
  cancelUrl: string;
}

// Checkout options (session context)
interface CheckoutOptions {
  cartId?: string;      // Guest cart ID
  sessionId?: string;   // Logged-in user session
}

Important Notes

Order ID Generation

Always generate a unique order ID on the client before calling checkout:

import { randomUUID } from "crypto";
const orderId = randomUUID();

Success/Cancel URL Pattern

Include the order ID in your URLs for easy order lookup:

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
const successUrl = `${baseUrl}/payment/success/${orderId}`;
const cancelUrl = `${baseUrl}/payment/cancel/${orderId}`;

Cart Context Headers

The SDK automatically handles cart context via options:

  • cartId - For guest users (from cookie)
  • sessionId - For logged-in users (from session cookie)

Pass at least one for cart identification.

Webhook Handling

After successful payment:

  1. Stripe/Paytrail sends webhook to your backend
  2. Order status updates from PENDING to PAID
  3. Cart is cleared
  4. User is redirected to successUrl

The order will be visible at /order/{orderId} after payment webhook processes.

On this page