{
  "openapi": "3.1.0",
  "info": {
    "title": "WarpMetal Backend API",
    "version": "2.1.0",
    "description": "Complete public contract for discovering live free OS images, preparing and paying for a VPS, polling provisioning, and managing power, destructive reloads, private networks, and explicit 30-day renewal using x402 v2."
  },
  "externalDocs": {
    "description": "Human-readable API reference and curl examples",
    "url": "https://warpmetal.com/docs"
  },
  "servers": [{ "url": "https://api.warpmetal.com" }],
  "components": {
    "securitySchemes": {
      "ownerToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Opaque owner token returned when an order is prepared."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "minLength": 8, "maxLength": 160 }
      },
      "PaymentSignature": {
        "name": "PAYMENT-SIGNATURE",
        "in": "header",
        "required": false,
        "schema": { "type": "string" }
      },
      "TaskId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Opaque task ID returned by POST /api/orders.",
        "schema": { "type": "string", "pattern": "^task_" }
      },
      "OperationId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Opaque operation ID returned by a lifecycle mutation.",
        "schema": { "type": "string", "pattern": "^op_" }
      }
    },
    "schemas": {
      "PlanId": {
        "type": "string",
        "enum": ["standard", "agent", "build", "heavy"]
      },
      "OperatingSystem": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "label", "monthlyPrice", "sshKey", "cloudInit"],
        "properties": {
          "name": { "type": "string", "description": "Exact value to send as osName for this plan. Never substitute label or a guessed version." },
          "label": { "type": "string", "description": "Human-readable display name." },
          "monthlyPrice": { "type": "number", "const": 0, "description": "Only zero-cost images are published." },
          "sshKey": { "type": "boolean", "const": true, "description": "The image accepts the submitted SSH public key." },
          "cloudInit": { "type": "boolean", "description": "Image capability only; the public order API does not accept cloud-init data." }
        }
      },
      "Region": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "label"],
        "properties": {
          "name": { "type": "string", "enum": ["us"] },
          "label": { "type": "string", "example": "United States" }
        }
      },
      "CatalogProduct": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "resourceClass", "name", "priceUsd", "cpu", "memoryGb", "diskGb", "bandwidthTb", "termDays", "workload", "featured", "operatingSystems", "regions"],
        "properties": {
          "id": { "$ref": "#/components/schemas/PlanId" },
          "resourceClass": { "type": "string", "enum": ["vps.standard", "vps.agent", "vps.build", "vps.heavy"] },
          "name": { "type": "string" },
          "priceUsd": { "type": "number" },
          "cpu": { "type": "integer" },
          "memoryGb": { "type": "integer" },
          "diskGb": { "type": "integer" },
          "bandwidthTb": { "type": "integer" },
          "termDays": { "type": "integer", "const": 30 },
          "workload": { "type": "string" },
          "featured": { "type": "boolean" },
          "operatingSystems": { "type": "array", "items": { "$ref": "#/components/schemas/OperatingSystem" } },
          "regions": { "type": "array", "items": { "$ref": "#/components/schemas/Region" } }
        }
      },
      "Catalog": {
        "type": "object",
        "additionalProperties": false,
        "required": ["products", "refreshedAt"],
        "properties": {
          "products": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogProduct" } },
          "refreshedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PrepareOrder": {
        "type": "object",
        "required": ["planId", "hostname", "osName", "sshPublicKey"],
        "additionalProperties": false,
        "properties": {
          "planId": { "$ref": "#/components/schemas/PlanId" },
          "hostname": { "type": "string", "maxLength": 63 },
          "osName": { "type": "string", "description": "Exact free OS name from GET /api/catalog." },
          "sshPublicKey": { "type": "string", "description": "OpenSSH RSA, Ed25519, or ECDSA public key." },
          "email": { "type": ["string", "null"], "format": "email" }
        }
      },
      "Checkout": {
        "type": "object",
        "required": ["taskId"],
        "additionalProperties": false,
        "properties": { "taskId": { "type": "string" } }
      },
      "PreparedOrder": {
        "type": "object",
        "required": ["task", "ownerToken", "warning"],
        "properties": {
          "task": { "$ref": "#/components/schemas/PreparedTask" },
          "ownerToken": { "type": "string", "description": "One-time resource control credential. Store securely; it cannot be recovered." },
          "warning": { "type": "string" }
        }
      },
      "PreparedTask": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "state", "planId", "hostname", "region", "osName", "sshFingerprint", "checkoutPath"],
        "properties": {
          "id": { "type": "string", "pattern": "^task_" },
          "state": { "type": "string", "const": "prepared" },
          "planId": { "$ref": "#/components/schemas/PlanId" },
          "hostname": { "type": "string" },
          "region": { "type": "string", "enum": ["us"] },
          "osName": { "type": "string" },
          "sshFingerprint": { "type": "string" },
          "checkoutPath": { "type": "string", "description": "Relative initial-checkout endpoint for this plan." }
        }
      },
      "Task": {
        "type": "object",
        "required": ["id", "planId", "hostname", "region", "osName", "sshFingerprint", "state"],
        "properties": {
          "id": { "type": "string" },
          "planId": { "$ref": "#/components/schemas/PlanId" },
          "hostname": { "type": "string" },
          "region": { "type": "string", "enum": ["us"] },
          "osName": { "type": "string" },
          "sshFingerprint": {
            "type": "string",
            "description": "Fingerprint of the submitted login public key, not the server host key."
          },
          "state": {
            "type": "string",
            "enum": [
              "prepared",
              "payment_pending",
              "paid",
              "provisioning",
              "ready",
              "expired",
              "cancellation_pending",
              "cancelled",
              "failed",
              "manual_review"
            ]
          },
          "checkoutAttempts": { "type": "integer", "minimum": 0 },
          "publicIp": { "type": ["string", "null"] },
          "paidAt": { "type": ["string", "null"], "format": "date-time" },
          "termStartsAt": { "type": ["string", "null"], "format": "date-time" },
          "termEndsAt": { "type": ["string", "null"], "format": "date-time" },
          "failure": { "type": ["object", "null"] },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "TaskPoll": {
        "type": "object",
        "required": ["task", "pollAfterSeconds"],
        "properties": {
          "task": { "$ref": "#/components/schemas/Task" },
          "pollAfterSeconds": { "type": "integer", "minimum": 0, "description": "Minimum suggested delay before the next status read." }
        }
      },
      "PowerRequest": {
        "type": "object",
        "required": ["action"],
        "additionalProperties": false,
        "properties": {
          "action": { "type": "string", "enum": ["boot", "reboot", "shutdown"] }
        }
      },
      "ReloadRequest": {
        "type": "object",
        "required": ["confirm"],
        "additionalProperties": false,
        "properties": {
          "confirm": { "type": "string", "const": "ERASE" },
          "hostname": { "type": "string", "maxLength": 63, "description": "Optional replacement; omitted means keep the current hostname." },
          "osName": { "type": "string", "description": "Optional replacement exact OS name from this task plan in GET /api/catalog; omitted means keep the current OS." },
          "sshPublicKey": { "type": "string", "description": "Optional replacement OpenSSH public key; omitted means keep the current login key." }
        }
      },
      "NetworkCreateRequest": {
        "type": "object",
        "required": ["taskId", "name"],
        "additionalProperties": false,
        "properties": {
          "taskId": { "type": "string" },
          "name": { "type": "string", "maxLength": 64 }
        }
      },
      "NetworkMutationRequest": {
        "type": "object",
        "required": ["networkId", "action"],
        "additionalProperties": false,
        "properties": {
          "networkId": { "type": "string" },
          "action": { "type": "string", "enum": ["attach", "detach"] }
        }
      },
      "Operation": {
        "type": "object",
        "required": ["id", "taskId", "kind", "state"],
        "properties": {
          "id": { "type": "string" },
          "taskId": { "type": "string" },
          "kind": { "type": "string" },
          "state": { "type": "string", "enum": ["queued", "running", "succeeded", "failed", "manual_review"] },
          "result": { "type": ["object", "null"] },
          "error": { "type": ["object", "null"] }
        }
      },
      "OperationAccepted": {
        "type": "object",
        "required": ["status", "operation", "pollPath"],
        "properties": {
          "status": { "type": "string", "const": "accepted" },
          "operation": { "$ref": "#/components/schemas/Operation" },
          "pollPath": { "type": "string" }
        }
      },
      "OperationPoll": {
        "type": "object",
        "required": ["operation", "pollAfterSeconds"],
        "properties": {
          "operation": { "$ref": "#/components/schemas/Operation" },
          "pollAfterSeconds": { "type": "integer", "minimum": 0 }
        }
      },
      "Network": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "name", "region", "vlanTag"],
        "properties": {
          "id": { "type": "string", "pattern": "^net_" },
          "name": { "type": "string" },
          "region": { "type": "string", "enum": ["us"] },
          "vlanTag": { "type": ["integer", "null"] }
        }
      },
      "NetworkList": {
        "type": "object",
        "required": ["networks"],
        "properties": { "networks": { "type": "array", "items": { "$ref": "#/components/schemas/Network" } } }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/": {
      "get": { "summary": "API discovery", "responses": { "200": { "description": "Service links." } } }
    },
    "/api/health": {
      "get": { "summary": "Dependency and purchasing readiness", "responses": { "200": { "description": "Health projection." }, "503": { "description": "Database unavailable." } } }
    },
    "/api/catalog": {
      "get": {
        "summary": "Plans with cached live free-OS inventory and public regions",
        "description": "No authentication. For an order or reload, choose a product and submit the exact name from that same product's operatingSystems array. Do not hard-code OS versions. Every published image is currently free and SSH-key compatible.",
        "responses": {
          "200": {
            "description": "Purchasable catalog and exact OS names.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Catalog" } } }
          },
          "503": { "description": "Fresh inventory unavailable; pause ordering and reloads instead of guessing an OS." }
        }
      }
    },
    "/api/orders": {
      "post": {
        "summary": "Prepare an unpaid purchase task",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PrepareOrder" } } } },
        "responses": {
          "201": {
            "description": "Task plus one-time ownerToken. Exact idempotent replay is available for 24 hours.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PreparedOrder" } } }
          },
          "400": { "description": "Invalid input." },
          "409": { "description": "Idempotency conflict." },
          "429": { "description": "Order limit reached." },
          "503": { "description": "Inventory or payment dependencies unavailable; purchasing is paused." }
        }
      }
    },
    "/api/checkout/{plan}": {
      "post": {
        "summary": "Create or retry the exact x402 payment for a prepared task",
        "security": [{ "ownerToken": [] }],
        "parameters": [
          { "name": "plan", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/PlanId" } },
          { "$ref": "#/components/parameters/PaymentSignature" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Checkout" } } } },
        "responses": {
          "202": { "description": "Payment/finality pending or provisioning queued." },
          "402": { "description": "Payment required. If a supplied authorization was rejected, read the new PAYMENT-REQUIRED challenge and create one replacement authorization." },
          "409": { "description": "Settled fulfillment requires manual review; never pay again." },
          "503": { "description": "Outcome ambiguous; retry the identical signed request." }
        }
      }
    },
    "/api/checkout/{plan}/renew": {
      "post": {
        "summary": "Create or retry an explicit 30-day renewal payment",
        "security": [{ "ownerToken": [] }],
        "parameters": [
          { "name": "plan", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/PlanId" } },
          { "$ref": "#/components/parameters/PaymentSignature" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Checkout" } } } },
        "responses": { "200": { "description": "Term extended." }, "202": { "description": "Pending; retry the exact authorization." }, "402": { "description": "Payment required or prior authorization definitively rejected." }, "409": { "description": "Manual review." } }
      }
    },
    "/api/tasks/{id}": {
      "get": {
        "summary": "Read a purchase/provisioning task",
        "description": "Follow pollAfterSeconds until ready or a terminal state. When ready, publicIp is the SSH address for the private key matching sshFingerprint. SSH usernames are image-specific and are not guessed by this API.",
        "security": [{ "ownerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/TaskId" }],
        "responses": {
          "200": { "description": "Task and poll delay.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskPoll" } } } },
          "404": { "description": "ID/token pair not found; do not retry this pair in a loop." },
          "429": { "description": "Polling limit reached; follow Retry-After." }
        }
      }
    },
    "/api/operations/{id}": {
      "get": {
        "summary": "Poll a durable lifecycle operation",
        "description": "Poll an asynchronous power, reload, or network mutation. queued and running are non-terminal; succeeded, failed, and manual_review are terminal. Never infer success from the original HTTP 202.",
        "security": [{ "ownerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/OperationId" }],
        "responses": {
          "200": { "description": "Operation state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OperationPoll" } } } },
          "404": { "description": "Operation and owner token pair not found." },
          "429": { "description": "Polling limit reached; follow Retry-After." }
        }
      }
    },
    "/api/tasks/{id}/power": {
      "post": {
        "summary": "Queue boot, reboot, or shutdown",
        "description": "The instance must be ready. Returns an asynchronous operation; poll its Location with the same owner token.",
        "security": [{ "ownerToken": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/TaskId" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PowerRequest" } } } },
        "responses": {
          "202": { "description": "Operation queued; this is not proof that the power action succeeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OperationAccepted" } } } },
          "400": { "description": "Invalid action or Idempotency-Key." },
          "404": { "description": "No ready instance matched the task/token pair." },
          "409": { "description": "Idempotency-Key was reused with different content." },
          "429": { "description": "Lifecycle mutation limit reached; follow Retry-After." }
        }
      }
    },
    "/api/tasks/{id}/reload": {
      "post": {
        "summary": "Queue a destructive free-OS reload",
        "description": "ERASES the server disk; this is not a reboot. The task must be ready and confirm must be ERASE. hostname, osName, and sshPublicKey are optional replacements; omitted values remain unchanged. Any osName must exactly match an operatingSystems[].name for the task plan in the live catalog. Poll the returned operation. If it reaches manual_review, do not retry because the compute outcome is ambiguous.",
        "security": [{ "ownerToken": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/TaskId" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReloadRequest" } } } },
        "responses": {
          "202": { "description": "Destructive reload queued; poll the operation before reading the updated task.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OperationAccepted" } } } },
          "400": { "description": "Missing ERASE confirmation, invalid hostname/key/OS, unavailable live catalog, or invalid Idempotency-Key." },
          "404": { "description": "No ready instance matched the task/token pair." },
          "409": { "description": "Idempotency-Key was reused with different content." },
          "429": { "description": "Lifecycle mutation limit reached; follow Retry-After." }
        }
      }
    },
    "/api/networks": {
      "get": {
        "summary": "List owner-scoped networks",
        "security": [{ "ownerToken": [] }],
        "parameters": [{ "name": "taskId", "in": "query", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Owner-scoped networks.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NetworkList" } } } },
          "400": { "description": "taskId is required." },
          "404": { "description": "Task and owner token pair not found." },
          "429": { "description": "Polling limit reached; follow Retry-After." }
        }
      },
      "post": {
        "summary": "Queue private-network creation",
        "security": [{ "ownerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NetworkCreateRequest" } } } },
        "responses": {
          "202": { "description": "Network creation operation queued.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OperationAccepted" } } } },
          "400": { "description": "Invalid name, taskId, or Idempotency-Key." },
          "404": { "description": "Task and owner token pair not found." },
          "409": { "description": "Instance not ready, network capacity reached, or idempotency conflict." },
          "429": { "description": "Lifecycle mutation limit reached; follow Retry-After." }
        }
      }
    },
    "/api/tasks/{id}/network": {
      "post": {
        "summary": "Queue network attachment or detachment",
        "description": "The task must be ready. action is attach or detach. Poll the returned operation; HTTP 202 is not proof that the network changed.",
        "security": [{ "ownerToken": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/TaskId" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NetworkMutationRequest" } } } },
        "responses": {
          "202": { "description": "Network operation queued.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OperationAccepted" } } } },
          "400": { "description": "Invalid networkId, action, or Idempotency-Key." },
          "404": { "description": "No ready instance matched the task/token pair." },
          "409": { "description": "Idempotency-Key was reused with different content." },
          "429": { "description": "Lifecycle mutation limit reached; follow Retry-After." }
        }
      }
    }
  }
}
