{
  "openapi": "3.1.0",
  "info": {
    "title": "UsageGate API",
    "version": "1.0.0",
    "description": "Usage limits and entitlements for Next.js. Prefer @usagegate/sdk. Bearer API keys authenticate /api/v1/* product routes.",
    "contact": { "name": "UsageGate", "email": "amine@usagegate.io", "url": "https://www.usagegate.io" }
  },
  "servers": [
    { "url": "https://www.usagegate.io", "description": "Production" }
  ],
  "tags": [
    { "name": "entitlements" },
    { "name": "plans" },
    { "name": "rules" },
    { "name": "webhooks" },
    { "name": "ops" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key",
        "description": "Workspace API key from Dashboard → API keys: gk_live_… (production) or gk_test_… (test)."
      }
    },
    "schemas": {
      "CheckResponse": {
        "type": "object",
        "properties": {
          "allowed": { "type": "boolean" },
          "balance": { "type": "number", "nullable": true },
          "feature": { "type": "string" },
          "userId": { "type": "string" }
        },
        "required": ["allowed", "feature", "userId"]
      },
      "ConsumeRequest": {
        "type": "object",
        "properties": {
          "userId": { "type": "string" },
          "feature": { "type": "string" },
          "amount": { "type": "integer", "minimum": 1 }
        },
        "required": ["userId", "feature", "amount"]
      },
      "ConsumeResponse": {
        "type": "object",
        "properties": {
          "allowed": { "type": "boolean" },
          "remaining": { "type": "number", "nullable": true },
          "feature": { "type": "string" },
          "userId": { "type": "string" }
        },
        "required": ["allowed", "feature", "userId"]
      },
      "GrantRequest": {
        "type": "object",
        "properties": {
          "userId": { "type": "string" },
          "feature": { "type": "string" },
          "balance": { "type": "number" },
          "resetAt": { "type": "string", "format": "date-time", "nullable": true }
        },
        "required": ["userId", "feature", "balance"]
      },
      "GrantPlanRequest": {
        "type": "object",
        "properties": { "userId": {"type": "string"}, "planId": {"type": "string"} },
        "required": ["userId", "planId"]
      },
      "SubscriptionRequest": {
        "type": "object",
        "properties": {
          "eventId": { "type": "string", "maxLength": 256, "description": "Your gateway's event id. A repeat returns alreadyApplied." },
          "userId": { "type": "string", "maxLength": 256, "description": "Same id you pass to check/consume." },
          "planId": { "type": "string", "maxLength": 128, "description": "Plan node id from Access rules." },
          "status": {
            "type": "string",
            "enum": ["active", "canceled"]
          },
          "periodEnd": {
            "type": "string",
            "format": "date-time",
            "description": "REQUIRED when status is \"active\" — usually the subscription's current period end. Becomes the user's resetAt; balances expire at this time."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the gateway created the event. Older events than the last applied one are ignored as stale."
          }
        },
        "required": ["eventId", "userId", "planId", "status"],
        "description": "periodEnd is required when status is active. If the workspace has no Stripe connected, the first call switches its Payment gateway to Other automatically. If Stripe is connected, returns 409."
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string", "description": "Machine-readable code, e.g. invalid_request or unknown_feature." }
        },
        "required": ["error"]
      },
      "BillingConfig": {
        "type": "object",
        "properties": {
          "connected": { "type": "boolean", "description": "A Stripe webhook secret is saved." },
          "hasWebhookSecret": { "type": "boolean" },
          "stripeAccountHint": {
            "type": ["string", "null"]
          },
          "billingProvider": {
            "type": "string",
            "enum": ["stripe", "other"]
          },
          "companyName": {
            "type": ["string", "null"]
          },
          "workspaceId": { "type": "string" }
        }
      },
      "BillingPatch": {
        "type": "object",
        "properties": {
          "stripeWebhookSecret": {
            "type": ["string", "null"],
            "description": "whsec_… signing secret. Empty string or null clears it."
          },
          "stripeSecretKey": {
            "type": ["string", "null"],
            "description": "Your sk_test_… / sk_live_… key. Empty string or null clears it."
          },
          "billingProvider": {
            "type": "string",
            "enum": ["stripe", "other"]
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/health": {
      "get": {
        "tags": ["ops"],
        "summary": "Liveness probe",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Service is up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "ok": {"type": "boolean"} }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/check": {
      "get": {
        "tags": ["entitlements"],
        "summary": "Check whether an end user can use a feature",
        "operationId": "checkAccess",
        "security": [
          { "bearerAuth": [] }
        ],
        "parameters": [
          { "name": "userId", "in": "query", "required": true, "schema": {"type": "string"} },
          {
            "name": "feature",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "Feature key from Access rules (e.g. ai_credits)"
          }
        ],
        "responses": {
          "200": {
            "description": "Access decision",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/CheckResponse"} }
            }
          },
          "400": {
            "description": "Missing or invalid params, e.g. \"userId and feature are required.\" (code invalid_request)",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked API key",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "402": {
            "description": "Your own UsageGate plan quota is used up — upgrade in Dashboard → Billing. Not your end user's credits.",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "404": {
            "description": "Unknown feature key (code unknown_feature). Add it to your plan table in Access rules, or check the spelling.",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "429": {
            "description": "Rate limited — retry with backoff",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "503": {
            "description": "Temporarily unavailable — retry",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          }
        },
        "description": "No access (not on the plan, or out of credits) is 200 with allowed: false — not an error."
      }
    },
    "/api/v1/consume": {
      "post": {
        "tags": ["entitlements"],
        "summary": "Atomically consume metered entitlement",
        "operationId": "consumeFeature",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {"$ref": "#/components/schemas/ConsumeRequest"} }
          }
        },
        "responses": {
          "200": {
            "description": "Consume result. allowed: false means not on the plan or not enough balance.",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/ConsumeResponse"} }
            }
          },
          "400": {
            "description": "Missing or invalid params, e.g. amount is not a positive integer (code invalid_request)",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked API key",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "402": {
            "description": "Your own UsageGate plan quota is used up — upgrade in Dashboard → Billing. Not your end user's credits.",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "404": {
            "description": "Unknown feature key (code unknown_feature). Add it to your plan table in Access rules, or check the spelling.",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "429": {
            "description": "Rate limited — retry with backoff",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "503": {
            "description": "Temporarily unavailable — retry",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          }
        },
        "description": "Atomic check-and-spend: for metered features this alone is the gate. Not enough balance is 200 with allowed: false — not 402."
      }
    },
    "/api/v1/grant": {
      "post": {
        "tags": ["entitlements"],
        "summary": "Set absolute entitlement for one end user",
        "operationId": "grantFeature",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {"$ref": "#/components/schemas/GrantRequest"} }
          }
        },
        "responses": { "200": {"description": "Grant applied"}, "401": {"description": "Missing or invalid API key"} }
      }
    },
    "/api/v1/grant-plan": {
      "post": {
        "tags": ["plans"],
        "summary": "Assign a free plan and expand rules into entitlements",
        "operationId": "grantPlan",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {"$ref": "#/components/schemas/GrantPlanRequest"} }
          }
        },
        "responses": {
          "200": { "description": "Plan granted, or alreadyEnrolled: true when the user already had this plan or a paid plan (nothing changed)" },
          "400": {
            "description": "Invalid body, unknown plan, or \"Paid plans are assigned by Stripe\" — paid plans come from the Stripe webhook or reportSubscription"
          },
          "401": { "description": "Missing or invalid API key" }
        },
        "description": "Call at signup or login — new users are not enrolled automatically. Safe to repeat: a user already on this plan, or on a paid plan, is left unchanged (no refill, no downgrade). Only unpaid plans (no Stripe Price)."
      }
    },
    "/api/v1/subscription": {
      "post": {
        "tags": ["plans"],
        "summary": "Report a paid plan from a non-Stripe gateway",
        "operationId": "reportSubscription",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {"$ref": "#/components/schemas/SubscriptionRequest"} }
          }
        },
        "responses": {
          "200": { "description": "Applied, or alreadyApplied / ignored (stale | superseded)" },
          "400": { "description": "Invalid body, unknown plan, missing or invalid periodEnd" },
          "401": { "description": "Missing or invalid API key" },
          "409": { "description": "Stripe is connected and the Payment gateway is Stripe — switch to Other in Dashboard → Payment gateway" }
        },
        "description": "periodEnd is required when status is active. With no Stripe connected, the first call switches the workspace's Payment gateway to Other. With Stripe connected, returns 409."
      }
    },
    "/api/v1/rules": {
      "get": {
        "tags": ["rules"],
        "summary": "Read the Plan → Feature → Limit graph",
        "operationId": "getRules",
        "security": [
          { "bearerAuth": [] }
        ],
        "responses": { "200": {"description": "Rules graph"}, "401": {"description": "Unauthorized"} }
      },
      "put": {
        "tags": ["rules"],
        "summary": "Replace the rules graph",
        "operationId": "putRules",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "nodes": {"type": "array"}, "edges": {"type": "array"} },
                "required": ["nodes", "edges"]
              }
            }
          }
        },
        "responses": { "200": {"description": "Rules saved"}, "401": {"description": "Unauthorized"} }
      }
    },
    "/api/v1/billing": {
      "get": {
        "tags": ["ops"],
        "summary": "Read Stripe connection status and payment gateway",
        "operationId": "getBilling",
        "description": "Used by the SDK's getStripeConfig. Never returns secrets.",
        "security": [
          { "bearerAuth": [] }
        ],
        "responses": {
          "200": {
            "description": "Billing config",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/BillingConfig"} }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked API key",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          }
        }
      },
      "patch": {
        "tags": ["ops"],
        "summary": "Update Stripe secrets or payment gateway",
        "operationId": "patchBilling",
        "description": "Used by the SDK's configureStripe. All fields optional.",
        "security": [
          { "bearerAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": {"$ref": "#/components/schemas/BillingPatch"} }
          }
        },
        "responses": {
          "200": {
            "description": "Updated billing config",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/BillingConfig"} }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked API key",
            "content": {
              "application/json": { "schema": {"$ref": "#/components/schemas/Error"} }
            }
          }
        }
      }
    },
    "/api/webhooks/stripe": {
      "post": {
        "tags": ["webhooks"],
        "summary": "Stripe-signed webhook for founder Stripe accounts",
        "operationId": "stripeWebhook",
        "description": "Endpoint URL: https://www.usagegate.io/api/webhooks/stripe. Handles invoice.paid and customer.subscription.* events. Requires Stripe-Signature. Only needed if you bill with Stripe.",
        "responses": { "200": {"description": "Event processed"}, "400": {"description": "Invalid signature or payload"} }
      }
    }
  },
  "externalDocs": { "description": "HTTP API docs", "url": "https://www.usagegate.io/docs/api" }
}
