merchant / docs

Merchant API

Open-source commerce backend. Connect your Stripe account, deploy to Cloudflare Workers, and start selling.

Base URL
https://your-worker.workers.dev
Version
v1

Quick Start

1. Clone and install
git clone https://github.com/withpluto/merchant
cd merchant && npm install
2. Initialize your store
npx tsx scripts/init.ts

Save the API keys — they're shown only once.

3. Start the server
npm run dev
4. Connect Stripe
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.

GET /v1/products

Returns all products with their variants.

Response
{
  "items": [
    {
      "id": "prod_abc123",
      "title": "Classic Tee",
      "status": "active",
      "variants": [...]
    }
  ]
}
POST /v1/products

Creates a new product.

Parameters
title required Product name
description optional Product description
Request
{
  "title": "Classic Tee",
  "description": "Premium cotton t-shirt"
}
PATCH /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.

POST /v1/products/:id/variants

Creates a variant. Automatically creates inventory record.

Parameters
sku required Unique identifier
title required e.g. "Black / Medium"
price_cents required Price in cents (2999 = $29.99)
image_url optional Image URL
Request
{
  "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.

GET /v1/inventory

Returns inventory for all SKUs. Filter with ?sku=TEE-BLK-M.

Response
{
  "items": [{
    "sku": "TEE-BLK-M",
    "on_hand": 100,
    "reserved": 5,
    "available": 95
  }]
}
POST /v1/inventory/:sku/adjust

Adjust stock. Positive delta adds, negative removes.

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

POST /v1/carts

Creates a new cart.

{ "customer_email": "buyer@example.com" }
POST /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.

POST /v1/carts/:id/checkout

Reserves inventory and creates Stripe session.

Request
{
  "success_url": "https://site.com/thanks",
  "cancel_url": "https://site.com/cart"
}
Response
{
  "checkout_url": "https://checkout.stripe.com/...",
  "stripe_checkout_session_id": "cs_..."
}

Orders

Orders are created automatically when Stripe payment completes. Admin key required.

GET /v1/orders

Returns all orders, most recent first.

GET /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": [...]
}
POST /v1/orders/:id/refund

Issues refund via Stripe. Omit amount_cents for full refund.

{ "amount_cents": 2999 }
POST /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.

POST /v1/images

Uploads an image. Use multipart/form-data with file field.

{
  "url": "https://...",
  "key": "store_id/uuid.jpg"
}
DELETE /v1/images/:key

Deletes an image. Can only delete your store's images.

Webhooks

Configure Stripe to send webhooks to /v1/webhooks/stripe.

POST /v1/webhooks/stripe

Handles checkout.session.completed — creates order and deducts inventory.

Local Development
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
unauthorized401Invalid API key
forbidden403Missing permission
not_found404Resource not found
invalid_request400Bad request data
conflict409Duplicate SKU, cart not open
insufficient_inventory409Not enough stock