Use this file to discover all available pages before exploring further.
Checkout sessions give you programmatic control over the payment flow. Unlike static payment links, checkout sessions are generated dynamically, allowing you to:
Pass custom tracking IDs for each payment
Pre-fill customer information like email
Set dynamic success URLs based on your app’s context
// app/page.tsx'use client'; // Optional: CreemCheckout also works in Server Componentsimport { CreemCheckout } from '@creem_io/nextjs';export function CheckoutButton() { return ( <CreemCheckout productId="prod_YOUR_PRODUCT_ID" referenceId="user_123" // Optional: Track this payment in your system > <button>Buy Now</button> </CreemCheckout> );}
The CreemCheckout component automatically handles the checkout session creation and redirects the user to the payment page.
Next.js SDK Documentation
Explore advanced features, server components, and webhook handling.
The TypeScript SDK provides full type-safety and works with any JavaScript framework.
The signature query parameter allows you to verify that the redirect came from Creem. This prevents malicious users from spoofing successful payment redirects.
The signature is a SHA-256 hex digest of the redirect parameters joined with |, with salt={apiKey} appended at the end. Parameters appear in the order they arrive in the redirect URL (not alphabetically sorted), and null or empty values are excluded — only include parameters that have actual values.
Then hashed with SHA-256 and hex-encoded to produce the signature value.
TypeScript
Python
Go
import * as crypto from 'crypto';interface RedirectParams { request_id?: string | null; checkout_id: string; order_id: string | null; customer_id: string | null; subscription_id: string | null; product_id: string; signature: string;}function verifyRedirectSignature( params: RedirectParams, apiKey: string,): boolean { const { signature, ...rest } = params; // Keep insertion order; exclude null/undefined/empty values. const data = Object.entries(rest) .filter(([, value]) => value !== null && value !== undefined && value !== '') .map(([key, value]) => `${key}=${value}`) .concat(`salt=${apiKey}`) .join('|'); const expectedSignature = crypto .createHash('sha256') .update(data) .digest('hex'); return signature === expectedSignature;}// Usage in your success pageexport async function GET(request: Request) { const url = new URL(request.url); // Read fields in the order they appear in the redirect URL. const params: RedirectParams = { request_id: url.searchParams.get('request_id'), checkout_id: url.searchParams.get('checkout_id')!, order_id: url.searchParams.get('order_id'), customer_id: url.searchParams.get('customer_id'), subscription_id: url.searchParams.get('subscription_id'), product_id: url.searchParams.get('product_id')!, signature: url.searchParams.get('signature')!, }; const isValid = verifyRedirectSignature(params, process.env.CREEM_API_KEY!); if (!isValid) { return new Response('Invalid signature', { status: 401 }); } // Proceed with success page...}
import hashlibimport hmacfrom urllib.parse import urlparse, parse_qsldef verify_redirect_signature(query_string: str, api_key: str) -> bool: """ Verify the redirect signature from Creem checkout. Args: query_string: The raw query string from the redirect URL. api_key: Your Creem API key. Returns: True if signature is valid, False otherwise. """ # parse_qsl preserves the order parameters appear in the URL. pairs = parse_qsl(query_string, keep_blank_values=False) signature = '' parts = [] for key, value in pairs: if key == 'signature': signature = value continue if value in (None, '', 'null'): continue parts.append(f'{key}={value}') parts.append(f'salt={api_key}') data = '|'.join(parts) expected_signature = hashlib.sha256(data.encode()).hexdigest() return hmac.compare_digest(signature, expected_signature)# Usage in Flask@app.route('/success')def success(): if not verify_redirect_signature(request.query_string.decode(), os.environ['CREEM_API_KEY']): return 'Invalid signature', 401 # Proceed with success page...
package mainimport ( "crypto/sha256" "crypto/subtle" "encoding/hex" "net/url" "strings")// verifyRedirectSignature checks the redirect signature using the raw query// string so parameter order matches what Creem signed.func verifyRedirectSignature(rawQuery, apiKey string) bool { var signature string var parts []string for _, pair := range strings.Split(rawQuery, "&") { if pair == "" { continue } key, value, _ := strings.Cut(pair, "=") decodedKey, err := url.QueryUnescape(key) if err != nil { return false } decodedValue, err := url.QueryUnescape(value) if err != nil { return false } if decodedKey == "signature" { signature = decodedValue continue } if decodedValue == "" || decodedValue == "null" { continue } parts = append(parts, decodedKey+"="+decodedValue) } parts = append(parts, "salt="+apiKey) data := strings.Join(parts, "|") sum := sha256.Sum256([]byte(data)) expected := hex.EncodeToString(sum[:]) return subtle.ConstantTimeCompare([]byte(signature), []byte(expected)) == 1}
Important: Parameters with null or empty values (like order_id for subscription-only checkouts, or subscription_id for one-time payments) must be excluded from the signed string. Including them as "order_id=null" will cause verification to fail.
// Email is automatically set from the authenticated userconst { data } = await authClient.creem.createCheckout({ productId: 'prod_YOUR_PRODUCT_ID', customer: { email: 'user@example.com', // Optional: if you want to overwrite the session },});
The Better Auth integration automatically uses the authenticated user’s email.