openapi: 3.0.3 info: title: Sankofa HW Infra API version: 0.1.0 servers: - url: /api/v1 security: - BearerAuth: [] components: schemas: ApiError: type: object properties: error: { type: string, description: Human-readable message } code: { type: string, enum: [BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL_ERROR] } details: { type: object, description: Optional validation or extra data } securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: JWT with optional vendorId for vendor users IngestionApiKey: type: apiKey in: header name: x-ingestion-api-key description: Required for POST /ingestion/offers (env INGESTION_API_KEY) paths: /health: get: summary: Health security: [] /auth/token: post: summary: Get JWT token description: Exchange email (and optional password) for a JWT with roles and vendorId. No auth required. security: [] requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string, format: email } password: { type: string } responses: "200": description: Token and user info "401": description: Invalid credentials /vendors: get: summary: List vendors description: If JWT contains vendorId (vendor user), returns only that vendor. post: summary: Create vendor description: Forbidden for vendor users. /vendors/{id}: get: summary: Get vendor description: Vendor users may only request their own vendor id. /offers: get: summary: List offers description: If JWT contains vendorId, returns only that vendor's offers. post: summary: Create offer description: Vendor users' vendorId is forced to their vendor. /offers/{id}: get: summary: Get offer patch: summary: Update offer delete: summary: Delete offer /purchase-orders: get: summary: List purchase orders description: If JWT contains vendorId, returns only POs for that vendor. /purchase-orders/{id}: get: summary: Get purchase order /ingestion/offers: post: summary: Ingest offer (scrape or email) description: Creates an offer with source (scraped|email), source_ref, source_metadata. Secured by x-ingestion-api-key only; no JWT. Use x-org-id for target org. security: - IngestionApiKey: [] requestBody: required: true content: application/json: schema: type: object required: [source, quantity, unit_price] properties: source: type: string enum: [scraped, email] source_ref: type: string description: URL or email message id source_metadata: type: object vendor_id: type: string format: uuid nullable: true sku: type: string mpn: type: string quantity: type: integer unit_price: type: string incoterms: type: string lead_time_days: type: integer country_of_origin: type: string condition: type: string warranty: type: string evidence_refs: type: array items: type: object properties: key: { type: string } hash: { type: string } responses: "201": description: Offer created "401": description: Invalid or missing x-ingestion-api-key /capacity/sites/{siteId}: get: summary: RU utilization for a site description: Returns usedRu, totalRu, utilizationPercent for the site (from racks and assigned positions). parameters: - name: siteId in: path required: true schema: { type: string, format: uuid } responses: "200": description: Site capacity (usedRu, totalRu, utilizationPercent) "404": description: Site not found /capacity/sites/{siteId}/power: get: summary: Power headroom for a site description: Returns circuitLimitWatts from rack power_feeds; measuredDrawWatts/headroomWatts null until Phase 4. parameters: - name: siteId in: path required: true schema: { type: string, format: uuid } responses: "200": description: Power info (circuitLimitWatts, measuredDrawWatts, headroomWatts) "404": description: Site not found /capacity/gpu-inventory: get: summary: GPU inventory description: Returns total, bySite, and byType (part number) counts. responses: "200": description: GPU counts (total, bySite, byType)