API v1

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.

  1. 1Go to Store Settings in your dashboard
  2. 2Scroll to Payment Settings and enter your PayPal business email
  3. 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.

bash
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.

bash
# 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).

typescript
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

typescript
// 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

POST/api/v1/payment/checkout-sessions
GET/api/v1/payment/checkout-sessions
GET/api/v1/payment/checkout-sessions/:id

Payments — Customers

POST/api/v1/payment/customers
GET/api/v1/payment/customers
GET/api/v1/payment/customers/:id
PATCH/api/v1/payment/customers/:id

Payments — Subscription Plans

POST/api/v1/payment/plans
GET/api/v1/payment/plans
GET/api/v1/payment/plans/:id
PATCH/api/v1/payment/plans/:id
DELETE/api/v1/payment/plans/:id

Payments — Subscriptions

POST/api/v1/payment/subscriptions
GET/api/v1/payment/subscriptions
GET/api/v1/payment/subscriptions/:id
PATCH/api/v1/payment/subscriptions/:id
POST/api/v1/payment/subscriptions/:id/cancel

Payments — Invoices

GET/api/v1/payment/invoices
GET/api/v1/payment/invoices/:id
GET/api/v1/payment/invoices/:id/pdf

Payments — Portal Sessions

POST/api/v1/payment/portal-sessions

Storefront — Products

POST/api/v1/storefront/products
GET/api/v1/storefront/products
GET/api/v1/storefront/products/:id
PATCH/api/v1/storefront/products/:id
DELETE/api/v1/storefront/products/:id
POST/api/v1/storefront/products/:id/files
DELETE/api/v1/storefront/products/:id/files/:fileId
POST/api/v1/storefront/products/:id/ai-generate

Storefront — Licenses

POST/api/v1/storefront/licenses
GET/api/v1/storefront/licenses
GET/api/v1/storefront/licenses/:id
POST/api/v1/storefront/licenses/:id/activate
POST/api/v1/storefront/licenses/:id/deactivate
PATCH/api/v1/storefront/licenses/:id

Storefront — Deliveries

GET/api/v1/storefront/deliveries
GET/api/v1/storefront/deliveries/:id

Shipping

GET/api/v1/shipping/couriers
GET/api/v1/shipping/areas?q=<query>
POST/api/v1/shipping/rates
GET/api/v1/shipping/origin
PATCH/api/v1/shipping/origin
GET/api/v1/shipping/shipments
GET/api/v1/shipping/shipments/:id
POST/api/v1/shipping/shipments/:id/cancel
GET/api/v1/shipping/shipments/:id/label
GET/api/v1/shipping/track/:waybillId

Buyer Portal (checkout-scoped cookie auth)

POST/api/v1/checkout/verify-email
POST/api/v1/checkout/verify-otp
GET/api/v1/checkout/me
POST/api/v1/checkout/signout
GET/api/v1/checkout/addresses
POST/api/v1/checkout/addresses
PATCH/api/v1/checkout/addresses/:id
DELETE/api/v1/checkout/addresses/:id
POST/api/v1/checkout/addresses/:id/default
GET/api/v1/checkout/orders
GET/api/v1/checkout/orders/:id
GET/api/v1/checkout/subscriptions
GET/api/v1/checkout/subscriptions/:id
POST/api/v1/checkout/subscriptions/:id/cancel
GET/api/v1/checkout/invoices
GET/api/v1/checkout/invoices/:id/pdf
GET/api/v1/checkout/profile
PATCH/api/v1/checkout/profile
POST/api/v1/checkout/profile/change-email
POST/api/v1/checkout/profile/confirm-email

Webhooks

POST/api/v1/payment/webhook-endpoints
GET/api/v1/payment/webhook-endpoints
GET/api/v1/payment/webhook-endpoints/:id
PATCH/api/v1/payment/webhook-endpoints/:id
DELETE/api/v1/payment/webhook-endpoints/:id
GET/api/v1/payment/webhook-events
POST/api/v1/payment/webhook-events/:id/resend

Account — Custom Domains

GET/api/v1/account/domains
POST/api/v1/account/domains
GET/api/v1/account/domains/:id
POST/api/v1/account/domains/:id/verify
DELETE/api/v1/account/domains/:id

Account — Audit Log

GET/api/v1/account/audit-log

Uploads

POST/api/v1/uploads/image

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.

typescript
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_updated

Shipping

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).

bash
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).

bash
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:

json
{
  "shipmentId": "shp_abc123",
  "status": "delivered",
  "waybillId": "JNE1234567890",
  "courierCode": "jne",
  "checkoutSessionId": "cs_xyz"
}

Shipment Statuses

pendingconfirmedallocatedpicking_uppicked_updropping_offin_transitdeliveredcancelledreturnedfailed

Inventory & 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

bash
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.

bash
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

bash
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.

bash
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.

bash
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

bash
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

bash
# 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_abc123

Ledger

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_refundpayoutadjustment

Auto-post hooks

You don't write to the ledger directly. The platform posts entries at these points:

  • Xendit / PayPal payment successsale credit + optional platform_fee / channel_fee debits
  • Shipment createdshipping_cost debit (Biteship pass-through)
  • Shipment cancelledshipping_refund credit (planned, Phase B.1)
  • Manual adjustment APIadjustment credit or debit

1. List entries

bash
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

bash
# 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.

bash
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

bash
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

bash
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.

bash
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 period

Payouts

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

bash
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.

bash
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

bash
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 passed

4. Cancel / track

bash
# 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.

bash
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.

bash
curl 'https://api.storlaunch.forjio.com/api/v1/storefront/public/acme/discount-codes?productId=prod_x&currency=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.

bash
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_MISMATCH

4. Apply at checkout session create

bash
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 row

Ledger effect

On payment success (webhook), in addition to the sale credit and platform_fee / channel_fee debits, the platform posts:

  • promo_discount debit — cart discount absorbed by merchant.
  • shipping_discount debit — 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.

bash
# 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.

bash
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 rejected

The 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 + WebSite on the merchant root
  • Product (name, image, brand, offers.price, offers.priceCurrency, offers.availability, itemCondition) on every product page
  • BreadcrumbList linking 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

bash
# 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:

bash
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:

bash
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-LD

Pixels & 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.

text
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.

text
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.

text
TikTok Events Manager → Web → your Pixel → Settings → Pixel ID

API endpoints

bash
# 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=test added 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

bash
# 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)

bash
# 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=30

One-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

bash
# 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=false

Format

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

bash
# 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=google

Submission steps

Google Merchant Center

  1. Products → Feeds → +
  2. Country + Language
  3. Name → Scheduled fetch
  4. Paste the URL, fetch daily

Meta Commerce Manager

  1. Catalog → Data Sources
  2. Add items → Use a data feed
  3. Paste the URL, choose Daily

TikTok Catalog Manager

  1. Data source → Add source
  2. Data feed → Paste URL
  3. 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

json
{
  "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_...)

bash
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 history

Public (storefront)

bash
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 slug empty 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 from coverImage.

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

  1. Buyer clicks a /s/<slug>/r/<code> URL → cookie set, click counter bumps.
  2. New buyer signs up via OTP → ReferralAttribution row created with status pending (self-referral + email-match guards applied).
  3. Buyer completes a checkout → session stamped with the attribution id.
  4. Payment webhook fires → pending flips to rewarded, both sides get auto-issued DiscountCodes (max 1 use each, TTL from program config).
  5. Refund → attribution voided. Unused codes deactivate; already-redeemed codes are preserved and the void reason records refunded_after_use for reporting.
  6. No conversion within attributionWindowDays → swept to expired.

Notes

  • Auto-issued codes carry source="referral_referrer" or source="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

CodeHTTPDescription
AUTHENTICATION_REQUIRED401No API key or JWT provided
INVALID_API_KEY401API key is invalid or revoked
INSUFFICIENT_PERMISSIONS403Key doesn't have access
PLAN_LIMIT_EXCEEDED403Account has hit a tier limit
RATE_LIMIT_EXCEEDED429Too many requests
RESOURCE_NOT_FOUND404Entity not found
VALIDATION_ERROR400Request body validation failed
IDEMPOTENCY_CONFLICT409Same idempotency key, different params
PAYMENT_FAILED422Payment provider rejected the charge
INTERNAL_ERROR500Unexpected server error

Ready to integrate?

Create your account and get your API keys in under 2 minutes.

Get API Keys