Merchant API
Open-source commerce backend. Connect your Stripe account, deploy to Cloudflare Workers, and start selling.
https://your-worker.workers.dev
v1
Quick Start
git clone https://github.com/withpluto/merchant
cd merchant && npm install
npx tsx scripts/init.ts
Save the API keys — they're shown only once.
npm run dev
curl -X POST http://localhost:8787/v1/setup/stripe \
-H "Authorization: Bearer sk_your_key" \
-H "Content-Type: application/json" \
-d '{"stripe_secret_key":"sk_test_..."}'
Authentication
All requests require a Bearer token in the Authorization header.
Authorization: Bearer <api_key>
| Prefix | Role | Access |
|---|---|---|
pk_ |
Public | Carts & checkout |
sk_ |
Admin | Full access |
Products
Products are top-level catalog items. Each product has variants with individual SKUs and prices.
/v1/products
Returns all products with their variants.
{
"items": [
{
"id": "prod_abc123",
"title": "Classic Tee",
"status": "active",
"variants": [...]
}
]
}
/v1/products
Creates a new product.
| title | required | Product name |
| description | optional | Product description |
{
"title": "Classic Tee",
"description": "Premium cotton t-shirt"
}
/v1/products/:id
Updates a product. All fields optional.
| title | optional | Product name |
| status | optional | active or draft |
Variants
Variants are purchasable SKUs with their own price and inventory.
/v1/products/:id/variants
Creates a variant. Automatically creates inventory record.
| sku | required | Unique identifier |
| title | required | e.g. "Black / Medium" |
| price_cents | required | Price in cents (2999 = $29.99) |
| image_url | optional | Image URL |
{
"sku": "TEE-BLK-M",
"title": "Black / Medium",
"price_cents": 2999
}
Inventory
Track stock per SKU. on_hand is total stock, reserved is held for pending checkouts, available is what can be sold.
/v1/inventory
Returns inventory for all SKUs. Filter with ?sku=TEE-BLK-M.
{
"items": [{
"sku": "TEE-BLK-M",
"on_hand": 100,
"reserved": 5,
"available": 95
}]
}
/v1/inventory/:sku/adjust
Adjust stock. Positive delta adds, negative removes.
| delta | required | Amount to add/remove |
| reason | required | restock, correction, damaged, return |
{ "delta": 50, "reason": "restock" }
Carts
Shopping carts expire after 30 minutes. Public keys can create and modify carts.
/v1/carts
Creates a new cart.
{ "customer_email": "buyer@example.com" }
/v1/carts/:id/items
Sets cart items. Replaces existing items.
{
"items": [
{ "sku": "TEE-BLK-M", "qty": 2 }
]
}
Checkout
Creates a Stripe Checkout Session. Redirect customer to the returned URL.
/v1/carts/:id/checkout
Reserves inventory and creates Stripe session.
{
"success_url": "https://site.com/thanks",
"cancel_url": "https://site.com/cart"
}
{
"checkout_url": "https://checkout.stripe.com/...",
"stripe_checkout_session_id": "cs_..."
}
Orders
Orders are created automatically when Stripe payment completes. Admin key required.
/v1/orders
Returns all orders, most recent first.
/v1/orders/:id
Returns a single order.
{
"id": "ord_abc123",
"number": "ORD-00001",
"status": "paid",
"customer_email": "buyer@example.com",
"amounts": {
"total_cents": 5998,
"currency": "USD"
},
"items": [...]
}
/v1/orders/:id/refund
Issues refund via Stripe. Omit amount_cents for full refund.
{ "amount_cents": 2999 }
/v1/orders/test
Creates order without Stripe. For development.
{
"customer_email": "test@example.com",
"items": [{ "sku": "TEE-BLK-M", "qty": 1 }]
}
Images
Upload images to R2 storage. Max 5MB. Accepts JPEG, PNG, WebP, GIF.
/v1/images
Uploads an image. Use multipart/form-data with file field.
{
"url": "https://...",
"key": "store_id/uuid.jpg"
}
/v1/images/:key
Deletes an image. Can only delete your store's images.
Webhooks
Configure Stripe to send webhooks to /v1/webhooks/stripe.
/v1/webhooks/stripe
Handles checkout.session.completed — creates order and deducts inventory.
stripe listen --forward-to localhost:8787/v1/webhooks/stripe
Errors
All errors return a consistent format with code and message.
{
"error": {
"code": "not_found",
"message": "Product not found"
}
}
| Code | Status | Description |
|---|---|---|
unauthorized | 401 | Invalid API key |
forbidden | 403 | Missing permission |
not_found | 404 | Resource not found |
invalid_request | 400 | Bad request data |
conflict | 409 | Duplicate SKU, cart not open |
insufficient_inventory | 409 | Not enough stock |