# Merchant API > Open-source commerce backend. Connect Stripe, deploy to Cloudflare, start selling. ## Overview Merchant is a simple REST API for commerce. It handles products, inventory, carts, checkout, and orders. Payments are processed via Stripe Checkout. Runs on Cloudflare Workers with D1 (SQLite) and R2 for images. ## Quick Start ```bash git clone https://github.com/withpluto/merchant cd merchant && npm install npx tsx scripts/init.ts # Creates store + API keys npm run dev # http://localhost:8787 ``` ## Authentication All endpoints require `Authorization: Bearer ` header. Two key types: - `pk_...` — Public key: Can create carts and checkout - `sk_...` — Admin key: Full access to all endpoints ## Base URL Local: `http://localhost:8787` Production: Your Cloudflare Workers URL --- # API Reference ## Products ### List Products ``` GET /v1/products Authorization: Bearer sk_... ``` Response: ```json { "items": [ { "id": "uuid", "title": "T-Shirt", "description": "Premium cotton tee", "status": "active", "created_at": "2025-01-01T00:00:00Z", "variants": [ { "id": "uuid", "sku": "TEE-BLK-M", "title": "Black / M", "price_cents": 2999, "image_url": null } ] } ] } ``` ### Get Product ``` GET /v1/products/{id} Authorization: Bearer sk_... ``` ### Create Product ``` POST /v1/products Authorization: Bearer sk_... Content-Type: application/json { "title": "T-Shirt", "description": "Premium cotton tee" } ``` Response (201): ```json { "id": "uuid", "title": "T-Shirt", "description": "Premium cotton tee", "status": "active", "variants": [] } ``` ### Update Product ``` PATCH /v1/products/{id} Authorization: Bearer sk_... Content-Type: application/json { "title": "Updated Title", "description": "New description", "status": "draft" } ``` Status values: `active`, `draft` ### Create Variant ``` POST /v1/products/{id}/variants Authorization: Bearer sk_... Content-Type: application/json { "sku": "TEE-BLK-M", "title": "Black / M", "price_cents": 2999, "image_url": "https://..." } ``` Required fields: `sku`, `title`, `price_cents` Response (201): ```json { "id": "uuid", "sku": "TEE-BLK-M", "title": "Black / M", "price_cents": 2999, "image_url": null } ``` --- ## Inventory ### List Inventory ``` GET /v1/inventory Authorization: Bearer sk_... ``` Response: ```json { "items": [ { "sku": "TEE-BLK-M", "on_hand": 100, "reserved": 5, "available": 95, "variant_title": "Black / M", "product_title": "T-Shirt" } ] } ``` ### Get Inventory by SKU ``` GET /v1/inventory?sku=TEE-BLK-M Authorization: Bearer sk_... ``` Response: ```json { "sku": "TEE-BLK-M", "on_hand": 100, "reserved": 5, "available": 95 } ``` ### Adjust Inventory ``` POST /v1/inventory/{sku}/adjust Authorization: Bearer sk_... Content-Type: application/json { "delta": 50, "reason": "restock" } ``` Reasons: `restock`, `correction`, `damaged`, `return` Use negative delta to decrease: `{"delta": -10, "reason": "damaged"}` --- ## Carts ### Create Cart ``` POST /v1/carts Authorization: Bearer pk_... (or sk_...) Content-Type: application/json { "customer_email": "buyer@example.com" } ``` Response: ```json { "id": "uuid", "status": "open", "currency": "USD", "customer_email": "buyer@example.com", "items": [], "expires_at": "2025-01-01T12:30:00Z" } ``` Carts expire after 30 minutes. ### Add Items to Cart ``` POST /v1/carts/{id}/items Authorization: Bearer pk_... (or sk_...) Content-Type: application/json { "items": [ {"sku": "TEE-BLK-M", "qty": 2}, {"sku": "CAP-BLK", "qty": 1} ] } ``` This replaces all items in the cart. Response: ```json { "id": "uuid", "status": "open", "currency": "USD", "customer_email": "buyer@example.com", "items": [ { "sku": "TEE-BLK-M", "title": "Black / M", "qty": 2, "unit_price_cents": 2999 }, { "sku": "CAP-BLK", "title": "Black", "qty": 1, "unit_price_cents": 2499 } ], "expires_at": "2025-01-01T12:30:00Z" } ``` ### Checkout ``` POST /v1/carts/{id}/checkout Authorization: Bearer pk_... (or sk_...) Content-Type: application/json { "success_url": "https://yoursite.com/thanks?session_id={CHECKOUT_SESSION_ID}", "cancel_url": "https://yoursite.com/cart" } ``` Response: ```json { "checkout_url": "https://checkout.stripe.com/c/pay/cs_...", "stripe_checkout_session_id": "cs_..." } ``` Redirect the customer to `checkout_url` to complete payment on Stripe. --- ## Orders ### List Orders ``` GET /v1/orders Authorization: Bearer sk_... ``` Response: ```json { "items": [ { "id": "uuid", "number": "ORD-00001", "status": "paid", "customer_email": "buyer@example.com", "amounts": { "subtotal_cents": 5998, "tax_cents": 0, "shipping_cents": 0, "total_cents": 5998, "currency": "USD" }, "items": [...], "created_at": "2025-01-01T12:00:00Z" } ] } ``` ### Get Order ``` GET /v1/orders/{id} Authorization: Bearer sk_... ``` Response: ```json { "id": "uuid", "number": "ORD-00001", "status": "paid", "customer_email": "buyer@example.com", "ship_to": { "line1": "123 Main St", "city": "San Francisco", "state": "CA", "postal_code": "94102", "country": "US" }, "amounts": { "subtotal_cents": 5998, "tax_cents": 0, "shipping_cents": 0, "total_cents": 5998, "currency": "USD" }, "stripe": { "checkout_session_id": "cs_...", "payment_intent_id": "pi_..." }, "items": [ { "sku": "TEE-BLK-M", "title": "Black / M", "qty": 2, "unit_price_cents": 2999 } ], "created_at": "2025-01-01T12:00:00Z" } ``` ### Refund Order ``` POST /v1/orders/{id}/refund Authorization: Bearer sk_... Content-Type: application/json { "amount_cents": 2999 } ``` Omit `amount_cents` for full refund. Response: ```json { "stripe_refund_id": "re_...", "status": "succeeded" } ``` ### Create Test Order ``` POST /v1/orders/test Authorization: Bearer sk_... Content-Type: application/json { "customer_email": "test@example.com", "items": [ {"sku": "TEE-BLK-M", "qty": 1} ] } ``` Creates an order without Stripe. Useful for development/testing. --- ## Images ### Upload Image ``` POST /v1/images Authorization: Bearer sk_... Content-Type: multipart/form-data file: ``` Response: ```json { "url": "https://...", "key": "store_id/uuid.jpg" } ``` Max size: 5MB Formats: jpeg, png, webp, gif ### Get Image ``` GET /v1/images/{key} ``` No auth required. Returns image with cache headers. ### Delete Image ``` DELETE /v1/images/{key} Authorization: Bearer sk_... ``` --- ## Webhooks ### Stripe Webhook ``` POST /v1/webhooks/stripe ``` Configure in Stripe Dashboard. Set endpoint to: `https://your-domain.com/v1/webhooks/stripe` Events handled: - `checkout.session.completed` — Creates order, deducts inventory For local development: ```bash stripe listen --forward-to localhost:8787/v1/webhooks/stripe ``` --- ## Setup ### Connect Stripe ``` POST /v1/setup/stripe Authorization: Bearer sk_... Content-Type: application/json { "stripe_secret_key": "sk_test_...", "stripe_webhook_secret": "whsec_..." } ``` Get keys from: https://dashboard.stripe.com/apikeys --- ## Errors All errors return: ```json { "error": { "code": "error_code", "message": "Human readable message" } } ``` Error codes: - `unauthorized` (401) — Missing or invalid API key - `forbidden` (403) — Key doesn't have permission - `not_found` (404) — Resource not found - `invalid_request` (400) — Bad request data - `conflict` (409) — Duplicate SKU or cart already checked out - `insufficient_inventory` (409) — Not enough stock for SKU - `stripe_error` (502) — Stripe API error --- ## Data Model ### Store Multi-tenant. Each store has its own products, inventory, and orders. ### Product Has title, description, status. Contains variants. ### Variant SKU-level item with price. Each variant has its own inventory. ### Inventory Tracked per SKU with `on_hand` and `reserved` quantities. - `on_hand`: Total stock - `reserved`: Stock held for pending checkouts - `available`: `on_hand - reserved` ### Cart Shopping cart with email and items. Expires in 30 minutes. ### Order Created from cart after Stripe payment succeeds. --- ## Complete Checkout Flow 1. Create cart with customer email 2. Add items to cart 3. Call checkout endpoint 4. Redirect customer to checkout_url 5. Customer pays on Stripe 6. Stripe webhook creates order 7. Inventory automatically deducted Example: ```bash # 1. Create cart curl -X POST http://localhost:8787/v1/carts \ -H "Authorization: Bearer pk_..." \ -H "Content-Type: application/json" \ -d '{"customer_email":"buyer@example.com"}' # 2. Add items curl -X POST http://localhost:8787/v1/carts/{cart_id}/items \ -H "Authorization: Bearer pk_..." \ -H "Content-Type: application/json" \ -d '{"items":[{"sku":"TEE-BLK-M","qty":1}]}' # 3. Checkout curl -X POST http://localhost:8787/v1/carts/{cart_id}/checkout \ -H "Authorization: Bearer pk_..." \ -H "Content-Type: application/json" \ -d '{"success_url":"https://site.com/thanks","cancel_url":"https://site.com/cart"}' ``` --- ## Stack - Runtime: Cloudflare Workers - Framework: Hono - Database: D1 (SQLite) - Images: R2 - Payments: Stripe - Dependencies: hono, stripe --- ## License MIT