{
  "openapi": "3.1.0",
  "info": {
    "title": "novagentum API",
    "version": "1.0.0",
    "description": "Scrape and crawl websites with your novagentum account. Requests use the same balance, workspace spending rules, limits and prices as the dashboard. Authenticate with a session obtained through the CLI device flow (`novagentum login`)."
  },
  "servers": [{ "url": "https://www.novagentum.ai" }],
  "security": [{ "bearer": [] }],
  "tags": [
    {
      "name": "Device sign-in",
      "description": "OAuth 2.0 device authorization flow (RFC 8628 style) used by the CLI."
    },
    { "name": "Scrape" },
    { "name": "Crawl" },
    { "name": "Account" }
  ],
  "paths": {
    "/api/cli/device": {
      "post": {
        "tags": ["Device sign-in"],
        "summary": "Start a device sign-in",
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_name": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Shown to the person approving."
                  },
                  "client_version": { "type": "string", "maxLength": 32 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Show `user_code` and `verification_uri` to the user, then poll /api/cli/token every `interval` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "device_code",
                    "user_code",
                    "verification_uri",
                    "verification_uri_complete",
                    "expires_in",
                    "interval"
                  ],
                  "properties": {
                    "device_code": {
                      "type": "string",
                      "description": "Secret; send only to /api/cli/token."
                    },
                    "user_code": {
                      "type": "string",
                      "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$"
                    },
                    "verification_uri": { "type": "string", "format": "uri" },
                    "verification_uri_complete": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Seconds (600)."
                    },
                    "interval": {
                      "type": "integer",
                      "description": "Minimum seconds between polls (5)."
                    }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/OAuthError" },
          "503": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/api/cli/token": {
      "post": {
        "tags": ["Device sign-in"],
        "summary": "Poll for the session",
        "description": "Returns HTTP 400 with `authorization_pending` until the user approves, `slow_down` when polled faster than the interval (add 5 seconds), `access_denied`, `expired_token`, or `invalid_grant` for an unknown or already used code. On approval the code is consumed once and a new, independent session is returned.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["device_code"],
                "properties": {
                  "device_code": { "type": "string" },
                  "grant_type": {
                    "const": "urn:ietf:params:oauth:grant-type:device_code"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session. Refresh it at `{supabase_url}/auth/v1/token?grant_type=refresh_token` with header `apikey: {supabase_anon_key}`; refresh tokens rotate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": { "type": "string" },
                    "refresh_token": { "type": "string" },
                    "expires_at": {
                      "type": "integer",
                      "description": "Unix seconds."
                    },
                    "expires_in": { "type": "integer" },
                    "token_type": { "const": "bearer" },
                    "supabase_url": { "type": "string", "format": "uri" },
                    "supabase_anon_key": {
                      "type": "string",
                      "description": "Public key."
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "email": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/OAuthError" },
          "429": { "$ref": "#/components/responses/OAuthError" },
          "503": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/api/cli/whoami": {
      "get": {
        "tags": ["Account"],
        "summary": "Signed-in user, workspaces and personal balance",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "email": { "type": ["string", "null"] },
                        "emailConfirmed": { "type": "boolean" }
                      }
                    },
                    "workspaces": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "format": "uuid" },
                          "slug": { "type": "string" },
                          "name": { "type": "string" },
                          "status": { "type": "string" },
                          "role": { "enum": ["owner", "admin", "member"] },
                          "canSpend": { "type": "boolean" }
                        }
                      }
                    },
                    "billing": { "$ref": "#/components/schemas/Billing" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "tags": ["Account"],
        "summary": "Balance for the account or a workspace",
        "parameters": [{ "$ref": "#/components/parameters/WorkspaceQuery" }],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Billing" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/v1/scrape": {
      "post": {
        "tags": ["Scrape"],
        "summary": "Scrape one page",
        "description": "Returns markdown, HTML, links and metadata. Charged as a scrape at the current price; failed fetches are not charged. Retry with the same `requestId` to fetch the result of an interrupted request without paying twice.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048
                  },
                  "workspaceId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Spend the workspace balance (requires spend permission)."
                  },
                  "requestId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Idempotency key; generated when omitted."
                  },
                  "useFreeAudit": { "type": "boolean" },
                  "formats": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Accepted for compatibility; all formats are returned."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`success` is false when the page could not be scraped (you were not charged).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "requestId": { "type": "string", "format": "uuid" },
                    "error": { "type": "string" },
                    "code": { "type": "string" },
                    "data": { "$ref": "#/components/schemas/ScrapeResult" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/v1/scrape/{id}": {
      "get": {
        "tags": ["Scrape"],
        "summary": "Reopen a saved scrape (30 days)",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          { "$ref": "#/components/parameters/WorkspaceQuery" }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "data": { "$ref": "#/components/schemas/ScrapeResult" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/v1/crawl": {
      "get": {
        "tags": ["Crawl"],
        "summary": "List crawls and scrapes",
        "parameters": [{ "$ref": "#/components/parameters/WorkspaceQuery" }],
        "responses": {
          "200": {
            "description": "Newest first; `result` is omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CrawlJob" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["Crawl"],
        "summary": "Crawl and audit a website",
        "description": "Reserves the quoted maximum (see GET /api/crawl/quote?maxPages=) and charges only for audited pages. Pass the quote you showed the user as `quotedMaximumCents`/`quotedPriceVersion`; a changed price is rejected with 409 QUOTE_CHANGED. Today the crawl finishes within the request; clients must also handle `queued`/`running` by polling GET /api/v1/crawl/{id}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "additionalProperties": false,
                "properties": {
                  "url": { "type": "string", "format": "uri" },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 10,
                    "description": "Maximum pages (pay-as-you-go: 25)."
                  },
                  "workspaceId": { "type": "string", "format": "uuid" },
                  "requestId": { "type": "string", "format": "uuid" },
                  "pageUrls": {
                    "type": "array",
                    "items": { "type": "string", "format": "uri" }
                  },
                  "useFreeAudit": { "type": "boolean" },
                  "quotedMaximumCents": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Cents, or credits in credit mode."
                  },
                  "quotedPriceVersion": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "id": { "type": "string", "format": "uuid" },
                    "status": { "$ref": "#/components/schemas/CrawlStatus" },
                    "requestId": { "type": "string", "format": "uuid" },
                    "data": { "$ref": "#/components/schemas/CrawlJob" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/v1/crawl/{id}": {
      "get": {
        "tags": ["Crawl"],
        "summary": "Crawl status and result",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          { "$ref": "#/components/parameters/WorkspaceQuery" }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "id": { "type": "string", "format": "uuid" },
                    "status": { "$ref": "#/components/schemas/CrawlStatus" },
                    "data": { "$ref": "#/components/schemas/CrawlJob" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Error" }
        }
      },
      "delete": {
        "tags": ["Crawl"],
        "summary": "Delete a finished crawl or scrape",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          { "$ref": "#/components/parameters/WorkspaceQuery" }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "type": "object",
                      "properties": { "deleted": { "const": true } }
                    }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/crawl/quote": {
      "get": {
        "tags": ["Crawl"],
        "summary": "Public crawl quote",
        "security": [],
        "parameters": [
          {
            "name": "maxPages",
            "in": "query",
            "required": true,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100 }
          }
        ],
        "responses": {
          "200": {
            "description": "`maximumCents` is in credits when credit billing is on.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "maximumCents": { "type": "integer" },
                    "priceVersion": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Session access token from the device flow (or `NOVAGENTUM_TOKEN` for the CLI)."
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" }
      },
      "WorkspaceQuery": {
        "name": "workspaceId",
        "in": "query",
        "required": false,
        "schema": { "type": "string", "format": "uuid" }
      }
    },
    "responses": {
      "Error": {
        "description": "Error",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["success", "error", "code"],
              "properties": {
                "success": { "const": false },
                "error": { "type": "string" },
                "code": {
                  "type": "string",
                  "examples": [
                    "AUTH_REQUIRED",
                    "AUTH_INVALID",
                    "INSUFFICIENT_BALANCE",
                    "WORKSPACE_SPEND_NOT_ALLOWED",
                    "QUOTE_CHANGED",
                    "PAGE_LIMIT",
                    "DUPLICATE_REQUEST",
                    "SCRAPE_PENDING",
                    "RATE_LIMITED"
                  ]
                },
                "requiredCents": { "type": "integer" },
                "balanceCents": { "type": "integer" },
                "runId": { "type": "string", "format": "uuid" }
              }
            }
          }
        }
      },
      "OAuthError": {
        "description": "OAuth 2.0 error",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "enum": [
                    "authorization_pending",
                    "slow_down",
                    "access_denied",
                    "expired_token",
                    "invalid_grant",
                    "invalid_request",
                    "unsupported_grant_type",
                    "temporarily_unavailable"
                  ]
                },
                "error_description": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "CrawlStatus": { "enum": ["queued", "running", "completed", "failed"] },
      "CrawlJob": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/CrawlStatus" },
          "url": { "type": "string" },
          "mode": { "enum": ["crawl_only", "scrape"] },
          "limit": { "type": "integer" },
          "pagesAttempted": { "type": "integer" },
          "pagesAudited": { "type": "integer" },
          "grade": { "type": ["string", "null"] },
          "error": { "type": ["string", "null"] },
          "createdAt": { "type": "string", "format": "date-time" },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "usage": {
            "type": ["object", "null"],
            "description": "Includes chargedCents, maximumCents and priceVersion."
          },
          "result": {
            "type": ["object", "null"],
            "description": "Full audit: coverage, pages and findings."
          }
        }
      },
      "ScrapeResult": {
        "type": "object",
        "properties": {
          "scrape": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "status": { "enum": ["completed", "failed"] },
              "url": { "type": "string" },
              "createdAt": { "type": "string", "format": "date-time" },
              "saved": { "type": "boolean" },
              "reopenUntil": {
                "type": ["string", "null"],
                "format": "date-time"
              }
            }
          },
          "data": {
            "type": ["object", "null"],
            "properties": {
              "markdown": { "type": "string" },
              "html": { "type": "string" },
              "links": { "type": "array", "items": { "type": "object" } },
              "metadata": { "type": "object" }
            }
          },
          "truncated": { "type": "object" },
          "warnings": { "type": "array", "items": { "type": "string" } },
          "error": { "type": ["object", "null"] },
          "audit": { "type": ["object", "null"] },
          "usage": {
            "type": "object",
            "properties": {
              "priceVersion": { "type": "string" },
              "chargedCents": { "type": "integer" },
              "unit": { "enum": ["usd_cents", "credits"] },
              "free": { "type": "boolean" }
            }
          }
        }
      },
      "Billing": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "mode": { "const": "credits" },
              "unit": { "const": "credits" },
              "available": { "type": "integer" },
              "monthlyCredits": { "type": "integer" },
              "purchasedCredits": { "type": "integer" },
              "monthlyDebt": { "type": "integer" },
              "renewsAt": { "type": ["string", "null"] }
            }
          },
          {
            "type": "object",
            "properties": {
              "mode": { "const": "payg" },
              "unit": { "const": "usd_cents" },
              "balanceCents": { "type": "integer" }
            }
          },
          { "type": "object", "properties": { "mode": { "const": "none" } } }
        ]
      }
    }
  }
}
