{
  "openapi": "3.1.0",
  "info": {
    "title": "Heartbreak Hedge API",
    "version": "0.2.0-planned",
    "summary": "Let a personal agent find, set up and manage a Heartbreak Hedge subscription for its owner.",
    "description": "PLANNED. Nothing here is live and the server below does not exist yet. Agents may read, prepare and manage; the owner approves identity, age, location, payment and any increase in money at risk. Every heartbreak payout goes to charity: Heartbreak Hedge is the named donor and the owner never receives winnings. Leg status values: scheduled, cracked, held, dropped. Ticket outcomes: open, heart_held, heartbreak. A leg never 'wins'."
  },
  "servers": [
    {
      "url": "https://api.heartbreakhedge.com/v1",
      "description": "Planned. Not live."
    }
  ],
  "x-status": "planned",
  "tags": [
    {
      "name": "Public",
      "description": "No sign-in"
    },
    {
      "name": "Subscribe",
      "description": "Agent prepares, owner approves"
    },
    {
      "name": "Manage",
      "description": "Scoped agent token after approval"
    },
    {
      "name": "Events"
    }
  ],
  "paths": {
    "/plans": {
      "get": {
        "operationId": "getPlans",
        "summary": "Plans, fee and rules",
        "description": "Same content as plans.json.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [],
        "tags": [
          "Public"
        ]
      }
    },
    "/teams": {
      "get": {
        "operationId": "listTeams",
        "summary": "Supported teams",
        "description": "Teams the service can schedule and price.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Team"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "league",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "NFL",
                "NCAAF",
                "NCAAB",
                "EPL"
              ]
            }
          }
        ],
        "tags": [
          "Public"
        ]
      }
    },
    "/charities": {
      "get": {
        "operationId": "listCharities",
        "summary": "Planned charities",
        "description": "A planned list; none is a partner yet.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Charity"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "tags": [
          "Public"
        ]
      }
    },
    "/quote": {
      "post": {
        "operationId": "quoteWeek",
        "summary": "Preview next week",
        "description": "Sample ticket for the chosen teams and amount: legs, prices, what a heartbreak would pay and an estimated yearly cost. Creates nothing.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Preferences"
              }
            }
          }
        },
        "tags": [
          "Public"
        ]
      }
    },
    "/subscription-intents": {
      "post": {
        "operationId": "createSubscriptionIntent",
        "summary": "Prepare a subscription for the owner",
        "description": "Creates a draft with the owner's choices and returns an approval_url the agent sends to its owner. Nothing is charged and no account is opened until the owner finishes the checks, adds payment and confirms on that page. The owner also grants this agent its scopes there (OAuth 2.1 authorization code with PKCE).",
        "x-status": "planned",
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionIntent"
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Preferences"
                  },
                  {
                    "type": "object",
                    "required": [
                      "owner_email",
                      "agent"
                    ],
                    "properties": {
                      "owner_email": {
                        "type": "string",
                        "format": "email"
                      },
                      "agent": {
                        "$ref": "#/components/schemas/AgentIdentity"
                      },
                      "requested_scopes": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Subscribe"
        ]
      }
    },
    "/subscription-intents/{intent_id}": {
      "get": {
        "operationId": "getSubscriptionIntent",
        "summary": "Check the owner's approval",
        "description": "Poll, or use the subscription.approved event.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionIntent"
                }
              }
            }
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "intent_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "tags": [
          "Subscribe"
        ]
      }
    },
    "/subscription": {
      "get": {
        "operationId": "getSubscription",
        "summary": "Current subscription",
        "description": "Teams, weekly amount, charities and how gifts split among them, limits and state.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:read"
            ]
          }
        ],
        "tags": [
          "Manage"
        ]
      },
      "patch": {
        "operationId": "updateSubscription",
        "summary": "Change preferences",
        "description": "Teams, charities, how gifts split among charities, and lowering the amount apply from next week. Raising the amount or a limit returns 202 with an approval_url for the owner.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "202": {
            "description": "Needs the owner. Returns an approval_url to send them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerApproval"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Preferences"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/subscription/pause": {
      "post": {
        "operationId": "pauseSubscription",
        "summary": "Pause",
        "description": "Takes effect at once. No ticket is placed while paused.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "until": {
                    "type": "string",
                    "format": "date"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/subscription/resume": {
      "post": {
        "operationId": "resumeSubscription",
        "summary": "Resume",
        "description": "Starts again from next week.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/subscription/cancel": {
      "post": {
        "operationId": "cancelSubscription",
        "summary": "Cancel",
        "description": "Takes effect at once. An open ticket runs to the end of its week.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/subscription/limits": {
      "post": {
        "operationId": "setLimits",
        "summary": "Set limits or self-exclude",
        "description": "Lowering a limit or self-excluding takes effect at once. Raising a limit needs the owner.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "202": {
            "description": "Needs the owner. Returns an approval_url to send them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerApproval"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "subscription:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "weekly_max": {
                    "type": "number"
                  },
                  "monthly_max": {
                    "type": "number"
                  },
                  "self_exclude_until": {
                    "type": "string",
                    "format": "date"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string. Retries with the same key return the first result."
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/tickets/current": {
      "get": {
        "operationId": "getCurrentTicket",
        "summary": "This week's ticket",
        "description": "Legs, prices, stake, what a heartbreak pays, and each leg's status.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "tickets:read"
            ]
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/tickets": {
      "get": {
        "operationId": "listTickets",
        "summary": "Past tickets",
        "description": "Every week's ticket and outcome.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Ticket"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "tickets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "season",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "team",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/giving": {
      "get": {
        "operationId": "getGiving",
        "summary": "Giving totals",
        "description": "What Heartbreak Hedge gave on the owner's behalf, per charity, this month and all time.",
        "x-status": "planned",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Giving"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "giving:read"
            ]
          }
        ],
        "tags": [
          "Manage"
        ]
      }
    },
    "/webhooks": {
      "post": {
        "operationId": "createWebhook",
        "summary": "Subscribe to events",
        "description": "Events: subscription.approved, ticket.placed, leg.cracked, ticket.heart_held, ticket.heartbreak, payout.sent, gift.sent. Signed with HMAC-SHA256.",
        "x-status": "planned",
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        },
        "security": [
          {
            "oauth": [
              "events:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "Events"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth": {
        "type": "oauth2",
        "description": "Granted by the owner on the approval page. Agents register with OAuth dynamic client registration and are listed, and revocable, in the owner's settings.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://heartbreakhedge.com/oauth/authorize",
            "tokenUrl": "https://api.heartbreakhedge.com/oauth/token",
            "scopes": {
              "subscription:read": "Read the subscription",
              "subscription:write": "Change teams and charities, lower the amount, pause, cancel, set lower limits",
              "tickets:read": "Read tickets and leg status",
              "giving:read": "Read giving totals",
              "events:write": "Register webhooks"
            }
          }
        }
      }
    },
    "schemas": {
      "Team": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "soccer_draw_holds": {
            "type": "boolean"
          }
        }
      },
      "Charity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "cause": {
            "type": "string"
          },
          "partner": {
            "type": "boolean"
          }
        }
      },
      "Preferences": {
        "type": "object",
        "properties": {
          "teams": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "weekly_amount": {
            "type": "number",
            "description": "USD, fixed, charged only in weeks with a ticket"
          },
          "charities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Charity ids from /charities. Every dollar of a heartbreak payout goes to these; the owner never receives winnings."
          },
          "charity_split": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Optional percent per charity id, summing to 100. Omitted means an even split."
          }
        }
      },
      "AgentIdentity": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "operator": {
            "type": "string"
          },
          "client_id": {
            "type": "string"
          },
          "contact": {
            "type": "string"
          }
        }
      },
      "OwnerApproval": {
        "type": "object",
        "properties": {
          "approval_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "SubscriptionIntent": {
        "type": "object",
        "properties": {
          "intent_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_owner",
              "approved",
              "declined",
              "expired",
              "ineligible"
            ]
          },
          "approval_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "subscription_id": {
            "type": "string"
          },
          "preferences": {
            "$ref": "#/components/schemas/Preferences"
          }
        }
      },
      "Subscription": {
        "type": "object",
        "properties": {
          "subscription_id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "cancelled",
              "self_excluded"
            ]
          },
          "preferences": {
            "$ref": "#/components/schemas/Preferences"
          },
          "fee": {
            "type": [
              "number",
              "null"
            ]
          },
          "limits": {
            "type": "object"
          },
          "connected_agents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentIdentity"
            }
          }
        }
      },
      "Leg": {
        "type": "object",
        "properties": {
          "team": {
            "type": "string"
          },
          "opponent": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "price": {
            "type": "string",
            "description": "American odds on the owner's team losing"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "cracked",
              "held",
              "dropped"
            ]
          }
        }
      },
      "Ticket": {
        "type": "object",
        "properties": {
          "week": {
            "type": "string"
          },
          "sportsbook": {
            "type": "string"
          },
          "stake": {
            "type": "number"
          },
          "fee": {
            "type": [
              "number",
              "null"
            ]
          },
          "heartbreak_pays": {
            "type": "number"
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Leg"
            }
          },
          "outcome": {
            "type": "string",
            "enum": [
              "open",
              "heart_held",
              "heartbreak"
            ]
          },
          "payout": {
            "type": "object",
            "description": "A heartbreak's whole payout, all of it to charity. Heartbreak Hedge is the donor.",
            "properties": {
              "charity_total": {
                "type": "number"
              },
              "by_charity": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "charity": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "sample_ticket": {
            "$ref": "#/components/schemas/Ticket"
          },
          "estimated_yearly_cost": {
            "type": "number"
          },
          "as_of": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Giving": {
        "type": "object",
        "properties": {
          "this_month": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "charity": {
                  "type": "string"
                },
                "amount": {
                  "type": "number"
                }
              }
            }
          },
          "all_time": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "charity": {
                  "type": "string"
                },
                "amount": {
                  "type": "number"
                }
              }
            }
          },
          "donor": {
            "type": "string",
            "const": "Heartbreak Hedge"
          }
        },
        "description": "Shown to the owner as \"Heartbreak Hedge gave $X on your behalf\" per charity."
      }
    }
  }
}
