API Documentation
Storlaunch is an API-first commerce platform. Everything available in the dashboard is also available via REST API. Base URL: https://storlaunch.forjio.com/api/v1
Getting Started
To get started, create an account and generate an API key from the API Keys dashboard. Use test keys (sk_test_*) for development and live keys (sk_live_*) for production.
sk_live_*Live secret key — production payments
sk_test_*Test secret key — sandbox mode
pk_live_*Live publishable key — public endpoints only
pk_test_*Test publishable key — public endpoints only
Payment Setup
Connect a payment provider to start accepting payments from your customers.
PayPal (Available Now)
Accept international payments in USD via PayPal. Payments go directly to your PayPal account.
- 1Go to Store Settings in your dashboard
- 2Scroll to Payment Settings and enter your PayPal business email
- 3Click Save Changes — you can now accept payments
Platform fee is deducted automatically based on your plan (Free: 2%, Pro: 1.5%, Business: 1%). The rest goes directly to your PayPal account.
Xendit — IDR Local Payments (Coming Soon)
QRIS, OVO, DANA, ShopeePay, bank transfer, credit/debit cards, and retail payments (Indomaret, Alfamart) in Indonesian Rupiah. Currently under review — will be available soon.
Authentication
All API requests require authentication via API key in the Authorization header.
curl https://storlaunch.forjio.com/api/v1/payment/checkout-sessions \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json"Response envelope
Every response follows the same structure:
{
"data": { ... }, // null on error
"error": null, // object on error
"meta": {
"requestId": "req_...",
"timestamp": "2026-04-04T12:00:00.000Z"
}
}Quickstart (CLI)
The fastest way to get started is the @forjio/storlaunch-cli.
# 1. Install the CLI
npm install -g @forjio/storlaunch-cli
# 2. Authenticate
storlaunch auth login --key sk_test_your_key_here
# 3. Create a checkout session
storlaunch payment checkout create \
--amount 99000 \
--currency IDR \
--description "Pro Plan"Code Examples
Use the REST API directly with any HTTP client. Node SDK coming soon.
Create a Checkout Session
Both successUrl and cancelUrl are required. URLs must use HTTPS (or http://localhost for development).
const res = await fetch('https://storlaunch.forjio.com/api/v1/payment/checkout-sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 99000,
currency: 'IDR',
description: 'Pro Plan - Monthly',
customerEmail: 'buyer@example.com',
successUrl: 'https://myapp.com/success',
cancelUrl: 'https://myapp.com/cancel',
}),
});
const { data } = await res.json();
console.log(data.checkoutUrl);
// → https://storlaunch.forjio.com/checkout/cs_01HX...Subscription Billing
// 1. Create a plan
const plan = await fetch('/api/v1/payment/plans', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Pro Monthly',
amount: 99000,
currency: 'IDR',
interval: 'monthly',
trialPeriodDays: 14,
}),
}).then(r => r.json());
// 2. Subscribe a customer
const sub = await fetch('/api/v1/payment/subscriptions', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: 'cust_01HX...',
planId: plan.data.id,
}),
}).then(r => r.json());
console.log(sub.data.status);
// → 'trialing'API Reference
All endpoints use cursor-based pagination. Pass limit and cursor query params. POST/PATCH requests accept an X-Idempotency-Key header.
Payments — Checkout Sessions
/api/v1/payment/checkout-sessionsCreate a checkout session/api/v1/payment/checkout-sessionsList checkout sessions/api/v1/payment/checkout-sessions/:idGet a checkout sessionPayments — Customers
/api/v1/payment/customersCreate a customer/api/v1/payment/customersList customers/api/v1/payment/customers/:idGet a customer/api/v1/payment/customers/:idUpdate a customerPayments — Subscription Plans
/api/v1/payment/plansCreate a plan/api/v1/payment/plansList plans/api/v1/payment/plans/:idGet a plan/api/v1/payment/plans/:idUpdate a plan/api/v1/payment/plans/:idDelete a planPayments — Subscriptions
/api/v1/payment/subscriptionsCreate a subscription/api/v1/payment/subscriptionsList subscriptions/api/v1/payment/subscriptions/:idGet a subscription/api/v1/payment/subscriptions/:idUpdate (pause, resume, change plan)/api/v1/payment/subscriptions/:id/cancelCancel subscriptionPayments — Invoices
/api/v1/payment/invoicesList invoices/api/v1/payment/invoices/:idGet an invoice/api/v1/payment/invoices/:id/pdfDownload invoice PDFPayments — Portal Sessions
/api/v1/payment/portal-sessionsCreate a billing portal sessionStorefront — Products
/api/v1/storefront/productsCreate a product/api/v1/storefront/productsList products/api/v1/storefront/products/:idGet a product/api/v1/storefront/products/:idUpdate a product/api/v1/storefront/products/:idDelete a product/api/v1/storefront/products/:id/filesUpload a product file/api/v1/storefront/products/:id/files/:fileIdDelete a product file/api/v1/storefront/products/:id/ai-generateGenerate AI product pageStorefront — Licenses
/api/v1/storefront/licensesCreate a license key/api/v1/storefront/licensesList license keys/api/v1/storefront/licenses/:idGet a license key/api/v1/storefront/licenses/:id/activateActivate a license/api/v1/storefront/licenses/:id/deactivateDeactivate a license/api/v1/storefront/licenses/:idUpdate license statusStorefront — Deliveries
/api/v1/storefront/deliveriesList deliveries/api/v1/storefront/deliveries/:idGet delivery with download URLsShipping
/api/v1/shipping/couriersList available couriers (dynamic, cached 1h)/api/v1/shipping/areas?q=<query>Search Biteship areas/api/v1/shipping/ratesQuote courier rates for a destination/api/v1/shipping/originGet merchant shipping origin/api/v1/shipping/originUpdate merchant shipping origin/api/v1/shipping/shipmentsList shipments (merchant)/api/v1/shipping/shipments/:idGet shipment with event timeline/api/v1/shipping/shipments/:id/cancelCancel a shipment/api/v1/shipping/shipments/:id/labelGet label PDF URL/api/v1/shipping/track/:waybillIdPublic shipment trackingBuyer Portal (checkout-scoped cookie auth)
/api/v1/checkout/verify-emailIssue OTP to a buyer email/api/v1/checkout/verify-otpVerify OTP, set session cookie/api/v1/checkout/meGet current buyer session info/api/v1/checkout/signoutRevoke session cookie/api/v1/checkout/addressesList buyer's saved addresses/api/v1/checkout/addressesCreate address/api/v1/checkout/addresses/:idUpdate address/api/v1/checkout/addresses/:idDelete address/api/v1/checkout/addresses/:id/defaultPromote address to default/api/v1/checkout/ordersList buyer's orders with a merchant/api/v1/checkout/orders/:idGet order with shipment + deliveries + invoice/api/v1/checkout/subscriptionsList buyer's subscriptions/api/v1/checkout/subscriptions/:idGet subscription detail/api/v1/checkout/subscriptions/:id/cancelCancel subscription (end-of-period or immediate)/api/v1/checkout/invoicesList buyer's invoices/api/v1/checkout/invoices/:id/pdfDownload invoice PDF/api/v1/checkout/profileGet buyer profile/api/v1/checkout/profileUpdate profile name/api/v1/checkout/profile/change-emailStart email change (OTP to new address)/api/v1/checkout/profile/confirm-emailConfirm email change with OTPWebhooks
/api/v1/payment/webhook-endpointsCreate a webhook endpoint/api/v1/payment/webhook-endpointsList webhook endpoints/api/v1/payment/webhook-endpoints/:idGet an endpoint/api/v1/payment/webhook-endpoints/:idUpdate an endpoint/api/v1/payment/webhook-endpoints/:idDelete an endpoint/api/v1/payment/webhook-eventsList webhook events/api/v1/payment/webhook-events/:id/resendResend a failed eventAccount — Custom Domains
/api/v1/account/domainsList custom domains/api/v1/account/domainsAdd a custom domain/api/v1/account/domains/:idGet domain details/api/v1/account/domains/:id/verifyVerify domain DNS/api/v1/account/domains/:idRemove a domainAccount — Audit Log
/api/v1/account/audit-logList audit log entriesUploads
/api/v1/uploads/imageUpload an image (multipart/form-data)Webhooks
Storlaunch sends signed POST requests to your endpoint URLs when events occur. Verify the X-Storlaunch-Signature header using HMAC-SHA256 with your webhook endpoint secret.
import crypto from 'crypto';
// Express.js webhook handler
app.post('/webhooks/storlaunch', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-storlaunch-signature'];
const secret = process.env.WEBHOOK_SECRET;
// Verify HMAC-SHA256 signature
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (signature !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'checkout.completed':
await provisionAccess(event.data.customerId);
break;
case 'subscription.past_due':
await sendPaymentReminder(event.data.customerId);
break;
case 'subscription.canceled':
await revokeAccess(event.data.customerId);
break;
}
res.json({ received: true });
});Webhook Events
checkout.completedcheckout.expiredsubscription.createdsubscription.renewedsubscription.past_duesubscription.canceledsubscription.pausedsubscription.resumedinvoice.createdinvoice.paidinvoice.overdueproduct.purchasedlicense.activatedshipment.status_updatedShipping
Storlaunch integrates with Biteship to aggregate 16 Indonesian couriers (JNE, J&T, SiCepat, POS, TIKI, Wahana, SAP, Ninja, Lion Parcel, AnterAja, ID Express, Lalamove, Grab Express, Deliveree, GoSend, Borzo). Rates are quoted live at checkout; labels are auto-generated on payment; tracking updates arrive via webhook.
1. Setup
In Dashboard → Shipping, configure your pickup origin (address, city, postal code, contact phone) and select which couriers to enable. To enable instant couriers (GoSend, Grab, Lalamove, Borzo, Deliveree) you must provide latitude + longitude.
2. Create a Physical Product
Set type: "physical" on a product and specify weight (grams) and optional length/width/height (cm).
curl -X POST https://api.storlaunch.forjio.com/api/v1/storefront/products \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Classic T-Shirt",
"price": 150000,
"currency": "IDR",
"type": "physical",
"weight": 300,
"length": 30,
"width": 25,
"height": 2
}'3. Quote Rates at Checkout
Public endpoint — no auth required. Returns rates sorted by price, grouped by service type (instant, same_day, overnight, regular, economy).
curl -X POST https://api.storlaunch.forjio.com/api/v1/shipping/rates \
-H "Content-Type: application/json" \
-d '{
"accountSlug": "my-merchant",
"destination": {
"contactName": "Buyer", "contactPhone": "081234567890",
"address": "Jl. Asia Afrika 65", "postalCode": "40111"
},
"items": [{ "name": "T-Shirt", "value": 150000, "weight": 300, "quantity": 1 }]
}'4. Webhook: shipment.status_updated
Fired when Biteship reports a courier status change. Event payload:
{
"shipmentId": "shp_abc123",
"status": "delivered",
"waybillId": "JNE1234567890",
"courierCode": "jne",
"checkoutSessionId": "cs_xyz"
}Shipment Statuses
pendingconfirmedallocatedpicking_uppicked_updropping_offin_transitdeliveredcancelledreturnedfailedInventory & Variants
Physical products with sizes, colors, or other attributes use variants. Each variant is an independently-stocked SKU with its own price delta, cost price, and low-stock threshold. Stock is tracked per variant per warehouse.
1. Create a Variant
curl -X POST https://api.storlaunch.forjio.com/api/v1/inventory/variants \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"productId": "prod_abc123",
"name": "Red / M",
"sku": "TSH-RED-M",
"priceDelta": 0,
"costPrice": 60000,
"lowStockThreshold": 5
}'2. Adjust Stock
Every stock change is recorded with a reason code and audit trail. Reason codes: manual_adjust, refund_restock, transfer_in, transfer_out, damaged, returned_to_supplier, initial_stock, import.
curl -X POST https://api.storlaunch.forjio.com/api/v1/inventory/adjust \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"variantId": "var_abc123",
"warehouseId": "wh_abc123",
"delta": 50,
"reason": "initial_stock",
"note": "PO #2026-04-01"
}'Returns 409 INSUFFICIENT_STOCK if a negative delta would drive quantity below zero.
3. Low-Stock Alerts
curl https://api.storlaunch.forjio.com/api/v1/inventory/low-stock \
-H "Authorization: Bearer sk_live_xxx"
# [ { "variantId", "variantName", "sku", "productName",
# "currentStock", "threshold", "warehouseId", "warehouseName" } ]4. Movement History
Paginated; filter by variantId or warehouseId.
curl "https://api.storlaunch.forjio.com/api/v1/inventory/movements?variantId=var_abc&limit=20" \
-H "Authorization: Bearer sk_live_xxx"5. Bulk CSV Import
Upsert variants and set stock in one call. Columns: sku, name, productSlug, warehouseId, quantity, lowStockThreshold, costPrice.
curl -X POST https://api.storlaunch.forjio.com/api/v1/inventory/import \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"csv": "sku,name,productSlug,warehouseId,quantity\nTSH-RED-M,Red / M,tshirt,wh_abc,25"
}'
# { "imported": 1, "skipped": 0, "errors": [] }Stock Reservations
When a buyer starts checkout, their line items are reserved against the first warehouse with stock. Reservations expire after 30 minutes, or commit on payment success, or release on cancel. The effective stock a merchant sees is available = on_hand − reserved.
Warehouses
Each workspace starts with a default warehouse. Add more locations for multi-site fulfillment. Exactly one warehouse per account is the default — setting isDefault: true on a warehouse atomically demotes the previous default.
Create
curl -X POST https://api.storlaunch.forjio.com/api/v1/inventory/warehouses \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Gudang Utama",
"address": "Jl. Sudirman 1",
"city": "Jakarta",
"postal": "12190",
"phone": "+628111111111",
"isDefault": true
}'List / Update / Archive
# List (ordered: default first)
GET /api/v1/inventory/warehouses
# Update (promote to default)
PATCH /api/v1/inventory/warehouses/wh_abc123
{ "isDefault": true }
# Archive (fails 409 if warehouse is the default)
DELETE /api/v1/inventory/warehouses/wh_abc123Ledger
Every money-moving event posts a signed entry into a per-merchant ledger with a running balance. Credits add to the balance (sales, inbound payments); debits subtract (fees, refunds to buyers, shipping cost, payouts). Entries are idempotent — replaying the same webhook never double-posts because each entry carries a deterministic transactionId (format: sourceType:sourceId:category).
Entry categories
salerefundplatform_feechannel_feeshipping_costshipping_refundpayoutadjustmentAuto-post hooks
You don't write to the ledger directly. The platform posts entries at these points:
- Xendit / PayPal payment success →
salecredit + optionalplatform_fee/channel_feedebits - Shipment created →
shipping_costdebit (Biteship pass-through) - Shipment cancelled →
shipping_refundcredit (planned, Phase B.1) - Manual adjustment API →
adjustmentcredit or debit
1. List entries
curl "https://api.storlaunch.forjio.com/api/v1/ledger/entries?category=sale&limit=20" \
-H "Authorization: Bearer sk_live_xxx"
# data: [{ id, accountId, customerId, transactionId, sourceType, sourceId,
# category, type: 'credit'|'debit', amount, currency,
# description, balanceBefore, balanceAfter, createdAt }, ... ]
# meta: { total, cursor, hasMore }2. Running balance
# Account-wide balance
curl https://api.storlaunch.forjio.com/api/v1/ledger/balance \
-H "Authorization: Bearer sk_live_xxx"
# { "balance": 4500000, "currency": "IDR", "lastEntryAt": "2026-04-14T03:15:00Z" }
# Per-customer balance (AR)
curl https://api.storlaunch.forjio.com/api/v1/ledger/balance/customers/cust_abc \
-H "Authorization: Bearer sk_live_xxx"
# { "customer": { id, email, name },
# "balance": -75000, "entryCount": 4 }3. Manual adjustment
Post a correction, goodwill credit, or write-off. Amounts are positive integers; direction comes from type.
curl -X POST https://api.storlaunch.forjio.com/api/v1/ledger/adjustments \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"type": "credit",
"amount": 10000,
"currency": "IDR",
"description": "Goodwill credit for order #2026-04-55",
"customerId": "cust_abc"
}'
# { "data": { id, balanceAfter, ... } }Financial Reports
Read-only aggregates over the ledger. Three endpoints cover the common accounting needs: a P&L (profit & loss) with revenue and expense breakdown, a Cash Flow with opening/closing balance and category splits, and a flat CSV export for handoff to an external accountant.
All endpoints take from and to (ISO-8601 datetimes, required) plus an optional currency filter. Cash flow opening balance is computed from the sum of signed deltas strictly before from — so backdated entries don't skew the number.
P&L statement
curl "https://api.storlaunch.forjio.com/api/v1/reports/pnl?from=2026-04-01T00:00:00Z&to=2026-04-30T23:59:59Z" \
-H "Authorization: Bearer sk_live_xxx"
# {
# "period": { from, to },
# "currency": "IDR",
# "revenue": { sales, refunds, net },
# "expenses": { platformFees, channelFees, shippingCosts,
# shippingRefunds, total },
# "netProfit": 0,
# "entryCount": 0
# }Cash flow
curl "https://api.storlaunch.forjio.com/api/v1/reports/cash-flow?from=2026-04-01T00:00:00Z&to=2026-04-30T23:59:59Z" \
-H "Authorization: Bearer sk_live_xxx"
# {
# "period": { from, to },
# "currency": "IDR",
# "openingBalance": 0,
# "closingBalance": 0,
# "netChange": 0,
# "inflows": { sale: ..., ... },
# "outflows": { platform_fee: ..., shipping_cost: ..., ... },
# "totalIn": 0,
# "totalOut": 0,
# "entryCount": 0
# }CSV export
Returns text/csv with a Content-Disposition: attachment header. Columns: date, entry_id, transaction_id, source_type, source_id, category, type, debit, credit, currency, balance_after, customer_email, description. Amounts are in the smallest currency unit.
curl -L -o april.csv \
"https://api.storlaunch.forjio.com/api/v1/reports/ledger.csv?from=2026-04-01T00:00:00Z&to=2026-04-30T23:59:59Z" \
-H "Authorization: Bearer sk_live_xxx"
# april.csv saved with all ledger entries for the periodPayouts
Withdraw funds to the merchant's bank account. Manual mode is active while Xendit XenPlatform is in approval — the platform operator wires the funds, then calls mark-paid. When XenPlatform goes live, the same endpoints switch to method=xendit_disbursement and drive the state transitions from webhook callbacks — no API changes on your side.
State machine
pending → in_transit → paid. Terminal side transitions: cancelled (merchant, while pending) or failed. On paid, a ledger payout debit is auto-posted with a deterministic transactionId — replays are safe.
1. Set default bank account
curl -X PATCH https://api.storlaunch.forjio.com/api/v1/payouts/bank-account \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"bankCode": "BCA",
"bankName": "Bank Central Asia",
"bankAccountNumber": "1234567890",
"bankAccountHolder": "Merchant Name"
}'2. Available balance
available = ledgerBalance − locked. Locked is the sum of open (pending + in_transit) payouts, so concurrent requests can't double-spend.
curl https://api.storlaunch.forjio.com/api/v1/payouts/balance \
-H "Authorization: Bearer sk_live_xxx"
# { "ledgerBalance": 5000000, "locked": 500000,
# "available": 4500000, "currency": "IDR" }3. Request a payout
curl -X POST https://api.storlaunch.forjio.com/api/v1/payouts \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency": "IDR",
"note": "April earnings"
}'
# { "data": { "id": "cl...", "status": "pending" } }
# 400 INSUFFICIENT_BALANCE — amount exceeds available
# 400 BANK_ACCOUNT_MISSING — no default bank set and no override passed4. Cancel / track
# Cancel (only while pending)
curl -X POST https://api.storlaunch.forjio.com/api/v1/payouts/PAYOUT_ID/cancel \
-H "Authorization: Bearer sk_live_xxx"
# List with status filter
curl "https://api.storlaunch.forjio.com/api/v1/payouts?status=paid" \
-H "Authorization: Bearer sk_live_xxx"
# Individual payout
curl https://api.storlaunch.forjio.com/api/v1/payouts/PAYOUT_ID \
-H "Authorization: Bearer sk_live_xxx"Bank-detail snapshot
When a payout is created, the destination bank details are snapshotted onto the payout row. Updating the account-level default bank later does NOT rewrite history — past payouts still show the account they were sent to. This keeps the audit trail honest.
Discount Codes
Four code types: percent and fixed apply to the cart subtotal; shipping_percent and shipping_fixed apply to the shipping fee. Cart discounts honor a scope: cart (whole cart), products (specific productIds), or tags (match against Product.tags).
Redemptions are idempotent — replaying a payment webhook never double-posts the discount debit nor double-counts the usage cap.
1. Create a code
Set public: true to advertise the code as a banner on the merchant's storefront product list, product detail, and cart. Leave it false (the default) for private codes you distribute via email, WhatsApp, or influencer drops.
curl -X POST https://api.storlaunch.forjio.com/api/v1/discount-codes \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"code": "SUMMER20",
"type": "percent",
"value": 20,
"currency": "IDR",
"scope": "tags",
"tagFilter": ["summer", "sale"],
"minPurchaseAmount": 100000,
"maxUsesTotal": 500,
"maxUsesPerCustomer": 1,
"expiresAt": "2026-08-31T23:59:59Z",
"public": true
}'2. List applicable public codes (storefront banner)
Read-only, merchant-scoped list of codes marked public. Pass productId on product pages (tags are derived automatically), or subtotal on the cart to filter by minimum purchase.
curl 'https://api.storlaunch.forjio.com/api/v1/storefront/public/acme/discount-codes?productId=prod_x¤cy=IDR'
# [
# {
# "code": "SUMMER20",
# "description": "Lebaran promo 2026",
# "type": "percent",
# "value": 20,
# "currency": "IDR",
# "scope": "tags",
# "minPurchaseAmount": 100000,
# "expiresAt": "2026-08-31T23:59:59Z"
# }
# ]3. Validate (public, dry-run)
Storefront / checkout pages call this before submission to preview the discount. No auth required — the merchantSlug scopes the lookup.
curl -X POST https://api.storlaunch.forjio.com/api/v1/storefront/public/validate-discount \
-H "Content-Type: application/json" \
-d '{
"merchantSlug": "acme",
"code": "SUMMER20",
"subtotal": 500000,
"currency": "IDR",
"shipping": 20000,
"customerId": "cust_abc",
"items": [
{ "productId": "prod_x", "price": 250000, "quantity": 2, "tags": ["summer"] }
]
}'
# {
# "valid": true,
# "discountAmount": 100000, // 20% of eligible line items
# "discountShipping": 0,
# "code": { code, type, description }
# }
# reasons on invalid: NOT_FOUND, INACTIVE, EXPIRED, NOT_YET_ACTIVE,
# CURRENCY_MISMATCH, MIN_PURCHASE, GLOBAL_LIMIT,
# PER_CUSTOMER_LIMIT, SCOPE_MISMATCH4. Apply at checkout session create
curl -X POST https://api.storlaunch.forjio.com/api/v1/payment/checkout-sessions \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency": "IDR",
"successUrl": "https://shop.example.com/thank-you",
"cancelUrl": "https://shop.example.com/cart",
"discountCode": "SUMMER20"
}'
# session.amount is the FINAL amount (subtotal − discount + shipping + insurance)
# discountCodeId / discountCodeValue / discountAmount / discountShipping
# are snapshotted onto the rowLedger effect
On payment success (webhook), in addition to the sale credit and platform_fee / channel_fee debits, the platform posts:
promo_discountdebit — cart discount absorbed by merchant.shipping_discountdebit — shipping discount absorbed by merchant.
P&L now has a Promotions block alongside Revenue and Expenses; the CSV export includes these rows so your accountant can reconcile promo cost against sales.
Cart
Buyers can stage multiple products before checkout. Cart endpoints are buyer-session scoped (HTTP-only cookie via the storefront sign-in flow). Guest carts live inlocalStorageon the storefront and merge into the authed cart on first sign-in.
Authed cart endpoints
Every line-item write accepts an optional note (max 500 chars) — buyers use it to capture per-item fulfillment details (gift wrap, sizing, color preference). The note flows through to CheckoutSession.metadata.shipping.items[].note.
# Get current cart (hydrated with product + variant + per-line totals + note)
GET /api/v1/checkout/cart?accountSlug=acme
# Add an item — productSlug or productId; variantId required for varianted products
POST /api/v1/checkout/cart/items
{
"accountSlug": "acme",
"productSlug": "tshirt",
"variantId": null,
"quantity": 2,
"note": "Please gift-wrap"
}
# Update quantity and/or note independently — omit a field to leave it unchanged
PATCH /api/v1/checkout/cart/items/:id { "accountSlug": "acme", "quantity": 5 }
PATCH /api/v1/checkout/cart/items/:id { "accountSlug": "acme", "note": "Size M in chest" }
DELETE /api/v1/checkout/cart/items/:id ?accountSlug=acme
DELETE /api/v1/checkout/cart ?accountSlug=acme
# Merge a guest cart into the authed cart (called automatically by the storefront on OTP verify)
POST /api/v1/checkout/cart/merge
{
"accountSlug": "acme",
"items": [
{ "productSlug": "tshirt", "variantId": null, "quantity": 1, "note": "Gift wrap" },
{ "productSlug": "mug", "variantId": null, "quantity": 2 }
]
}Multi-item checkout (public)
The storefront cart page POSTs here to convert the cart into a CheckoutSession. No buyer auth required (the existing Customer is created if missing); a discount code + multi-item shipping block can be passed in the same call.
curl -X POST https://api.storlaunch.forjio.com/api/v1/storefront/public/acme/cart-checkout \
-H "Content-Type: application/json" \
-d '{
"email": "buyer@example.com",
"items": [
{ "productSlug": "tshirt", "variantId": "var_red_m", "quantity": 2 },
{ "productSlug": "mug", "quantity": 1 }
],
"discountCode": "SUMMER20"
}'
# { "data": { "sessionId": "cs_...", "checkoutUrl": "https://.../checkout/cs_..." } }
# 400 OUT_OF_STOCK if any line item exceeds available stock
# 400 DISCOUNT_INVALID with reason if discount code rejectedThe legacy single-product endpoint POST /storefront/public/:merchantSlug/:productSlug/checkout still works for back-compat — use it for direct "Buy Now" flows.
SEO & Discoverability
Storefront pages (/s/:merchant and /s/:merchant/:product) are server-rendered with full metadata, JSON-LD, sitemaps, and dynamic Open Graph images. Nothing to configure — ship a published product and it's indexable.
Metadata shape
Every storefront page renders <title>, <meta name="description">, <link rel="canonical">, and Open Graph + Twitter card tags. Images point at the per-page /opengraph-image route below.
JSON-LD structured data
Inline <script type="application/ld+json"> blocks are emitted server-side:
Organization+WebSiteon the merchant rootProduct(name, image, brand, offers.price, offers.priceCurrency, offers.availability, itemCondition) on every product pageBreadcrumbListlinking Home → Merchant → Product
Availability flips to schema.org/OutOfStock automatically when a physical product's variants all report zero stock, so Google Shopping stops showing out-of-stock items.
Sitemaps
# Top-level marketing sitemap
curl https://storlaunch.forjio.com/sitemap.xml
# Per-merchant sitemap — submit this one to Google Search Console
curl https://storlaunch.forjio.com/s/acme/sitemap.xml
# Raw endpoint the Next sitemap builds on
curl https://storlaunch.forjio.com/api/v1/storefront/public/acme/sitemap
# { "data": { "merchantUpdatedAt": "...", "products": [{ "slug": "...", "updatedAt": "..." }] } }Open Graph images
Generated on-demand by Next's ImageResponse. Merchant card: logo + name + description. Product card: merchant name + product name + price + thumbnail. Accessible at:
https://storlaunch.forjio.com/s/acme/opengraph-image
https://storlaunch.forjio.com/s/acme/widget/opengraph-image
# Both return a 1200x630 PNG. Crawlers pick them up via the og:image meta
# tag; you don't have to share the URL directly.Verifying the integration
Use Google's Rich Results Test on a product URL — look for Product and Breadcrumbs detected with 0 errors. For faster iteration locally, the CLI has built-in inspection:
storlaunch sell seo inspect --merchant acme # pass/fail per check
storlaunch sell seo sitemap --merchant acme # URL count + date range
storlaunch sell seo product-schema --merchant acme --product widget # extract JSON-LDPixels & Conversion Tracking
Per-merchant conversion tracking for Meta Ads, Google Ads, and TikTok Ads. Configure your Pixel IDs in /dashboard/marketing/pixels and the storefront injects the scripts + emits standard ecommerce events on every page. Five events out of the box: PageView, ViewContent, AddToCart, InitiateCheckout, Purchase.
Meta Pixel + Conversions API
The Meta Pixel fires client-side on every page. On Purchase, a second event is also posted server-side from the payment webhook via Meta's Conversions API — Meta dedupes the pair using a shared event_id (the CheckoutSession ID). This is how you keep attribution alive on iOS Safari, in aggressively ad-blocked browsers, and when the buyer closes the tab before the pixel fires.
Meta Events Manager → Data Sources → your Pixel
• Pixel ID (15-digit)
• Settings → Conversions API (Generate access token)
• Test Events → Test event code (optional; scoped to Test Events tab)Google Analytics 4 + Google Ads
GA4 events use the standard schema: items[], value, currency, transaction_id. Google Ads conversion tracking piggybacks the gtag — provide the conversion ID + purchase label and Purchase events are reported to Ads automatically.
GA4 Admin → Data Streams → Web → Measurement ID (starts with G-)
Google Ads → Tools → Conversions → Tag setup
• Google tag ID (starts with AW-)
• Purchase conversion → Label (under the conversion action row)TikTok Pixel
TikTok's event names are slightly different from Meta/Google's; Storlaunch maps the internal events automatically: PageView → Pageview, Purchase → CompletePayment, etc.
TikTok Events Manager → Web → your Pixel → Settings → Pixel IDAPI endpoints
# Read config (authed owner — includes CAPI access token)
curl https://api.storlaunch.forjio.com/api/v1/account/pixels \
-H "Authorization: Bearer sk_live_xxx"
# Update — partial patch, only supplied fields change
curl -X PATCH https://api.storlaunch.forjio.com/api/v1/account/pixels \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"metaPixelId":"1234567890",
"metaCapiAccessToken":"EAAB...",
"googleAnalyticsId":"G-ABC123",
"tiktokPixelId":"C123..."
}'
# Public read (storefront SSR) — NEVER returns CAPI secrets
curl https://api.storlaunch.forjio.com/api/v1/storefront/public/acme/pixels
# Test CAPI — fire a real Purchase payload against your own session id
curl -X POST https://api.storlaunch.forjio.com/api/v1/account/pixels/test-capi \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"sessionId":"cs_..."}'Verifying wiring
- Meta: Events Manager → Test Events (set a test event code) → visit storefront → PageView / ViewContent / etc. should show up within 10s.
- Meta dedup: Events Manager → Diagnostics → Deduplication tab should show Purchase events flagged as "Deduplicated" after a real checkout (one from browser, one from CAPI).
- Google: GA4 Realtime report → you as a user → events fire in near real-time.
- TikTok: Events Manager → Test Events with
?ttclid=testadded to the storefront URL.
Abandoned Cart Recovery
Opt-in reminder emails for buyers who add items and don't check out. A cron sweep runs every 15 minutes; eligible carts receive a branded email with a deep link back to the cart. Per-cart-version eligibility means edits reset the delay clock, so actively shopping buyers don't get pinged.
Eligibility
A cart is eligible when all of these are true:
- Merchant config
enabled = true - Cart has at least one item AND an authenticated customer with a verified email
lastActivityAt < now - delayHours(default 4h)- No reminder sent in the last 72 hours for this cart
- Buyer has not opted out (
BuyerEmailPreference.abandonedCartOptOut = false)
Recovery attribution
When a CheckoutSession completes for a customer who has an unrecovered reminder in the last 72 hours, the reminder is marked recovered. Time-window match (not item overlap) — matches industry standard (Shopify, Klaviyo, Mailchimp). The dashboard surfaces recovery rate + recovered revenue over a 30-day window.
Cron endpoint
# External scheduler hits this every 15 minutes
curl -X POST https://api.storlaunch.forjio.com/api/v1/cron/abandoned-cart-sweep \
-H "X-Cron-Secret: $CRON_SECRET"
# { "ok": true, "data": { "processed": 12, "sent": 8, "errors": 0, "skipped": { "optOut": 1, "noEmail": 2, "empty": 0, "claimed": 1 } } }API endpoints (authed merchant)
# Config
GET /api/v1/account/abandoned-cart
PATCH /api/v1/account/abandoned-cart { enabled, delayHours, emailSubject, emailPreview, discountCodeId }
# Dashboard reads
GET /api/v1/account/abandoned-cart/reminders?limit=50
GET /api/v1/account/abandoned-cart/stats?windowDays=30One-click unsubscribe
Reminder emails include a signed token URL and the List-Unsubscribe + List-Unsubscribe-Post: List-Unsubscribe=One-Click headers per RFC 8058, so Gmail and Yahoo render their native unsubscribe button without needing a custom UI. A successful opt-out sets BuyerEmailPreference.abandonedCartOptOut = true scoped to (account, email). Transactional email (OTP, order confirmations, delivery tracking) is unaffected.
Product Feeds for Shopping Ads
Storlaunch auto-generates three product feeds at stable URLs — Google Shopping, Meta Catalog, and TikTok Catalog. Submit one URL per platform and your products show up in image-rich Shopping ads, Advantage+ Catalog campaigns, and TikTok Shop ads. Feeds are rebuilt on every request and cached for 1 hour (ad networks poll daily, so this is plenty).
Feed URLs
# Public (no auth), variant-aware RSS 2.0 with g: namespace
GET https://api.storlaunch.forjio.com/api/v1/storefront/public/:merchantSlug/feeds/google.xml
GET https://api.storlaunch.forjio.com/api/v1/storefront/public/:merchantSlug/feeds/meta.xml
GET https://api.storlaunch.forjio.com/api/v1/storefront/public/:merchantSlug/feeds/tiktok.xml
# Returns 404 when merchant config has enabled=falseFormat
All three feeds use Google's RSS 2.0 spec with the g: namespace — Meta and TikTok both accept it natively. One <item> per variant, grouped by g:item_group_id = product.id. Brand falls back to the merchant's store name when the product doesn't set one. Image URLs are forced absolute (https://...) so ad-network crawlers can fetch them. When a variant has no GTIN, g:identifier_exists=no is emitted so Google approves the product without a valid barcode.
Per-product fields (dashboard → product editor → Product feeds)
gtin— UPC / EAN / ISBN (optional; emits identifier_exists=no when missing)brand— override brand (falls back to merchant name)googleProductCategory— path or numeric ID (per-product override)feedExcluded— omit this product entirely from ad-network feeds
Account config
# Authed (Bearer sk_live_...), default fallbacks for all products
GET /api/v1/account/feeds
PATCH /api/v1/account/feeds { enabled, defaultGoogleProductCategory, includeUnpublished }
# Response:
# {
# "enabled": true,
# "defaultGoogleProductCategory": "Apparel & Accessories > Clothing > Shirts & Tops",
# "includeUnpublished": false,
# "urls": {
# "google": "https://api.storlaunch.forjio.com/api/v1/storefront/public/my-shop/feeds/google.xml",
# "meta": "https://api.storlaunch.forjio.com/api/v1/storefront/public/my-shop/feeds/meta.xml",
# "tiktok": "https://api.storlaunch.forjio.com/api/v1/storefront/public/my-shop/feeds/tiktok.xml"
# }
# }
# Preview your own feed (authed — works even when enabled=false)
GET /api/v1/account/feeds/preview?format=googleSubmission steps
Google Merchant Center
- Products → Feeds → +
- Country + Language
- Name → Scheduled fetch
- Paste the URL, fetch daily
Meta Commerce Manager
- Catalog → Data Sources
- Add items → Use a data feed
- Paste the URL, choose Daily
TikTok Catalog Manager
- Data source → Add source
- Data feed → Paste URL
- Frequency → Daily
Blog CMS
Publish long-form content at /s/:merchant/blog. Every post becomes an indexed URL in your per-merchant sitemap, surfaces as schema.org/BlogPosting JSON-LD, renders OG / Twitter cards, and streams out via an RSS 2.0 feed. Markdown body — simple to write, CI-friendly, image URLs via the existing uploads endpoint.
Resource shape
{
"id": "clr8abc...",
"accountId": "acc_...",
"slug": "how-we-doubled-conversions",
"title": "How we doubled conversions with a simple policy change",
"excerpt": "Short hook shown in the list view + OG card.",
"body": "# Intro\n\nMarkdown body. **Bold**, *italic*, [links](...), \ncode fences, lists, quotes.",
"coverImage": "https://.../uploads/cover.jpg",
"status": "draft" | "published",
"publishedAt": "2026-04-15T10:00:00.000Z",
"authorName": "Bang Adi",
"tags": ["launch", "update"],
"metaTitle": null,
"metaDescription": null,
"createdAt": "...",
"updatedAt": "..."
}Authed API (Bearer sk_live_...)
GET /api/v1/account/blog/posts[?status=draft|published&limit=50]
GET /api/v1/account/blog/posts/:id
POST /api/v1/account/blog/posts { title, body, slug?, excerpt?, coverImage?,
authorName?, tags?, metaTitle?,
metaDescription?, status? }
PATCH /api/v1/account/blog/posts/:id — partial update (any field above)
DELETE /api/v1/account/blog/posts/:id — permanent
POST /api/v1/account/blog/posts/:id/publish — stamps publishedAt if unset
POST /api/v1/account/blog/posts/:id/unpublish — keeps publishedAt historyPublic (storefront)
GET /api/v1/storefront/public/:merchantSlug/blog[?limit=20]
→ { merchant: { slug, name }, posts: [{ id, slug, title, excerpt,
coverImage, publishedAt, authorName, tags }] }
GET /api/v1/storefront/public/:merchantSlug/blog/:postSlug
→ { merchant, post } — only returns published posts with publishedAt <= now
GET /api/v1/storefront/public/:merchantSlug/blog/rss.xml
→ RSS 2.0 feed (up to 50 newest published posts)
Discoverable from /s/:merchant/blog via <link rel="alternate" type="application/rss+xml">Plan limits
Tier-limit enforcement only fires on publish. Drafts are unlimited on all tiers, so you can write in peace. Free tier caps published posts at 5 — POST /publish returns 403 PLAN_LIMIT_EXCEEDED once the count is reached. Pro and Business are unlimited.
Tips
- Leave
slugempty to auto-generate from the title; server resolves collisions by appending-2,-3, etc. - Cover image: upload via
POST /api/v1/uploads/image(auto-compressed), paste the returned URL. - Markdown is HTML-sanitized on render (sanitize-html) — raw
<script>tags stripped before the browser sees them. - OG metadata falls back:
metaTitle ?? title,metaDescription ?? excerpt, image fromcoverImage.
Referral Program
Per-merchant referral program (Phase F.5). Rewards reuse the existing DiscountCode as the redemption vehicle, so ledger + discount-usage reports pick them up automatically. Pro tier and up.
Endpoints
GET /api/v1/account/referrals — fetch the program config (returns defaults envelope when unconfigured).PUT /api/v1/account/referrals — upsert. Body: enabled, rewardType, referrerValue, refereeValue, currency, minPurchaseAmount, rewardExpiryDays, attributionWindowDays, maxRewardsPerReferrer, programTerms.GET /api/v1/account/referrals/links — paginated top referrers with per-link stats.GET /api/v1/account/referrals/attributions — attribution lifecycle log. Optional ?status=pending|rewarded|voided|expired.GET /api/v1/account/referrals/stats — aggregated clicks, signups, rewards, attributed revenue, conversion rate.GET /api/v1/checkout/referrals/my-link — (buyer-authed) shareable link + per-buyer stats.GET /api/v1/checkout/referrals/my-rewards — (buyer-authed) reward codes earned via referrals.GET /api/v1/storefront/public/:slug/r/:code — public capture. Sets cookie storlaunch_ref_<accountId>, bumps click counter, 302s to the storefront home.POST /api/v1/storefront/public/:slug/referral/capture — same as above but no redirect (used client-side for deep-linked ?ref= pages).POST /api/v1/cron/referral-expiry-sweep — flips pending attributions past their window to expired. Auth via X-Cron-Secret.Lifecycle
- Buyer clicks a
/s/<slug>/r/<code>URL → cookie set, click counter bumps. - New buyer signs up via OTP →
ReferralAttributionrow created with status pending (self-referral + email-match guards applied). - Buyer completes a checkout → session stamped with the attribution id.
- Payment webhook fires → pending flips to rewarded, both sides get auto-issued
DiscountCodes (max 1 use each, TTL from program config). - Refund → attribution voided. Unused codes deactivate; already-redeemed codes are preserved and the void reason records
refunded_after_usefor reporting. - No conversion within
attributionWindowDays→ swept to expired.
Notes
- Auto-issued codes carry
source="referral_referrer"orsource="referral_referee"and are excluded from the storefront's public applicable-codes list so personal rewards don't leak to other buyers. - Free-tier merchants can't enable the program (anti-spam). Returns 403
PLAN_UPGRADE_REQUIRED. - Attribution is scoped per merchant: a buyer on Shop A can never redeem a code issued by Shop B's program.
- Rewards fire on payment-completed (not post-fulfillment). Webhook retries are idempotent via the
attribution.status === 'pending'gate.
Error Codes
| Code | HTTP | Description |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | No API key or JWT provided |
INVALID_API_KEY | 401 | API key is invalid or revoked |
INSUFFICIENT_PERMISSIONS | 403 | Key doesn't have access |
PLAN_LIMIT_EXCEEDED | 403 | Account has hit a tier limit |
RATE_LIMIT_EXCEEDED | 429 | Too many requests |
RESOURCE_NOT_FOUND | 404 | Entity not found |
VALIDATION_ERROR | 400 | Request body validation failed |
IDEMPOTENCY_CONFLICT | 409 | Same idempotency key, different params |
PAYMENT_FAILED | 422 | Payment provider rejected the charge |
INTERNAL_ERROR | 500 | Unexpected server error |
Ready to integrate?
Create your account and get your API keys in under 2 minutes.
Get API Keys