{"openapi":"3.1.0","info":{"title":"Quartermaster API","version":"2026-10-24.15 (landing v4, embeddings on in render.yaml, agent v6 traced, interface v3, reimburse v1, search v2.1 elastic + es|ql, dashboard v2.2 studio, money-in v1, mcp v1, api v2.2 admin reset + roles)","description":"AI purchasing agent for volunteer teams. Money moves only through PayPal and only inside spending mandates a human signed; the mandate engine decides, separate code executes. Sandbox only.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"https://quartermaster-bg3a.onrender.com"}],"tags":[{"name":"System"},{"name":"Session"},{"name":"Agent"},{"name":"Payments"},{"name":"Reimbursements"},{"name":"Money in"},{"name":"Agents"},{"name":"Dashboard"},{"name":"Webhooks"}],"paths":{"/api/health":{"get":{"tags":["System"],"summary":"Health check","operationId":"getApiHealth","security":[],"responses":{"200":{"description":"The app is up","content":{"application/json":{"schema":{"type":"object"},"example":{"ok":true}}}}}}},"/api/openapi.json":{"get":{"tags":["System"],"summary":"This document","operationId":"getApiOpenapiJson","security":[],"responses":{"200":{"description":"OpenAPI 3.1","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/admin/reset-demo":{"post":{"tags":["System"],"summary":"Reset the demo org","description":"Wipes and re-seeds the demo org (as npm run db:seed), then sets up the dues plan in the PayPal sandbox (as npm run setup:dues). Bearer APP_SECRET; 503 when APP_SECRET is unset. 207 when the seed worked but the dues setup failed.","operationId":"postApiAdminResetDemo","security":[{"adminSecret":[]}],"responses":{"200":{"description":"Seeded","content":{"application/json":{"schema":{"type":"object"},"example":{"ok":true,"seeded":{"org":"Riverside Cleanup Crew","members":6,"purchases":4},"dues":{"ok":true,"planId":"P-…","amountCents":1000}}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/session":{"post":{"tags":["Session"],"summary":"Demo sign-in","description":"Signs in as the demo volunteer or treasurer and sets the session cookie. No passwords: this is a sandbox demo.","operationId":"postApiSession","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["member","treasurer"],"description":"demo sign-in: volunteer or treasurer"}},"required":["role"],"additionalProperties":false}}}},"security":[],"responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"type":"object"},"example":{"member":{"id":"…","name":"Priya Nair","role":"treasurer"}}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Session"],"summary":"Sign out","operationId":"deleteApiSession","security":[],"responses":{"200":{"description":"Signed out","content":{"application/json":{"schema":{"type":"object"},"example":{"ok":true}}}}}}},"/api/chat":{"post":{"tags":["Agent"],"summary":"Chat with the purchasing agent","description":"AI SDK v6 UI message stream. Body: { id, messages } as sent by useChat. The agent can search, dry-run and propose; it cannot pay.","operationId":"postApiChat","security":[{"session":[]}],"responses":{"200":{"description":"Server-sent UI message stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/paypal/client-token":{"get":{"tags":["Payments"],"summary":"Browser client token for the PayPal JS SDK v6","operationId":"getApiPaypalClientToken","security":[],"responses":{"200":{"description":"Short-lived client token","content":{"application/json":{"schema":{"type":"object"},"example":{"accessToken":"…"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/payments/{id}/order":{"post":{"tags":["Payments"],"summary":"Create the PayPal order for an approved proposal","description":"Approver only. The mandate is re-checked first; PayPal-Request-Id = order-<id>.","operationId":"postApiPaymentsIdOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"PayPal order","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"5O190127TN364715T","status":"CREATED"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/payments/{id}/capture":{"post":{"tags":["Payments"],"summary":"Capture after the approver pays in the PayPal popup","description":"Mandate re-checked again; PayPal-Request-Id = capture-<id>. Idempotent.","operationId":"postApiPaymentsIdCapture","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"orderId":{"type":"string","minLength":1,"description":"the PayPal order id returned by the order route and approved in the PayPal popup"}},"required":["orderId"],"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Capture result","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"captured"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/payments/{id}/refund":{"post":{"tags":["Payments"],"summary":"Refund a paid purchase","description":"Mandate approver only. PayPal-Request-Id = refund-<id>.","operationId":"postApiPaymentsIdRefund","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200,"default":"","description":"shown on the trust receipt and sent to the payer"}},"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Refund requested","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"COMPLETED","refundId":"1JU08902781691411"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/reimbursements":{"post":{"tags":["Reimbursements"],"summary":"Submit a receipt photo","description":"Gemini reads it; duplicate (hash, exact and by meaning) and mandate checks run; the treasurer is notified.","operationId":"postApiReimbursements","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mandateId":{"type":"string","minLength":1,"description":"the budget the receipt is charged to"},"imageDataUrl":{"type":"string","pattern":"^data\\:image\\/","description":"the receipt photo as a data URL (JPEG, PNG or WebP, max 4 MB)"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"manual":{"anyOf":[{"type":"object","properties":{"merchant":{"type":"string","minLength":1,"maxLength":120},"date":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}]},"totalDollars":{"type":"number","exclusiveMinimum":0,"maximum":10000},"category":{"type":"string","minLength":1}},"required":["merchant","date","totalDollars","category"],"additionalProperties":false},{"type":"null"}],"description":"details typed in by hand when the photo can't be read; flagged for the treasurer"}},"required":["mandateId","imageDataUrl"],"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"…","status":"needs_approval"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/reimbursements/{id}/approve":{"post":{"tags":["Reimbursements"],"summary":"Approve and send the PayPal payout","description":"Approver only. Mandate re-checked; sender_batch_id = reimb-<id>; safe against double clicks.","operationId":"postApiReimbursementsIdApprove","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"Payout sent","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"approved","payoutBatchId":"9G526YC9UN5D8","payoutStatus":"PENDING"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/reimbursements/{id}/reject":{"post":{"tags":["Reimbursements"],"summary":"Reject a receipt","operationId":"postApiReimbursementsIdReject","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":300,"default":""}},"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Rejected","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/reimbursements/{id}/status":{"get":{"tags":["Reimbursements"],"summary":"Payout status (asks PayPal while in flight)","operationId":"getApiReimbursementsIdStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"paid","payoutStatus":"SUCCESS","seconds":44}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions":{"post":{"tags":["Money in"],"summary":"Record a member's dues subscription","description":"The server reads the subscription back from PayPal and checks the plan before recording it.","operationId":"postApiSubscriptions","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string","pattern":"^I-[A-Z0-9]+$","description":"the id PayPal returns after the member approves"}},"required":["subscriptionId"],"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"ACTIVE","credited":1}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invoices/{id}/send":{"post":{"tags":["Money in"],"summary":"Send a drafted sponsor invoice","description":"Treasurer only: this asks the sponsor for money.","operationId":"postApiInvoicesIdSend","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"Sent","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"SENT","recipientUrl":"https://www.sandbox.paypal.com/invoice/p/#…"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"PayPal returned an error (paypalDebugId included)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invoices/{id}/sync":{"post":{"tags":["Money in"],"summary":"Check whether a sponsor invoice was paid","operationId":"postApiInvoicesIdSync","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"Current status","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"PAID"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/keys":{"post":{"tags":["Agents"],"summary":"Create an MCP API key","description":"Treasurer only. The key is returned once; only its hash is stored.","operationId":"postApiKeys","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":60,"default":"","description":"who will use the key, e.g. 'Claude Desktop'"}},"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"…","name":"Claude Desktop","hint":"qm_live_ab12…xy9z","key":"qm_live_…"}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/keys/{id}":{"delete":{"tags":["Agents"],"summary":"Revoke an MCP API key","operationId":"deleteApiKeysId","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"session":[]}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"},"example":{"ok":true}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/mcp":{"post":{"tags":["Agents"],"summary":"MCP server (streamable HTTP, JSON-RPC)","description":"Tools: list_mandates, search_products, check_purchase, request_purchase, search_policy, get_trust_receipt, submit_reimbursement. None can pay.","operationId":"postApiMcp","security":[{"apiKey":[]}],"responses":{"200":{"description":"JSON-RPC response","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/treasurer/insights":{"get":{"tags":["Dashboard"],"summary":"Treasurer figures for the AG Studio agent","description":"Read-only, from the same ledger the mandate engine checks: spend by event, mandates at or above a share of their cap, receipts waiting, blocked attempts, mandate health (approval rate, rules that fired most). Money in integer cents.","operationId":"getApiTreasurerInsights","parameters":[{"name":"view","in":"query","required":true,"schema":{"type":"string","enum":["spend_by_event","over_budget_share","pending_reimbursements","blocked_attempts","mandate_health"]}},{"name":"threshold","in":"query","required":false,"description":"over_budget_share only: percent of the cap used (spent + approved, not yet paid)","schema":{"type":"number","minimum":0,"maximum":1000,"default":80}},{"name":"days","in":"query","required":false,"description":"blocked_attempts only: how far back","schema":{"type":"integer","minimum":1,"maximum":365,"default":30}}],"security":[{"session":[]}],"responses":{"200":{"description":"The figures","content":{"application/json":{"schema":{"type":"object"},"example":{"view":"over_budget_share","asOf":"2026-11-01T10:00:00.000Z","source":"live ledger","result":[{"title":"Beach Cleanup supplies","usedPct":99.9,"remainingCents":83}]}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/studio/llm":{"post":{"tags":["Dashboard"],"summary":"One AG Studio agent turn on Gemini","description":"Called by the AG Studio adapter in the browser. Returns the model's text and tool calls; the browser runs the tools. The Gemini key stays on the server.","operationId":"postApiStudioLlm","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":true},"maxItems":400,"description":"the conversation so far, as AG Studio conversation items"},"instructions":{"type":"string","maxLength":60000},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"parameters":{"type":"object","additionalProperties":{}}},"required":["name","description","parameters"],"additionalProperties":true},"maxItems":80,"description":"tool schemas the agent may call; the browser runs them, never the server"},"toolChoice":{"anyOf":[{"type":"string","enum":["auto","none","required"]},{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false}]},"responseFormat":{"type":"object","properties":{"type":{"type":"string","enum":["text","json"]},"name":{"type":"string"},"schema":{"type":"object","additionalProperties":{}}},"required":["type"],"additionalProperties":true}},"required":["input","responseFormat"],"additionalProperties":false}}}},"security":[{"session":[]}],"responses":{"200":{"description":"An AgLlmResponse (status failed with a readable error when the model is unavailable)","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"resp_…","status":"completed","output":[{"type":"function_call","name":"over_budget_share","arguments":"{\"thresholdPct\":80}"}]}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in, or missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhooks/paypal":{"post":{"tags":["Webhooks"],"summary":"PayPal webhook receiver","description":"Signature verified with PayPal, stored by event id before processing (duplicates ignored).","operationId":"postApiWebhooksPaypal","security":[{"paypalSignature":[]}],"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object"},"example":{"ok":true}}}},"400":{"description":"Invalid input, or the action isn't allowed in the current state (e.g. blocked by a mandate rule)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"qm_member","description":"Set by POST /api/session (demo sign-in)"},"apiKey":{"type":"http","scheme":"bearer","description":"qm_live_… key created on /settings (MCP only)"},"paypalSignature":{"type":"apiKey","in":"header","name":"paypal-transmission-sig","description":"Verified with PayPal's verify-webhook-signature API"},"adminSecret":{"type":"http","scheme":"bearer","description":"The server's APP_SECRET (admin routes only; refused when it is unset)"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"},"paypalDebugId":{"type":["string","null"]}},"required":["error"]}}}}