{
  "openapi": "3.1.0",
  "info": {
    "title": "UDS Protocol Simulator API",
    "version": "1.0.0",
    "summary": "HTTP API behind udssimulator.com.",
    "description": "Public HTTP surface of the UDS Protocol Simulator (https://udssimulator.com), a browser-based simulator for the ISO 14229 Unified Diagnostic Services protocol.\n\nThe UDS simulation itself runs entirely client-side; this API covers the surrounding account, billing, donation, and support functionality. Endpoints that exist only for gateway webhooks or scheduled maintenance are intentionally not documented here — they are not callable by third parties.\n\nEvery non-2xx response uses the same `Error` envelope, which carries a stable machine-readable `code`, a human-readable `message`, and a `hint` describing how to resolve it.",
    "contact": {
      "name": "UDS Protocol Simulator Support",
      "url": "https://udssimulator.com/support"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://udssimulator.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://udssimulator.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "UDS protocol documentation and LLM-friendly index",
    "url": "https://udssimulator.com/llms.txt"
  },
  "tags": [
    {
      "name": "Geo",
      "description": "Locale and currency detection."
    },
    {
      "name": "Donations",
      "description": "One-off donations via Razorpay and PayPal."
    },
    {
      "name": "Support",
      "description": "Support and contact messages."
    },
    {
      "name": "Billing",
      "description": "Subscription checkout and cancellation. Requires a Supabase bearer token."
    },
    {
      "name": "Invoices",
      "description": "Invoice creation, listing, and reminders. Requires a Supabase bearer token."
    }
  ],
  "paths": {
    "/api/geo-lookup": {
      "get": {
        "tags": [
          "Geo"
        ],
        "operationId": "geoLookup",
        "summary": "Resolve the caller's country and preferred billing currency",
        "description": "Derives country, region, and city from edge geolocation headers and maps the country to a billing currency (INR for India, EUR for the euro zone, USD otherwise). No authentication required.",
        "security": [],
        "parameters": [
          {
            "name": "testCountry",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2 country code that overrides the detected country. Intended for testing.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2,
              "example": "IN"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detected location and currency.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "country",
                    "region",
                    "city",
                    "currency",
                    "success"
                  ],
                  "properties": {
                    "country": {
                      "type": "string",
                      "description": "ISO 3166-1 alpha-2 code, or UNKNOWN.",
                      "example": "IN"
                    },
                    "region": {
                      "type": "string",
                      "example": "KA"
                    },
                    "city": {
                      "type": "string",
                      "example": "Bengaluru"
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "INR",
                        "EUR",
                        "USD"
                      ]
                    },
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/donations": {
      "post": {
        "tags": [
          "Donations"
        ],
        "operationId": "donations",
        "summary": "Create or verify a donation payment",
        "description": "Single endpoint covering the whole donation flow for both gateways. The `action` query parameter selects the operation, and the request body shape depends on it. No authentication required.\n\nRazorpay flow: `create-razorpay` then `verify-razorpay`.\nPayPal flow: `paypal-create` then `paypal-capture`.",
        "security": [],
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": true,
            "description": "Which step of the donation flow to run.",
            "schema": {
              "type": "string",
              "enum": [
                "create-razorpay",
                "verify-razorpay",
                "paypal-create",
                "paypal-capture"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/CreateRazorpayDonation"
                  },
                  {
                    "$ref": "#/components/schemas/VerifyRazorpayDonation"
                  },
                  {
                    "$ref": "#/components/schemas/CreatePayPalDonation"
                  },
                  {
                    "$ref": "#/components/schemas/CapturePayPalDonation"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The order was created, or the payment was verified and recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "orderId": {
                      "type": "string",
                      "description": "Gateway order id."
                    },
                    "paymentId": {
                      "type": "string",
                      "description": "Razorpay payment id (verify-razorpay only)."
                    },
                    "captureId": {
                      "type": "string",
                      "description": "PayPal capture id (paypal-capture only)."
                    },
                    "amount": {
                      "description": "Paise for INR, or a decimal string for USD.",
                      "oneOf": [
                        {
                          "type": "integer"
                        },
                        {
                          "type": "string"
                        }
                      ]
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "INR",
                        "USD"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_AMOUNT, MISSING_PARAMETER, INVALID_PAYMENT_SIGNATURE, PAYMENT_NOT_CAPTURED, CAPTURE_FAILED, or UNKNOWN_ACTION."
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError",
            "description": "PAYPAL_ORDER_FAILED — the gateway rejected the order."
          }
        }
      }
    },
    "/api/emails": {
      "post": {
        "tags": [
          "Support"
        ],
        "operationId": "sendSupportEmail",
        "summary": "Send a support or contact message",
        "description": "Delivers a support message to the site operators. Only `action=support` is part of the public surface; the other actions this route accepts (`confirmation`, `invoice`, `renewal-reminder`) are internal and require a shared server secret.",
        "security": [],
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "support"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupportMessage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Delivery provider message id."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_JSON, MISSING_PARAMETER, or UNKNOWN_ACTION."
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "EMAIL_SEND_FAILED or INTERNAL_ERROR."
          }
        }
      }
    },
    "/api/create-razorpay-order": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "createRazorpayOrder",
        "summary": "Start subscription checkout",
        "description": "Creates a Razorpay order (or, with `mode: \"subscription\"` / `action=subscription`, a recurring Razorpay subscription) for the authenticated user. The price is resolved server-side from the plan catalogue — any amount sent by the client is ignored.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": false,
            "description": "Set to `subscription` to create a recurring mandate instead of a one-time order.",
            "schema": {
              "type": "string",
              "enum": [
                "subscription"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order created. Hand `orderId` to the Razorpay checkout widget.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "orderId",
                    "amount",
                    "currency"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "orderId": {
                      "type": "string",
                      "example": "order_MkQ1a2b3c4d5e6"
                    },
                    "amount": {
                      "type": "integer",
                      "description": "Smallest currency unit (paise for INR, cents for USD/EUR)."
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "INR",
                        "USD",
                        "EUR"
                      ]
                    },
                    "planId": {
                      "type": "string",
                      "example": "pro"
                    },
                    "planName": {
                      "type": "string",
                      "example": "PRO"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_USER_ID, INVALID_PLAN, INVALID_BILLING_INTERVAL, MISSING_PARAMETER, or ALREADY_SUBSCRIBED."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "RECURRING_UNAVAILABLE — recurring billing is not offered for this plan and currency. Fall back to the one-time order flow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/verify-razorpay-payment": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "verifyRazorpayPayment",
        "summary": "Verify a completed payment and activate the subscription",
        "description": "Validates the Razorpay HMAC signature, re-confirms the payment server-to-server, checks the paid amount against the plan price, then records the payment and activates the subscription.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyPaymentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment verified and subscription activated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "subscription": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_PAYMENT_SIGNATURE, PAYMENT_NOT_CAPTURED, PAYMENT_AMOUNT_MISMATCH, or INVALID_PLAN."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "RECORD_FAILED, SUBSCRIPTION_ACTIVATION_FAILED, or PROFILE_UPDATE_FAILED. Do not retry the payment — contact support."
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError",
            "description": "PAYMENT_VERIFICATION_FAILED — the gateway could not be reached."
          }
        }
      }
    },
    "/api/cancel-subscription": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "cancelSubscription",
        "summary": "Cancel the authenticated user's subscription",
        "description": "Cancels the active subscription. Access is retained until the current period ends; the account downgrades to the Free tier at that point.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId"
                ],
                "properties": {
                  "userId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Must match the authenticated user."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription canceled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "expiresAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_USER_ID."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "SUBSCRIPTION_LOOKUP_FAILED, SUBSCRIPTION_WRITE_FAILED, or PROFILE_UPDATE_FAILED."
          }
        }
      }
    },
    "/api/invoices": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "operationId": "listInvoices",
        "summary": "List the authenticated user's invoices",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Must match the authenticated user."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "paid",
                "issued",
                "partially_paid",
                "expired",
                "canceled",
                "deleted"
              ]
            }
          },
          {
            "name": "dateFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "dateTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice page plus aggregate statistics.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invoices",
                    "total",
                    "stats"
                  ],
                  "properties": {
                    "invoices": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "paid": {
                          "type": "integer"
                        },
                        "issued": {
                          "type": "integer"
                        },
                        "overdue": {
                          "type": "integer"
                        },
                        "totalPaidAmount": {
                          "type": "integer"
                        },
                        "totalPendingAmount": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "INVALID_USER_ID."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "INVOICE_FETCH_FAILED."
          }
        }
      },
      "post": {
        "tags": [
          "Invoices"
        ],
        "operationId": "invoiceAction",
        "summary": "Create an invoice or send a reminder",
        "description": "The `action` field in the body selects the operation. Omitting it falls back to `create` when `planId` and `amount` are present, otherwise `list`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/CreateInvoiceRequest"
                  },
                  {
                    "$ref": "#/components/schemas/SendReminderRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice created, or reminder sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "invoiceId": {
                      "type": "string"
                    },
                    "invoiceNumber": {
                      "type": "string"
                    },
                    "shortUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "status": {
                      "type": "string"
                    },
                    "cacheId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "reminderCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "MISSING_PARAMETER, INVOICE_SETTLED, or MAX_REMINDERS_REACHED."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "USER_MISMATCH or INVOICE_NOT_FOUND."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "INVOICE_CREATE_FAILED or REMINDER_FAILED."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Supabase session JWT. Send as `Authorization: Bearer <token>`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Every non-2xx response from this API uses this envelope. `error` and `code` always hold the same stable machine-readable value; `error` is retained for backwards compatibility with existing clients.",
        "required": [
          "error",
          "code",
          "message",
          "status",
          "docs"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable error code. Identical to `code`.",
            "example": "INVALID_AMOUNT"
          },
          "code": {
            "type": "string",
            "description": "Stable, machine-readable error code. Branch on this, never on `message`.",
            "example": "INVALID_AMOUNT"
          },
          "message": {
            "type": "string",
            "description": "Human-readable description of what went wrong.",
            "example": "Donation must be between 10 and 100000."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve the error. Absent when any hint would disclose server internals.",
            "example": "Send an integer amount within the documented minimum and maximum."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, repeated in the body so it survives logging and proxying.",
            "example": 400
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "URL of this specification.",
            "const": "https://udssimulator.com/openapi.json"
          }
        },
        "additionalProperties": true,
        "examples": [
          {
            "error": "INVALID_AMOUNT",
            "code": "INVALID_AMOUNT",
            "message": "Donation must be between 10 and 100000.",
            "hint": "Send an integer amount within the documented minimum and maximum.",
            "status": 400,
            "docs": "https://udssimulator.com/openapi.json"
          }
        ]
      },
      "CreateRazorpayDonation": {
        "type": "object",
        "title": "create-razorpay",
        "required": [
          "amount"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Donation amount in paise (1000 = 10 INR, 10000000 = 100000 INR)."
          },
          "donorName": {
            "type": "string",
            "maxLength": 100
          },
          "donorEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 150
          },
          "message": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "VerifyRazorpayDonation": {
        "type": "object",
        "title": "verify-razorpay",
        "required": [
          "razorpay_payment_id",
          "razorpay_order_id",
          "razorpay_signature"
        ],
        "properties": {
          "razorpay_payment_id": {
            "type": "string"
          },
          "razorpay_order_id": {
            "type": "string"
          },
          "razorpay_signature": {
            "type": "string",
            "description": "HMAC-SHA256 of `order_id|payment_id`, as returned by checkout."
          },
          "amount": {
            "type": "integer",
            "description": "Advisory only — the server uses the amount reported by the gateway."
          },
          "donorName": {
            "type": "string",
            "maxLength": 100
          },
          "donorEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 150
          },
          "message": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "CreatePayPalDonation": {
        "type": "object",
        "title": "paypal-create",
        "required": [
          "amount"
        ],
        "properties": {
          "amount": {
            "type": "number",
            "minimum": 1,
            "maximum": 1000,
            "description": "Donation amount in USD."
          },
          "donorName": {
            "type": "string",
            "maxLength": 100
          },
          "donorEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          }
        }
      },
      "CapturePayPalDonation": {
        "type": "object",
        "title": "paypal-capture",
        "required": [
          "orderId"
        ],
        "properties": {
          "orderId": {
            "type": "string",
            "description": "PayPal order id returned by paypal-create, after the payer approves it."
          },
          "donorName": {
            "type": "string",
            "maxLength": 100
          },
          "donorEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "message": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "SupportMessage": {
        "type": "object",
        "required": [
          "description",
          "email"
        ],
        "properties": {
          "type": {
            "type": "string",
            "maxLength": 50,
            "default": "General",
            "description": "Category of the request."
          },
          "subject": {
            "type": "string",
            "maxLength": 200,
            "default": "Support"
          },
          "description": {
            "type": "string",
            "maxLength": 10000,
            "description": "The message body."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Reply-to address."
          },
          "priority": {
            "type": "string",
            "maxLength": 20,
            "default": "normal",
            "enum": [
              "low",
              "normal",
              "high",
              "urgent"
            ]
          },
          "user_name": {
            "type": "string",
            "maxLength": 100,
            "default": "Anonymous"
          },
          "system_info": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional browser/environment details."
          }
        }
      },
      "CreateOrderRequest": {
        "type": "object",
        "required": [
          "userId",
          "planId"
        ],
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid",
            "description": "Must match the authenticated user."
          },
          "planId": {
            "type": "string",
            "enum": [
              "pro",
              "enterprise"
            ]
          },
          "currency": {
            "type": "string",
            "enum": [
              "INR",
              "USD",
              "EUR"
            ],
            "default": "USD"
          },
          "interval": {
            "type": "string",
            "enum": [
              "monthly",
              "yearly"
            ],
            "default": "monthly"
          },
          "mode": {
            "type": "string",
            "enum": [
              "subscription"
            ],
            "description": "Equivalent to `?action=subscription`."
          },
          "userEmail": {
            "type": "string",
            "format": "email"
          },
          "userName": {
            "type": "string"
          }
        }
      },
      "VerifyPaymentRequest": {
        "type": "object",
        "required": [
          "userId",
          "planId",
          "razorpay_payment_id",
          "razorpay_signature"
        ],
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid",
            "description": "Must match the authenticated user."
          },
          "planId": {
            "type": "string",
            "enum": [
              "pro",
              "enterprise"
            ]
          },
          "razorpay_payment_id": {
            "type": "string"
          },
          "razorpay_order_id": {
            "type": "string",
            "description": "Required for one-time orders."
          },
          "razorpay_subscription_id": {
            "type": "string",
            "description": "Required instead of `razorpay_order_id` for recurring mandates."
          },
          "razorpay_signature": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "description": "Advisory only — the server uses the amount reported by the gateway."
          },
          "currency": {
            "type": "string",
            "enum": [
              "INR",
              "USD",
              "EUR"
            ],
            "default": "INR"
          },
          "interval": {
            "type": "string",
            "enum": [
              "monthly",
              "yearly"
            ],
            "default": "monthly"
          }
        }
      },
      "CreateInvoiceRequest": {
        "type": "object",
        "title": "create",
        "required": [
          "action",
          "userId",
          "planId",
          "amount"
        ],
        "properties": {
          "action": {
            "type": "string",
            "const": "create"
          },
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "planId": {
            "type": "string",
            "enum": [
              "pro",
              "enterprise"
            ]
          },
          "amount": {
            "type": "integer",
            "description": "Smallest currency unit."
          },
          "currency": {
            "type": "string",
            "enum": [
              "INR",
              "USD",
              "EUR"
            ]
          },
          "paymentId": {
            "type": "string"
          },
          "orderId": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SendReminderRequest": {
        "type": "object",
        "title": "send-reminder",
        "required": [
          "action",
          "razorpayInvoiceId"
        ],
        "properties": {
          "action": {
            "type": "string",
            "const": "send-reminder"
          },
          "razorpayInvoiceId": {
            "type": "string"
          },
          "medium": {
            "type": "string",
            "enum": [
              "email",
              "sms"
            ],
            "default": "email"
          }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "razorpay_invoice_id": {
            "type": "string"
          },
          "invoice_number": {
            "type": "string"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "amount": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "issued",
              "partially_paid",
              "expired",
              "canceled",
              "deleted"
            ]
          },
          "short_url": {
            "type": "string",
            "format": "uri"
          },
          "issued_at": {
            "type": "string",
            "format": "date-time"
          },
          "due_date": {
            "type": "string",
            "format": "date-time"
          },
          "plan_id": {
            "type": "string"
          },
          "is_recurring": {
            "type": "boolean"
          }
        },
        "additionalProperties": true
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was rejected. Read `code` to find out which validation failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "AUTH_HEADER_MISSING, AUTH_TOKEN_INVALID, or UNAUTHORIZED — authenticate and retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "USER_MISMATCH — the bearer token does not own the requested resource.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "METHOD_NOT_ALLOWED — see this specification for the accepted methods.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "RATE_LIMITED — wait until the `reset` timestamp in the body before retrying.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "An unexpected server-side failure.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UpstreamError": {
        "description": "An upstream payment provider returned an error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "x-internal-endpoints": {
    "description": "Routes that exist in the deployment but are deliberately excluded from this specification: they are gateway webhook receivers or scheduled maintenance jobs authenticated with a shared secret, not a public API surface.",
    "paths": [
      "/api/razorpay-webhook",
      "/api/donation-webhook",
      "/api/resend-webhook",
      "/api/maintenance-usage-reset",
      "/api/indexnow-submit"
    ]
  },
  "x-error-codes": {
    "description": "Every error code this API can return, with the HTTP status it maps to, its default message, and its resolution hint. Codes marked \"internal\" are only reachable from the webhook and maintenance routes listed under x-internal-endpoints. Branch on `code`, never on `message` — messages may be reworded, codes will not.",
    "codes": {
      "METHOD_NOT_ALLOWED": {
        "status": 405,
        "message": "HTTP method not allowed for this endpoint.",
        "hint": "Check /openapi.json for the methods each endpoint accepts.",
        "scope": "public"
      },
      "INVALID_JSON": {
        "status": 400,
        "message": "Request body is not valid JSON.",
        "hint": "Send a JSON body with Content-Type: application/json.",
        "scope": "public"
      },
      "MISSING_PARAMETER": {
        "status": 400,
        "message": "A required parameter is missing.",
        "hint": "Check the requestBody schema for this endpoint in /openapi.json.",
        "scope": "public"
      },
      "UNKNOWN_ACTION": {
        "status": 400,
        "message": "Unknown value for the action query parameter.",
        "hint": "See the enum of accepted action values for this path in /openapi.json.",
        "scope": "public"
      },
      "RATE_LIMITED": {
        "status": 429,
        "message": "Too many requests.",
        "hint": "Wait until the reset timestamp in this response before retrying.",
        "scope": "public"
      },
      "AUTH_HEADER_MISSING": {
        "status": 401,
        "message": "Missing or malformed Authorization header.",
        "hint": "Send Authorization: Bearer <supabase_jwt>.",
        "scope": "public"
      },
      "AUTH_TOKEN_INVALID": {
        "status": 401,
        "message": "Session token is invalid or expired.",
        "hint": "Re-authenticate and retry with a fresh Supabase JWT.",
        "scope": "public"
      },
      "UNAUTHORIZED": {
        "status": 401,
        "message": "Caller is not authorized to use this endpoint.",
        "hint": "This endpoint is not part of the public API surface.",
        "scope": "public"
      },
      "USER_MISMATCH": {
        "status": 403,
        "message": "Authenticated user does not match the requested userId.",
        "hint": "Send the userId that belongs to the bearer token you authenticated with.",
        "scope": "public"
      },
      "INVALID_USER_ID": {
        "status": 400,
        "message": "userId is missing or is not a valid UUID.",
        "hint": "Send the Supabase user UUID as userId.",
        "scope": "public"
      },
      "INVALID_SIGNATURE": {
        "status": 401,
        "message": "Webhook signature verification failed.",
        "hint": "Sign the raw request body with the configured webhook secret.",
        "scope": "internal"
      },
      "INVALID_AMOUNT": {
        "status": 400,
        "message": "Amount is outside the accepted range.",
        "hint": "Send an integer amount within the documented minimum and maximum.",
        "scope": "public"
      },
      "INVALID_PAYMENT_SIGNATURE": {
        "status": 400,
        "message": "Payment signature verification failed.",
        "hint": "Forward razorpay_order_id, razorpay_payment_id and razorpay_signature exactly as checkout returned them.",
        "scope": "public"
      },
      "PAYMENT_NOT_CAPTURED": {
        "status": 400,
        "message": "Payment is not in a captured state.",
        "hint": "Only captured payments can be recorded. Retry once the gateway reports captured.",
        "scope": "public"
      },
      "PAYMENT_AMOUNT_MISMATCH": {
        "status": 400,
        "message": "Paid amount does not match the price of the selected plan.",
        "hint": "Create a fresh order for the plan and currency you intend to buy.",
        "scope": "public"
      },
      "PAYMENT_VERIFICATION_FAILED": {
        "status": 502,
        "message": "Could not verify the payment with the payment gateway.",
        "hint": "Retry shortly, or contact support with your order ID.",
        "scope": "public"
      },
      "CAPTURE_FAILED": {
        "status": 400,
        "message": "PayPal capture did not complete.",
        "hint": "Ensure the payer approved the order before calling capture.",
        "scope": "public"
      },
      "PAYPAL_ORDER_FAILED": {
        "status": 502,
        "message": "Could not create the PayPal order.",
        "hint": "Retry shortly. If it persists, the gateway is rejecting the request.",
        "scope": "public"
      },
      "RECORD_FAILED": {
        "status": 500,
        "message": "Payment succeeded but could not be recorded.",
        "hint": "Do not retry the payment. Contact support with your payment ID.",
        "scope": "public"
      },
      "INVALID_PLAN": {
        "status": 400,
        "message": "Unknown plan or unsupported currency.",
        "hint": "Use a planId and currency pair listed in /openapi.json.",
        "scope": "public"
      },
      "INVALID_BILLING_INTERVAL": {
        "status": 400,
        "message": "Unsupported billing interval.",
        "hint": "Use monthly or yearly.",
        "scope": "public"
      },
      "ALREADY_SUBSCRIBED": {
        "status": 400,
        "message": "An active subscription already exists for this plan.",
        "hint": "Cancel the existing subscription before purchasing the same plan again.",
        "scope": "public"
      },
      "RECURRING_UNAVAILABLE": {
        "status": 400,
        "message": "Recurring billing is not available for this plan.",
        "hint": "Use the one-time checkout flow for this plan instead.",
        "scope": "public"
      },
      "BILLING_PLAN_UNAVAILABLE": {
        "status": 500,
        "message": "Could not resolve the billing plan.",
        "hint": "Retry shortly. If it persists, the plan catalogue is misconfigured.",
        "scope": "public"
      },
      "SUBSCRIPTION_LOOKUP_FAILED": {
        "status": 500,
        "message": "Could not look up the subscription.",
        "hint": "Retry shortly.",
        "scope": "public"
      },
      "SUBSCRIPTION_WRITE_FAILED": {
        "status": 500,
        "message": "Could not persist the subscription record.",
        "hint": "Do not retry the payment. Contact support with your payment ID.",
        "scope": "public"
      },
      "SUBSCRIPTION_ACTIVATION_FAILED": {
        "status": 500,
        "message": "Payment was recorded but the subscription could not be activated.",
        "hint": "Do not retry the payment. Contact support with your payment ID.",
        "scope": "public"
      },
      "PROFILE_UPDATE_FAILED": {
        "status": 500,
        "message": "Subscription changed but the user profile could not be updated.",
        "hint": "Access may be delayed. Contact support if it does not resolve shortly.",
        "scope": "public"
      },
      "INVOICE_CREATE_FAILED": {
        "status": 500,
        "message": "Could not create the invoice.",
        "hint": "Retry shortly, or contact support with your payment ID.",
        "scope": "public"
      },
      "INVOICE_FETCH_FAILED": {
        "status": 500,
        "message": "Could not fetch invoices.",
        "hint": "Retry shortly.",
        "scope": "public"
      },
      "INVOICE_NOT_FOUND": {
        "status": 403,
        "message": "Invoice not found, or it does not belong to the authenticated user.",
        "hint": "Check the razorpayInvoiceId, and authenticate as the invoice owner.",
        "scope": "public"
      },
      "INVOICE_SETTLED": {
        "status": 400,
        "message": "Invoice is already paid.",
        "hint": "Reminders can only be sent for unpaid invoices.",
        "scope": "public"
      },
      "MAX_REMINDERS_REACHED": {
        "status": 400,
        "message": "The reminder limit for this invoice has been reached.",
        "hint": "Contact the customer directly instead of sending another reminder.",
        "scope": "public"
      },
      "REMINDER_FAILED": {
        "status": 500,
        "message": "Could not send the invoice reminder.",
        "hint": "Retry shortly.",
        "scope": "public"
      },
      "EMAIL_SEND_FAILED": {
        "status": 500,
        "message": "Could not deliver the email.",
        "hint": "Retry shortly. Repeated failures mean the address is suppressed or invalid.",
        "scope": "public"
      },
      "WEBHOOK_SECRET_MISSING": {
        "status": 500,
        "message": "Webhook secret is not configured on the server.",
        "scope": "internal"
      },
      "WEBHOOK_PROCESSING_FAILED": {
        "status": 500,
        "message": "Webhook processing failed.",
        "hint": "The sender should retry according to its own backoff policy.",
        "scope": "internal"
      },
      "UNKNOWN_WEBHOOK_SOURCE": {
        "status": 400,
        "message": "Could not identify the webhook sender.",
        "hint": "Send the gateway signature header this endpoint expects.",
        "scope": "internal"
      },
      "USAGE_RESET_FAILED": {
        "status": 500,
        "message": "Monthly usage reset failed.",
        "hint": "Retry shortly.",
        "scope": "internal"
      },
      "QUERY_FAILED": {
        "status": 500,
        "message": "A database query failed.",
        "hint": "Retry shortly.",
        "scope": "internal"
      },
      "INDEXNOW_KEY_MISSING": {
        "status": 500,
        "message": "INDEXNOW_KEY is not configured on the server.",
        "scope": "internal"
      },
      "NO_URLS_PROVIDED": {
        "status": 400,
        "message": "No URLs were provided.",
        "hint": "Send a non-empty urls or urlList array.",
        "scope": "internal"
      },
      "INVALID_URLS": {
        "status": 400,
        "message": "Every submitted URL must be absolute and on the configured host.",
        "hint": "Submit fully-qualified https URLs on this site only.",
        "scope": "internal"
      },
      "INDEXNOW_SUBMIT_FAILED": {
        "status": 502,
        "message": "IndexNow submission failed.",
        "hint": "Retry shortly.",
        "scope": "internal"
      },
      "SERVER_MISCONFIGURED": {
        "status": 500,
        "message": "Server is misconfigured.",
        "scope": "public"
      },
      "INTERNAL_ERROR": {
        "status": 500,
        "message": "Something went wrong. Please try again.",
        "hint": "Retry shortly. If it persists, contact support.",
        "scope": "public"
      }
    }
  }
}
