{"openapi":"3.1.0","info":{"title":"PayFlow Studio API","version":"1.0.0","summary":"Agent-friendly API over the PayFlow event bus.","description":"Agent-friendly API over the PayFlow event bus. Lifecycle, playbook and Dev Studio endpoints serve synthetic demo data for a fictional merchant (Northwind Notes) and carry the header X-PayFlow-Data: synthetic. The three lifecycle read endpoints (summary, high-risk, events) return the owner's real data, with X-PayFlow-Data: live, when called with an API key from the Settings page (Authorization: Bearer pf_live_...). Keys are read-only and cannot send. The playbook endpoint stays on demo data even with a key. The readiness scan fetches real public pages. Agents may read, scan and propose; sending always needs human approval in the app. Machine-readable summary for agents: /llms.txt and /.well-known/agent.json.","contact":{"name":"Amelior Labs","url":"https://ameliorlabs.ca"},"license":{"name":"AGPL-3.0-only","identifier":"AGPL-3.0-only"}},"servers":[{"url":"https://payflow.ameliorlabs.ca"}],"security":[],"tags":[{"name":"Lifecycle","description":"Subscription health and risk."},{"name":"Playbooks","description":"Recovery drafts. Never sent from the API."},{"name":"Readiness","description":"Agent-readiness scanner for any public store."},{"name":"Dev Studio","description":"Replayable PayPal-shaped scenarios."}],"paths":{"/api/lifecycle/summary":{"get":{"operationId":"getLifecycleSummary","tags":["Lifecycle"],"security":[{},{"bearerAuth":[]}],"summary":"Subscription health snapshot: active count, MRR, 30-day churn, high-risk count.","responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"`synthetic` without an API key, `live` with a valid one.","schema":{"type":"string","enum":["synthetic","live"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleSummary"},"example":{"activeSubscriptions":75,"monthlyRecurringRevenue":1451,"churnRate30d":0.05,"highRiskSubscriptions":8,"atRiskMrr":152,"dataMode":"synthetic"}}}},"401":{"description":"invalid_api_key: a key was sent but it is malformed, unknown or revoked. Requests with a bad key never fall back to demo data.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited: more than 120 requests per minute for this key. Retry-After is 60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions/high-risk":{"get":{"operationId":"listHighRiskSubscriptions","tags":["Lifecycle"],"security":[{},{"bearerAuth":[]}],"summary":"Subscriptions that need attention, with a plain-language riskReason. Query: limit (default 50).","parameters":[{"name":"limit","in":"query","required":false,"description":"Maximum rows, 1 to 200. Default 50.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"example":5}],"responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"`synthetic` without an API key, `live` with a valid one.","schema":{"type":"string","enum":["synthetic","live"]}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/HighRiskSubscription"}},"example":[{"id":"sub_005","customerId":"cust_005","customer":{"email":"theo.larsen4@example.com","name":"Theo Larsen"},"planName":"Team","price":49,"currency":"USD","status":"past_due","lastPaymentDate":"2026-08-16T14:52:52.089Z","riskLevel":"high","riskReason":"2 failed renewals in 30 days."}]}}},"401":{"description":"invalid_api_key: a key was sent but it is malformed, unknown or revoked. Requests with a bad key never fall back to demo data.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited: more than 120 requests per minute for this key. Retry-After is 60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions/{id}/events":{"get":{"operationId":"getSubscriptionEvents","tags":["Lifecycle"],"security":[{},{"bearerAuth":[]}],"summary":"Normalized event history for one subscription.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"sub_001"}],"responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"`synthetic` without an API key, `live` with a valid one.","schema":{"type":"string","enum":["synthetic","live"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionEvents"},"example":{"subscriptionId":"sub_001","events":[{"id":"evt_00001","derivedType":"SubscriptionCreated","type":"BILLING.SUBSCRIPTION.ACTIVATED","amount":9,"currency":"USD","timestamp":"2026-06-17T14:52:52.089Z"},{"id":"evt_00002","derivedType":"PaymentSucceeded","type":"PAYMENT.SALE.COMPLETED","amount":9,"currency":"USD","timestamp":"2026-07-17T14:52:52.089Z"}]}}}},"401":{"description":"invalid_api_key: a key was sent but it is malformed, unknown or revoked. Requests with a bad key never fall back to demo data.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"subscription_not_found: no subscription with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited: more than 120 requests per minute for this key. Retry-After is 60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/playbooks/recover-failed-payments":{"post":{"operationId":"draftRecoveryPlaybook","tags":["Playbooks"],"summary":"Draft recovery emails for 1 to 25 subscriptions. Drafts only: a person approves before anything is sent.","description":"Runs the decision and writing steps for each subscription and stores the result as a playbook that awaits approval. It never sends anything. Jev decides tone and likely cause when configured, a rules baseline otherwise. An AI model writes the email when a key is configured, a template otherwise. `writtenBy` and `decision.source` say which.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoverRequest"},"example":{"subscriptionIds":["sub_001"]}}}},"responses":{"201":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"Always `synthetic` from the demo API.","schema":{"type":"string","enum":["synthetic"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Playbook"},"example":{"playbookId":"pb_muwssxw4kl","status":"awaiting_approval","generatedEmails":[{"subscriptionId":"sub_001","subject":"We couldn't process your last payment","body":"Hi Maya,\n\nIt looks like your latest $9.00 payment for Starter didn't go through. This happens, often because of an expired card or a temporary bank hold.\n\nYou can update your payment method in under a minute here: https://northwind-notes.example/billing\n\nThanks,\nThe Northwind Notes team","writtenBy":"template","decision":{"churnProbability":0.07,"tone":"friendly","cause":null,"source":"rules"}}]}}}},"400":{"description":"invalid_json or invalid_subscription_ids (provide 1 to 25 ids).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"subscription_not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/playbooks/{id}":{"get":{"operationId":"getPlaybook","tags":["Playbooks"],"summary":"Status and drafted emails for a playbook.","description":"Demo playbooks are kept in server memory and are lost when the server restarts.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"pb_muwssxw4kl"}],"responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"Always `synthetic` from the demo API.","schema":{"type":"string","enum":["synthetic"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredPlaybook"},"example":{"id":"pb_muwssxw4kl","name":"RecoverFailedPayments","status":"awaiting_approval","createdAt":"2026-10-06T14:53:00.000Z","generatedEmails":[{"subscriptionId":"sub_001","subject":"We couldn't process your last payment","body":"Hi Maya, ...","writtenBy":"template","decision":{"churnProbability":0.07,"tone":"friendly","cause":null,"source":"rules"}}]}}}},"404":{"description":"playbook_not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/readiness/scan":{"post":{"operationId":"scanStoreReadiness","tags":["Readiness"],"summary":"Scan a store for agent readiness: score, per-dimension scores, findings and fix snippets.","description":"Fetches the public store page, robots.txt, sitemap, llms.txt and common policy pages, then scores four dimensions. This is a live scan, not synthetic data.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanRequest"},"example":{"storeUrl":"https://example.com"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessResult"},"example":{"auditId":"ra_muwst0k7","storeUrl":"https://example.com/","overallScore":5,"label":"Invisible to agents","scores":{"discoverability":0,"catalog":0,"trust":0,"checkout":0.25},"findings":[{"id":"llms_txt","dimension":"discoverability","ok":false,"severity":"warn","text":"No llms.txt found","fixId":"llms_txt"},{"id":"https","dimension":"checkout","ok":true,"severity":"info","text":"Served over HTTPS"}],"suggestedFixes":[{"id":"llms_txt","title":"Add an llms.txt file","why":"AI shopping agents look here first for a plain-language map of your store.","snippet":"# example.com\n> One sentence describing what you sell."}],"createdAt":"2026-10-06T14:53:09.224Z"}}}},"400":{"description":"invalid_json or missing_store_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"scan_failed: the page could not be fetched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/readiness/audits":{"get":{"operationId":"listReadinessAudits","tags":["Readiness"],"summary":"Past readiness audits. Empty in demo mode.","description":"Demo mode keeps no audit history, so this returns an empty array.","responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"Always `synthetic` from the demo API.","schema":{"type":"string","enum":["synthetic"]}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReadinessResult"}},"example":[]}}},"4XX":{"description":"Client error, in the standard error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/dev/scenarios":{"get":{"operationId":"listScenarios","tags":["Dev Studio"],"summary":"Available sandbox scenarios with event counts.","responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"Always `synthetic` from the demo API.","schema":{"type":"string","enum":["synthetic"]}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Scenario"}},"example":[{"name":"subscription_churn_storm","title":"Subscription churn storm","description":"Many failed renewals and cancellations over five days after a card-network outage.","eventCount":162},{"name":"dispute_spike","title":"Dispute spike","description":"A cluster of disputes opened within three days of a shipping delay.","eventCount":14},{"name":"donation_campaign","title":"Donation campaign","description":"A burst of small donations during a two-day fundraising push.","eventCount":60}]}}},"4XX":{"description":"Client error, in the standard error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/dev/scenarios/{name}/events":{"get":{"operationId":"getScenarioEvents","tags":["Dev Studio"],"summary":"Event timeline and agent decisions for a scenario.","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"dispute_spike"}],"responses":{"200":{"description":"Success","headers":{"X-PayFlow-Data":{"description":"Always `synthetic` from the demo API.","schema":{"type":"string","enum":["synthetic"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioRun"},"example":{"name":"dispute_spike","events":[{"id":"dispute_spike_0011","derivedType":"DisputeOpened","timestamp":"2026-09-30T22:12:25.282Z","customerId":"cust_d11","subscriptionId":"sub_d11","amount":44,"currency":"USD","scenarioTag":"dispute_spike","playbookTriggered":false,"riskFlag":false}],"decisions":[{"id":"dec_1","at":1790890798280,"rule":"3+ disputes within 24 hours","reasoning":"Dispute volume is well above normal.","action":"Alert the merchant and draft a proactive customer message","eventId":"dispute_spike_0003"}]}}}},"404":{"description":"scenario_not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","examples":["subscription_not_found"]},"message":{"type":"string","examples":["No subscription with id sub_999."]},"details":{"type":"string"}}}}},"LifecycleSummary":{"type":"object","required":["activeSubscriptions","monthlyRecurringRevenue","churnRate30d","highRiskSubscriptions","atRiskMrr","dataMode"],"properties":{"activeSubscriptions":{"type":"integer","description":"Subscriptions that are active or past due."},"monthlyRecurringRevenue":{"type":"number","description":"Sum of monthly prices of active and past-due subscriptions."},"churnRate30d":{"type":"number","description":"Cancelled in the last 30 days divided by (live + cancelled in 30 days). A fraction from 0 to 1."},"highRiskSubscriptions":{"type":"integer"},"atRiskMrr":{"type":"number","description":"Monthly price total of the high-risk subscriptions."},"dataMode":{"type":"string","enum":["synthetic","live"],"description":"`synthetic` without an API key, `live` with a valid key."}}},"RiskLevel":{"type":"string","enum":["low","medium","high","churned"]},"HighRiskSubscription":{"type":"object","required":["id","customerId","customer","planName","price","currency","status","lastPaymentDate","riskLevel","riskReason"],"properties":{"id":{"type":"string","examples":["sub_005"]},"customerId":{"type":"string","examples":["cust_005"]},"customer":{"type":"object","properties":{"email":{"type":"string","examples":["theo.larsen4@example.com"]},"name":{"type":"string","examples":["Theo Larsen"]}}},"planName":{"type":"string","examples":["Team"]},"price":{"type":"number","examples":[49]},"currency":{"type":"string","examples":["USD"]},"status":{"type":"string","enum":["active","past_due","cancelled","churned"]},"lastPaymentDate":{"type":["string","null"],"format":"date-time"},"riskLevel":{"$ref":"#/components/schemas/RiskLevel"},"riskReason":{"type":"string","examples":["2 failed renewals in 30 days."]}}},"SubscriptionEvent":{"type":"object","required":["id","derivedType","type","amount","currency","timestamp"],"properties":{"id":{"type":"string","examples":["evt_00002"]},"derivedType":{"type":"string","enum":["SubscriptionCreated","PaymentSucceeded","PaymentFailed","SubscriptionCancelled","SubscriptionSuspended","DisputeOpened","PlaybookTriggered","Unknown"],"description":"PayFlow's normalized event type."},"type":{"type":"string","examples":["PAYMENT.SALE.COMPLETED"],"description":"The raw PayPal event type."},"amount":{"type":["number","null"]},"currency":{"type":["string","null"]},"timestamp":{"type":"string","format":"date-time"}}},"SubscriptionEvents":{"type":"object","required":["subscriptionId","events"],"properties":{"subscriptionId":{"type":"string","examples":["sub_001"]},"events":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionEvent"}}}},"RecoverRequest":{"type":"object","required":["subscriptionIds"],"properties":{"subscriptionIds":{"type":"array","minItems":1,"maxItems":25,"items":{"type":"string","examples":["sub_001"]},"description":"Ids from /api/subscriptions/high-risk."}}},"EmailDecision":{"type":"object","properties":{"churnProbability":{"type":"number","minimum":0,"maximum":1},"tone":{"type":"string","enum":["friendly","urgent","concise"]},"cause":{"type":["string","null"],"description":"Likely cause from Jev, or null when the rules baseline decided."},"source":{"type":"string","enum":["jev","rules"],"description":"Which system made the decision."}}},"GeneratedEmail":{"type":"object","required":["subscriptionId","subject","body","writtenBy","decision"],"properties":{"subscriptionId":{"type":"string","examples":["sub_001"]},"subject":{"type":"string","examples":["We couldn't process your last payment"]},"body":{"type":"string","examples":["Hi Maya,\n\nIt looks like your latest $9.00 payment for Starter didn't go through. ..."]},"writtenBy":{"type":"string","enum":["openai","claude","template"],"description":"Which writer produced the text. Template is the always-available baseline."},"decision":{"$ref":"#/components/schemas/EmailDecision"}}},"Playbook":{"type":"object","required":["playbookId","status","generatedEmails"],"properties":{"playbookId":{"type":"string","examples":["pb_muwssxw4kl"]},"status":{"type":"string","enum":["awaiting_approval"],"description":"Drafts are never sent from the API. A person approves in the app."},"generatedEmails":{"type":"array","items":{"$ref":"#/components/schemas/GeneratedEmail"}}}},"StoredPlaybook":{"type":"object","required":["id","name","status","createdAt","generatedEmails"],"properties":{"id":{"type":"string","examples":["pb_muwssxw4kl"]},"name":{"type":"string","examples":["RecoverFailedPayments"]},"status":{"type":"string","examples":["awaiting_approval"]},"createdAt":{"type":"string","format":"date-time"},"generatedEmails":{"type":"array","items":{"$ref":"#/components/schemas/GeneratedEmail"}}}},"ScanRequest":{"type":"object","required":["storeUrl"],"properties":{"storeUrl":{"type":"string","examples":["https://example-store.com"],"description":"A public store address. A missing scheme is treated as https."}}},"Finding":{"type":"object","required":["id","dimension","ok","severity","text"],"properties":{"id":{"type":"string","examples":["llms_txt"]},"dimension":{"type":"string","enum":["discoverability","catalog","trust","checkout"]},"ok":{"type":"boolean"},"severity":{"type":"string","enum":["info","warn","critical"]},"text":{"type":"string","examples":["No llms.txt found"]},"fixId":{"type":"string","examples":["llms_txt"],"description":"Present when a copy-ready fix exists in suggestedFixes."}}},"FixSnippet":{"type":"object","required":["id","title","why","snippet"],"properties":{"id":{"type":"string","examples":["llms_txt"]},"title":{"type":"string","examples":["Add an llms.txt file"]},"why":{"type":"string","examples":["AI shopping agents look here first for a plain-language map of your store."]},"snippet":{"type":"string","examples":["# example.com\n> One sentence describing what you sell."]}}},"ReadinessResult":{"type":"object","required":["auditId","storeUrl","overallScore","label","scores","findings","suggestedFixes","createdAt"],"properties":{"auditId":{"type":"string","examples":["ra_muwst0k7"]},"storeUrl":{"type":"string","examples":["https://example.com/"]},"overallScore":{"type":"integer","minimum":0,"maximum":100},"label":{"type":"string","enum":["Agent-ready","Partially agent-ready","Hard for agents to use","Invisible to agents"]},"scores":{"type":"object","description":"Each dimension from 0 to 1.","required":["discoverability","catalog","trust","checkout"],"properties":{"discoverability":{"type":"number","examples":[0]},"catalog":{"type":"number","examples":[0]},"trust":{"type":"number","examples":[0]},"checkout":{"type":"number","examples":[0.25]}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}},"suggestedFixes":{"type":"array","items":{"$ref":"#/components/schemas/FixSnippet"}},"createdAt":{"type":"string","format":"date-time"}}},"Scenario":{"type":"object","required":["name","title","description","eventCount"],"properties":{"name":{"type":"string","enum":["subscription_churn_storm","dispute_spike","donation_campaign"]},"title":{"type":"string","examples":["Dispute spike"]},"description":{"type":"string","examples":["A cluster of disputes opened within three days of a shipping delay."]},"eventCount":{"type":"integer","examples":[14]}}},"ScenarioEvent":{"type":"object","properties":{"id":{"type":"string","examples":["dispute_spike_0011"]},"derivedType":{"type":"string","examples":["DisputeOpened"]},"timestamp":{"type":"string","format":"date-time"},"customerId":{"type":["string","null"]},"subscriptionId":{"type":["string","null"]},"amount":{"type":["number","null"]},"currency":{"type":["string","null"]},"scenarioTag":{"type":"string","examples":["dispute_spike"]},"playbookTriggered":{"type":"boolean"},"riskFlag":{"type":"boolean"}}},"AgentDecision":{"type":"object","properties":{"id":{"type":"string","examples":["dec_1"]},"at":{"type":"integer","description":"Unix time in milliseconds."},"rule":{"type":"string","examples":["3+ disputes within 24 hours"]},"reasoning":{"type":"string","examples":["Dispute volume is well above normal."]},"action":{"type":"string","examples":["Alert the merchant and draft a proactive customer message"]},"eventId":{"type":"string","examples":["dispute_spike_0003"]},"playbookId":{"type":"string"}}},"ScenarioRun":{"type":"object","required":["name","events","decisions"],"properties":{"name":{"type":"string","examples":["dispute_spike"]},"events":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioEvent"}},"decisions":{"type":"array","items":{"$ref":"#/components/schemas/AgentDecision"}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Optional on the three lifecycle read endpoints. Create a read-only key in Settings. Send it only in the Authorization header, never in a URL. Without a key those endpoints return synthetic demo data."}}}}