{
  "openapi": "3.0.3",
  "info": {
    "title": "Balance & Billing API",
    "version": "1.0.0",
    "description": "Read credit balance and purchase top-ups for the authenticated team."
  },
  "servers": [
    {
      "url": "https://api.olostep.com"
    }
  ],
  "tags": [
    {
      "name": "Balance & Billing",
      "description": "Credit balance and top-up purchase endpoints."
    }
  ],
  "components": {
    "securitySchemes": {
      "Authorization": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer authentication header of the form Bearer <token>, where <token> is your auth token."
      }
    },
    "schemas": {
      "CreditsBreakdownItem": {
        "type": "object",
        "properties": {
          "purchase_kind": {
            "type": "string",
            "enum": [
              "Subscription",
              "Top-up",
              "Manual",
              "Setup",
              "Pending"
            ],
            "description": "How the credit lot was issued."
          },
          "allocated_units": {
            "type": "integer",
            "description": "Credits originally allocated to this lot."
          },
          "remaining_units": {
            "type": "integer",
            "description": "Credits still available in this lot."
          },
          "expiry_date": {
            "type": "integer",
            "description": "Unix timestamp when this lot expires."
          }
        }
      },
      "ActiveSubscription": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Subscription product id (for example `SUB_PRO`). Falls back to `SUB_BASE` when no active subscription exists."
          },
          "display_name": {
            "type": "string",
            "description": "Human-readable plan name."
          },
          "credits": {
            "type": "integer",
            "description": "Credits included in the active plan."
          },
          "created_at": {
            "type": "integer",
            "description": "Unix timestamp when the subscription was created."
          }
        }
      },
      "CreditsInfoResponse": {
        "type": "object",
        "properties": {
          "credits": {
            "type": "integer",
            "description": "Total remaining credits across all non-expired lots."
          },
          "breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreditsBreakdownItem"
            },
            "description": "Per-lot credit detail."
          },
          "active_subscription": {
            "$ref": "#/components/schemas/ActiveSubscription"
          },
          "allow_usage": {
            "type": "boolean",
            "description": "Whether the team can still consume credits."
          }
        }
      },
      "PurchaseTopupRequest": {
        "type": "object",
        "required": [
          "credits"
        ],
        "properties": {
          "credits": {
            "type": "integer",
            "enum": [
              10000,
              20000,
              80000,
              100000
            ],
            "description": "Number of credits to purchase. Allowed values: **10,000**, **20,000**, **80,000**, or **100,000**."
          }
        }
      },
      "PurchaseTopupSuccess": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "payment_intent_id": {
            "type": "string",
            "description": "Stripe PaymentIntent id for the charge."
          },
          "credits": {
            "type": "integer",
            "description": "Credits purchased in this request."
          }
        }
      },
      "PurchaseTopupProcessing": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ]
          },
          "payment_intent_id": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Human-readable note that credits will be added once payment is confirmed."
          },
          "credits": {
            "type": "integer"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code."
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds until another purchase attempt is allowed (cooldown responses only)."
          },
          "cooldown_seconds": {
            "type": "integer",
            "description": "Cooldown window in seconds (cooldown responses only)."
          }
        }
      }
    }
  },
  "paths": {
    "/user/credits/info": {
      "get": {
        "tags": [
          "Balance & Billing"
        ],
        "summary": "Get credit info",
        "description": "Returns the authenticated team's credit balance, per-lot breakdown, active subscription, and whether usage is allowed. Useful for billing widgets, usage dashboards, and pre-flight checks before large jobs.",
        "security": [
          {
            "Authorization": []
          }
        ],
        "responses": {
          "200": {
            "description": "Credit balance and billing context for the authenticated team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsInfoResponse"
                },
                "example": {
                  "credits": 12500,
                  "breakdown": [
                    {
                      "purchase_kind": "Subscription",
                      "allocated_units": 10000,
                      "remaining_units": 8500,
                      "expiry_date": 1735689600
                    },
                    {
                      "purchase_kind": "Top-up",
                      "allocated_units": 5000,
                      "remaining_units": 4000,
                      "expiry_date": 1743465600
                    }
                  ],
                  "active_subscription": {
                    "id": "SUB_PRO",
                    "display_name": "Pro",
                    "credits": 10000,
                    "created_at": 1704067200
                  },
                  "allow_usage": true
                }
              }
            }
          },
          "402": {
            "description": "Invalid API key."
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "Internal Server Error"
                }
              }
            }
          }
        }
      }
    },
    "/user/purchase-topup": {
      "post": {
        "tags": [
          "Balance & Billing"
        ],
        "summary": "Purchase top-up",
        "description": "Charges a saved card on Stripe and purchases credits in one step. There is no Checkout redirect or embedded payment UI.\n\n**Requirements:**\n- The team must have a Stripe customer with at least one saved card.\n- Only one purchase attempt is allowed every **60 seconds** per team.\n\n**Credit issuance:** Credits are added by the Stripe webhook after payment confirmation. Poll `GET /user/credits/info` to confirm the updated balance.",
        "security": [
          {
            "Authorization": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PurchaseTopupRequest"
              },
              "example": {
                "credits": 10000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseTopupSuccess"
                },
                "example": {
                  "success": true,
                  "payment_intent_id": "pi_xxx",
                  "credits": 10000
                }
              }
            }
          },
          "202": {
            "description": "Payment is processing. Credits are added once Stripe confirms the payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseTopupProcessing"
                },
                "example": {
                  "success": true,
                  "status": "processing",
                  "payment_intent_id": "pi_xxx",
                  "message": "Payment is processing. Credits will be added once the payment is confirmed.",
                  "credits": 10000
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. Common errors: `missing_topup_selector`, `invalid_credits`, `no_stripe_customer`, `no_payment_method`, `topup_not_available`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "missing_topup_selector": {
                    "value": {
                      "error": "missing_topup_selector"
                    }
                  },
                  "invalid_credits": {
                    "value": {
                      "error": "invalid_credits"
                    }
                  },
                  "no_payment_method": {
                    "value": {
                      "error": "no_payment_method"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Invalid API key, or payment failed on all saved cards (`payment_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "payment_failed"
                }
              }
            }
          },
          "404": {
            "description": "Team not found (`team_not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "team_not_found"
                }
              }
            }
          },
          "429": {
            "description": "Purchase cooldown active. At most one purchase every 60 seconds per team. Includes `Retry-After` header.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until another purchase attempt is allowed."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "purchase_topup_cooldown",
                  "retry_after": 42,
                  "cooldown_seconds": 60
                }
              }
            }
          },
          "500": {
            "description": "Internal server error."
          },
          "503": {
            "description": "Ambiguous Stripe error (`payment_status_unknown`). Wait before retrying.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "payment_status_unknown"
                }
              }
            }
          }
        }
      }
    }
  }
}