Stripe Checkout Session
Create a Stripe Checkout session and return the payment URL to your frontend.
Overview
This workflow exposes an API endpoint that receives product details (name, price, quantity), validates all inputs, creates a Stripe Checkout session via the Stripe API, and returns the checkout URL. Your frontend redirects the customer to that URL to complete payment.
This is the most common Stripe integration pattern — no need to build a custom payment form. The workflow includes input validation, redirect URL allowlisting, Stripe error handling, and secret management.
Prerequisites
- A Stripe account (test mode is fine for development)
- Your Stripe Secret Key (from the Stripe Dashboard → Developers → API Keys)
- A success and cancel URL on your domain
What You'll Build
A single API endpoint that:
- Receives product name, price (in cents), and quantity
- Validates all inputs (type, range, length)
- Validates redirect URLs against an allowed domain list
- Creates a Stripe Checkout session with those line items
- Handles Stripe API errors gracefully
- Returns the checkout URL for the customer to pay
Endpoint: POST /api/v1/YOUR_ID/create-checkout
Request body:
{
"product_name": "Pro Plan",
"price": 2999,
"quantity": 1,
"success_url": "https://yourdomain.com/success",
"cancel_url": "https://yourdomain.com/cancel"
}
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"session_id": "cs_test_..."
}
Vault
Store your Stripe key in Settings → Vault:
| Key | Value |
|---|---|
stripe_secret_key |
Your Stripe secret key (sk_test_... or sk_live_...) |
Never hardcode the Stripe secret key in the workflow. Use
{{secrets.stripe_secret_key}}to reference it securely.
Workflow Nodes
1. Flow Start - API Endpoint
| Setting | Value |
|---|---|
| Trigger Type | API |
| Method | POST |
| Custom Path | create-checkout |
| CORS Origins | Your domain(s) (e.g. https://yourdomain.com) |
| Rate Limit | 10/min |
| Timeout | 30s |
CORS restricts which domains can call this endpoint from a browser. Without it, any website could create checkout sessions on your Stripe account. The rate limit of 10/min prevents abuse — lower it further if your traffic is low.
2. Code - Validate & Build Stripe Payload
Validates all inputs and constructs the request body for the Stripe Checkout Sessions API. Stripe expects application/x-www-form-urlencoded format.
Output variable: codeJs1
var productName = variables.product_name || "";
var price = parseInt(variables.price);
var quantity = parseInt(variables.quantity);
var successUrl = variables.success_url || "";
var cancelUrl = variables.cancel_url || "";
// Validate product name
if (!productName || productName.length < 1 || productName.length > 200) {
throw new Error("Invalid product name");
}
// Validate price (minimum $0.50 = 50 cents, max $100,000)
if (!price || price < 50 || price > 10000000) {
throw new Error("Invalid price: must be between 50 and 10000000 cents");
}
// Validate quantity
if (!quantity || quantity < 1 || quantity > 99) {
throw new Error("Invalid quantity: must be between 1 and 99");
}
// Validate redirect URLs against allowed domains
var allowedDomains = ["https://yourdomain.com"];
var successAllowed = false;
var cancelAllowed = false;
for (var i = 0; i < allowedDomains.length; i++) {
if (successUrl.indexOf(allowedDomains[i]) === 0) successAllowed = true;
if (cancelUrl.indexOf(allowedDomains[i]) === 0) cancelAllowed = true;
}
if (!successAllowed || !cancelAllowed) {
throw new Error("Invalid redirect URL: must match allowed domain");
}
var params = [
"payment_method_types[0]=card",
"line_items[0][price_data][currency]=usd",
"line_items[0][price_data][product_data][name]=" + encodeURIComponent(productName),
"line_items[0][price_data][unit_amount]=" + price,
"line_items[0][quantity]=" + quantity,
"mode=payment",
"success_url=" + encodeURIComponent(successUrl),
"cancel_url=" + encodeURIComponent(cancelUrl)
];
({
stripeBody: params.join("&")
});
Input validation rules
| Field | Type | Constraints |
|---|---|---|
product_name |
string | 1–200 characters |
price |
integer | 50–10,000,000 (cents). Stripe minimum is $0.50 |
quantity |
integer | 1–99 |
success_url |
string | Must start with an allowed domain |
cancel_url |
string | Must start with an allowed domain |
The redirect URL validation is critical. Without it, an attacker could set
success_urlto a phishing site that mimics your payment confirmation page. TheallowedDomainsarray should contain only your domain(s).
Understanding the Stripe payload
| Field | Description |
|---|---|
payment_method_types[0] |
Payment method — card for credit/debit cards |
line_items[0][price_data][currency] |
Currency code (e.g., usd, eur) |
line_items[0][price_data][product_data][name] |
Product name shown on checkout |
line_items[0][price_data][unit_amount] |
Price in cents (2999 = $29.99) |
line_items[0][quantity] |
Number of items |
mode |
payment for one-time, subscription for recurring |
success_url |
Where to redirect after successful payment |
cancel_url |
Where to redirect if customer cancels |
Stripe prices are always in the smallest currency unit. For USD that's cents, for EUR it's cents, for JPY it's yen (no decimals).
3. HTTP Request - Stripe API
Creates the Checkout session via the Stripe API.
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://api.stripe.com/v1/checkout/sessions |
Headers:
| Key | Value |
|---|---|
| Authorization | Bearer {{secrets.stripe_secret_key}} |
| Content-Type | application/x-www-form-urlencoded |
Body: {{codeJs1.stripeBody}}
Output variable: httpRequest
The Stripe secret key is stored as a secret and referenced with
{{secrets.stripe_secret_key}}. Never hardcode API keys in workflow nodes — if the workflow JSON is exported or shared, the key would be exposed.
4. Code - Extract Checkout URL
Extracts the checkout URL and session ID from the Stripe response, with error handling for failed API calls.
Output variable: codeJs2
var resp = variables.httpRequest;
if (resp.status !== 200) {
throw new Error("Stripe error " + resp.status + ": " + (resp.data.error ? resp.data.error.message : resp.statusText));
}
var session = resp.data;
({
checkout_url: session.url,
session_id: session.id
});
Without this check, a Stripe error (invalid key, bad parameters, rate limit) would cause the next node to crash with an unhelpful error. This surfaces the actual Stripe error message.
5. Simple Output - Return URL
Returns the checkout URL to the caller.
| Setting | Value |
|---|---|
| Status | 200 |
| Type | JSON |
| Output | {{codeJs2}} |
Frontend Integration
Once you have the endpoint, redirect the customer from your frontend:
async function checkout() {
const res = await fetch('https://your-domain.com/api/v1/YOUR_ID/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
product_name: 'Pro Plan',
price: 2999,
quantity: 1,
success_url: window.location.origin + '/success',
cancel_url: window.location.origin + '/cancel'
})
});
if (!res.ok) {
const error = await res.text();
console.error('Checkout error:', error);
return;
}
const data = await res.json();
window.location.href = data.checkout_url;
}
Always handle errors on the frontend. If validation fails or Stripe returns an error, show a user-friendly message instead of silently failing.
Test vs Live Mode
| Mode | Secret Key Prefix | Cards |
|---|---|---|
| Test | sk_test_... |
Use 4242 4242 4242 4242 with any future date and CVC |
| Live | sk_live_... |
Real cards are charged |
Always develop and test with test mode keys. Switch to live keys only when deploying to production.
Testing
Using curl
curl -X POST https://your-domain.com/api/v1/YOUR_ID/create-checkout \
-H "Content-Type: application/json" \
-d '{
"product_name": "Pro Plan",
"price": 2999,
"quantity": 1,
"success_url": "https://yourdomain.com/success",
"cancel_url": "https://yourdomain.com/cancel"
}'
Expected:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"session_id": "cs_test_..."
}
Open the checkout_url in your browser — you should see the Stripe Checkout page with your product.
Testing validation
| Test | Input | Expected |
|---|---|---|
| Missing product name | "product_name": "" |
Error: Invalid product name |
| Price too low | "price": 10 |
Error: Invalid price |
| Price too high | "price": 99999999 |
Error: Invalid price |
| Quantity zero | "quantity": 0 |
Error: Invalid quantity |
| Bad redirect URL | "success_url": "https://evil.com/phish" |
Error: Invalid redirect URL |
| Valid request | All fields correct | 200 with checkout URL |
Using Ubex Manual Mode
- Open the workflow in the Ubex editor
- Click Manual Run
- Enter the test input:
{
"product_name": "Pro Plan",
"price": 2999,
"quantity": 1,
"success_url": "https://yourdomain.com/success",
"cancel_url": "https://yourdomain.com/cancel"
}
- Step through each node and verify you get a valid checkout URL back.
Test card numbers
| Card | Number |
|---|---|
| Visa (success) | 4242 4242 4242 4242 |
| Visa (decline) | 4000 0000 0000 0002 |
| 3D Secure | 4000 0025 0000 3155 |
| Insufficient funds | 4000 0000 0000 9995 |
Use any future expiry date and any 3-digit CVC.
What to verify
| Check | Expected |
|---|---|
checkout_url |
Starts with https://checkout.stripe.com/ |
session_id |
Starts with cs_test_ (test mode) |
| Checkout page | Shows your product name and price |
| Success redirect | After payment, redirects to your success URL |
| Invalid price rejected | Error thrown for out-of-range values |
| Bad URL rejected | Error thrown for non-allowed domains |
| Stripe error surfaced | Bad API key returns readable error message |
Security Checklist
| Control | Status |
|---|---|
| POST only endpoint | ✅ |
| Stripe secret key stored as secret | ✅ |
| CORS restricted to your domain(s) | ✅ |
| Rate limiting (10/min) | ✅ |
| Input validation (price, quantity, name) | ✅ |
| Redirect URL allowlist | ✅ |
| Stripe API error handling | ✅ |
| Stripe Checkout hosted page (PCI compliant) | ✅ |
| Price in smallest currency unit (prevents rounding errors) | ✅ |
| Test vs live mode separation | ✅ |
| No real secrets in tutorial JSON | ✅ |