{"openapi":"3.1.0","info":{"title":"StablePizza API","description":"Order whole pies from L'industrie Pizzeria (Williamsburg) with same-day courier delivery. Pay via wallet (x402 USDC on Base, or MPP) — no card needed from the buyer. Charge-on-placement: the wallet is charged only once the pizzeria order is confirmed placed; failed orders cost nothing.","version":"1.0.0","x-guidance":"StablePizza — order whole pies from L'industrie Pizzeria (Williamsburg, Brooklyn) with same-day courier delivery. Pay via wallet: x402 (USDC on Base) or MPP. No card needed from the buyer.\n\nWhat it does:\n- Places a real prepaid order at L'industrie's Cash App menu with our card, then dispatches a same-day courier to pick it up and deliver to the buyer's address.\n- Whole pies only. Delivery limited to the Williamsburg / Greenpoint / lower-Manhattan zone; a courier fee (shown in the 402 quote) is added for delivery, and addresses outside the zone are rejected at quote time.\n\nPayment model — charge-on-placement:\n- Your wallet is charged ONLY once the pizzeria order is confirmed placed. If placement fails for any reason, the payment is skipped: no money moves, nothing to refund.\n- This means POST /api/orders holds the connection open while the real order is placed (~1-4 minutes). Do not time out early, and do NOT retry a request that is still running.\n- The order's payment_status field tells the truth at all times: \"charged\" (order placed, wallet debited), \"not_charged\" (no money was ever taken), \"authorized\" (decision pending — only while the POST is in flight).\n- refund_owed=true only appears when a CHARGED order later failed (e.g. the courier abandoned a placed pizza). The wallet is made whole in that case.\n\nHappy path:\n1. GET /api/menu — show the pies (name, price), tip options, and store hours. Free, no wallet needed.\n   - If store_open is false, tell the user the shop is closed (store_note has the reason + hours) and don't attempt an order.\n2. Ask the buyer:\n   - Which pies and how many (max 14 pies total).\n   - Delivery address (full street, city, state, zip), plus any courier instructions (\"buzz 4B\", \"leave with doorman\") as delivery_notes — these are passed to the courier.\n   - Tip percent for the L'industrie staff (options: 0/10/15/20%; default 15% if they don't care).\n   - Buyer name + phone (used for the courier delivery and order status texts). The pizzeria order is placed under '<buyer name> (Gift from AgentCash)' — for pickup orders, tell the buyer to give that combined name at the counter.\n3. POST /api/orders { fulfillment: \"delivery\"|\"pickup\", items: [{ sku, quantity }], contact: { name, phone }, dropoff_address?, tip_percent?, delivery_notes? }\n   → The wallet payment covers the full amount: pies + tax + tip + delivery (courier fee, delivery only) + a small service fee.\n   → The request drives the real pizzeria order before returning (~1-4 minutes; keep the connection open). You are charged only if it succeeds.\n   → Returns { order_id, status, order_name, payment_status, ... }.\n4. GET /api/orders/{order_id} — poll for fulfillment progress. Once placed, share confirmation_number, pickup_ready_at (when the kitchen expects it ready), and for delivery the tracking_url + delivery_eta (courier dropoff ETA; the tracking link has the live view).\n\nRules:\n- sku MUST come from GET /api/menu, and check its available flag — pies sell out during the day. Ordering a sold-out pie returns 409 item_sold_out (no payment taken). If one sells out between acceptance and placement, the order fails and you are not charged.\n- If POST /api/orders returns store_closed (409), the shop just closed — no payment was taken; relay the hours and stop.\n- Always confirm the full total (shown in the 402 quote) and get an explicit \"yes\" before paying.\n- Delivery zone is limited; if delivery_unavailable, the address is out of range — no retry will help.\n- The buyer never enters a card. Payment is wallet-only (x402 or MPP).\n- Order status flow: paid → placing_order → order_placed → (delivery: dispatching → dispatched → delivered | pickup: ready_for_pickup). Terminal failures are failed or needs_review with failure_reason; check payment_status to know whether money moved (usually it didn't — failures skip the charge).","contact":{"name":"Merit Systems","url":"https://stablepizza.dev"}},"servers":[{"url":"https://stablepizza.dev"}],"tags":[{"name":"Menu"},{"name":"Orders"}],"paths":{"/api/menu":{"get":{"operationId":"menu","summary":"List L'industrie pies, prices, tip options, and fulfillment modes.","tags":["Menu"],"security":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"merchant":{"type":"string"},"pickup_address":{"type":"string"},"store_open":{"type":"boolean"},"store_hours":{"type":"string"},"store_note":{"anyOf":[{"type":"string"},{"type":"null"}]},"fulfillment_modes":{"type":"array","items":{"type":"string"}},"availability_as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When item availability was last read from the live menu (null = unknown; items then show available)"},"pies":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"price_usd":{"type":"string"},"available":{"type":"boolean","description":"false = sold out right now; ordering it returns 409"}},"required":["sku","name","price_usd","available"],"additionalProperties":false}},"tip_percent_options":{"type":"array","items":{"type":"number"}},"default_tip_percent":{"type":"number"},"max_pies_per_order":{"type":"number"},"delivery_zone":{"type":"string"}},"required":["merchant","pickup_address","store_open","store_hours","store_note","fulfillment_modes","availability_as_of","pies","tip_percent_options","default_tip_percent","max_pies_per_order","delivery_zone"],"additionalProperties":false}}}}}}},"/api/orders":{"post":{"operationId":"orders_create","summary":"Order pizza via wallet (x402 USDC or MPP). Quote covers pies + tax + tip + service fee, plus a courier fee for delivery. Charge-on-placement: this request drives the real order at L'industrie (~1-4 min — keep the connection open) and your wallet is charged ONLY if the order is confirmed placed; a failed placement costs nothing. Poll GET /orders/{id} for progress.","tags":["Orders"],"x-payment-info":{"price":{"mode":"dynamic","currency":"USD","min":"0.01","max":"500.00"},"protocols":[{"x402":{}}],"has_checkout":true},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fulfillment":{"type":"string","enum":["delivery","pickup"],"description":"delivery = same-day courier to an address; pickup = collect at the shop"},"items":{"minItems":1,"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","minLength":1,"description":"Pie sku from GET /api/menu"},"quantity":{"type":"integer","minimum":1,"maximum":14}},"required":["sku","quantity"]}},"contact":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Name on the order"},"phone":{"type":"string","minLength":7,"description":"Phone in E.164, e.g. +15551234567 (order + courier updates)"}},"required":["name","phone"]},"dropoff_address":{"description":"Delivery address: street, city, ST zip (delivery only)","type":"string","minLength":6},"tip_percent":{"default":15,"description":"Tip percent for L'industrie staff (0/10/15/20)","type":"number"},"delivery_notes":{"type":"string","maxLength":280}},"required":["fulfillment","items","contact"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"string"},"status":{"type":"string"},"fulfillment":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number"}},"required":["sku","name","quantity"],"additionalProperties":false}},"contact":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Name on the order"},"phone":{"type":"string","minLength":7,"description":"Phone in E.164, e.g. +15551234567 (order + courier updates)"}},"required":["name","phone"],"additionalProperties":false},"dropoff_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"tip_percent":{"type":"number"},"quoted_total_usd":{"type":"string"},"order_name":{"type":"string","description":"Name the pizzeria order is placed under — give this at the counter for pickup"},"confirmation_number":{"anyOf":[{"type":"string"},{"type":"null"}]},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_ready_at":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO time the kitchen expects the order ready (both modes)"},"delivery_eta":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO courier dropoff ETA (delivery only, set at dispatch; tracking_url has the live view)"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"payment_status":{"type":"string","enum":["authorized","charged","not_charged"],"description":"charged = the wallet payment settled (this happens only once the pizzeria order is confirmed placed). not_charged = no money was taken — failed/unconfirmed orders cost nothing. authorized = payment verified, charge pending the placement outcome (transient)."},"refund_owed":{"type":"boolean","description":"true only when a charged order later failed (e.g. courier abandoned a placed pizza) — the wallet will be made whole"},"created_at":{"type":"string"}},"required":["order_id","status","fulfillment","items","contact","dropoff_address","tip_percent","quoted_total_usd","order_name","confirmation_number","tracking_url","pickup_address","pickup_ready_at","delivery_eta","failure_reason","payment_status","refund_owed","created_at"],"additionalProperties":false}}}},"402":{"description":"Payment Required"}}},"get":{"operationId":"orders_list","summary":"List orders for the signing wallet.","tags":["Orders"],"security":[{"siwx":[]}],"parameters":[{"in":"query","name":"limit","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"offset","schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"type":"object","properties":{"order_id":{"type":"string"},"status":{"type":"string"},"fulfillment":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number"}},"required":["sku","name","quantity"],"additionalProperties":false}},"contact":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Name on the order"},"phone":{"type":"string","minLength":7,"description":"Phone in E.164, e.g. +15551234567 (order + courier updates)"}},"required":["name","phone"],"additionalProperties":false},"dropoff_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"tip_percent":{"type":"number"},"quoted_total_usd":{"type":"string"},"order_name":{"type":"string","description":"Name the pizzeria order is placed under — give this at the counter for pickup"},"confirmation_number":{"anyOf":[{"type":"string"},{"type":"null"}]},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_ready_at":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO time the kitchen expects the order ready (both modes)"},"delivery_eta":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO courier dropoff ETA (delivery only, set at dispatch; tracking_url has the live view)"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"payment_status":{"type":"string","enum":["authorized","charged","not_charged"],"description":"charged = the wallet payment settled (this happens only once the pizzeria order is confirmed placed). not_charged = no money was taken — failed/unconfirmed orders cost nothing. authorized = payment verified, charge pending the placement outcome (transient)."},"refund_owed":{"type":"boolean","description":"true only when a charged order later failed (e.g. courier abandoned a placed pizza) — the wallet will be made whole"},"created_at":{"type":"string"}},"required":["order_id","status","fulfillment","items","contact","dropoff_address","tip_percent","quoted_total_usd","order_name","confirmation_number","tracking_url","pickup_address","pickup_ready_at","delivery_eta","failure_reason","payment_status","refund_owed","created_at"],"additionalProperties":false}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["orders","total","limit","offset"],"additionalProperties":false}}}},"402":{"description":"Authentication Required"}}}},"/api/orders/{orderId}":{"get":{"operationId":"orders_get","summary":"Get order status, payment_status (charged / not_charged), confirmation number, and courier tracking URL.","tags":["Orders"],"security":[{"siwx":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"string"},"status":{"type":"string"},"fulfillment":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number"}},"required":["sku","name","quantity"],"additionalProperties":false}},"contact":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Name on the order"},"phone":{"type":"string","minLength":7,"description":"Phone in E.164, e.g. +15551234567 (order + courier updates)"}},"required":["name","phone"],"additionalProperties":false},"dropoff_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"tip_percent":{"type":"number"},"quoted_total_usd":{"type":"string"},"order_name":{"type":"string","description":"Name the pizzeria order is placed under — give this at the counter for pickup"},"confirmation_number":{"anyOf":[{"type":"string"},{"type":"null"}]},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"pickup_ready_at":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO time the kitchen expects the order ready (both modes)"},"delivery_eta":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO courier dropoff ETA (delivery only, set at dispatch; tracking_url has the live view)"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"payment_status":{"type":"string","enum":["authorized","charged","not_charged"],"description":"charged = the wallet payment settled (this happens only once the pizzeria order is confirmed placed). not_charged = no money was taken — failed/unconfirmed orders cost nothing. authorized = payment verified, charge pending the placement outcome (transient)."},"refund_owed":{"type":"boolean","description":"true only when a charged order later failed (e.g. courier abandoned a placed pizza) — the wallet will be made whole"},"created_at":{"type":"string"}},"required":["order_id","status","fulfillment","items","contact","dropoff_address","tip_percent","quoted_total_usd","order_name","confirmation_number","tracking_url","pickup_address","pickup_ready_at","delivery_eta","failure_reason","payment_status","refund_owed","created_at"],"additionalProperties":false}}}},"402":{"description":"Authentication Required"}}}}},"components":{"securitySchemes":{"siwx":{"type":"apiKey","in":"header","name":"SIGN-IN-WITH-X"}}}}