{"openapi":"3.0.3","info":{"title":"WSQ API Lab","version":"1.0.0","description":"Reference REST API for exercising WSQ API testing: JWT auth, full CRUD, typed/required query + header params, JSON bodies with validation, pagination, filtering, sorting, ETags, rate limiting, and structured error envelopes."},"servers":[{"url":"https://api.wsqdemo.com/api/v1","description":"WSQ API Lab v1"}],"tags":[{"name":"auth","description":"Authentication & tokens"},{"name":"products","description":"Product catalog CRUD"},{"name":"system","description":"Health & metadata"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"requestId":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"issue":{"type":"string"}}}}}}}},"Product":{"type":"object","required":["id","name","price","category","sku","inStock"],"properties":{"id":{"type":"integer","example":1},"name":{"type":"string","example":"Relay Mechanical Keyboard"},"price":{"type":"number","example":129},"category":{"type":"string","example":"keyboards"},"sku":{"type":"string","example":"RLY-KBD-001"},"inStock":{"type":"boolean","example":true},"updatedAt":{"type":"string","example":"2026-01-01T00:00:00.000Z"}}},"ProductInput":{"type":"object","required":["name","price","category","sku"],"properties":{"name":{"type":"string","minLength":1,"example":"Relay Wrist Rest"},"price":{"type":"number","minimum":0,"example":24.99},"category":{"type":"string","example":"accessories"},"sku":{"type":"string","pattern":"^RLY-[A-Z]+-[0-9]{3}$","example":"RLY-WRR-009"},"inStock":{"type":"boolean","example":true}}},"Pagination":{"type":"object","required":["page","pageSize","total","totalPages"],"properties":{"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"ProductList":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"LoginInput":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string","example":"admin"},"password":{"type":"string","example":"admin123"}}},"TokenResponse":{"type":"object","required":["accessToken","tokenType","expiresIn"],"properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"tokenType":{"type":"string","example":"Bearer"},"expiresIn":{"type":"integer","example":3600},"user":{"type":"object","properties":{"username":{"type":"string"},"role":{"type":"string"}}}}}}},"paths":{"/auth/login":{"post":{"tags":["auth"],"operationId":"login","summary":"Exchange credentials for tokens","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/refresh":{"post":{"tags":["auth"],"operationId":"refresh","summary":"Exchange a refresh token for a new access token","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refreshToken"],"properties":{"refreshToken":{"type":"string","example":"wsq-demo-refresh-token"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid refresh token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/token":{"post":{"tags":["auth"],"operationId":"oauthToken","summary":"OAuth2 client-credentials grant (form or JSON) -> access_token","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","example":"client_credentials"},"client_id":{"type":"string","example":"wsq-admin"},"client_secret":{"type":"string","example":"admin-secret"}}}},"application/json":{"schema":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","example":"client_credentials"},"client_id":{"type":"string","example":"wsq-admin"},"client_secret":{"type":"string","example":"admin-secret"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"integer"},"scope":{"type":"string"}}}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/me":{"get":{"tags":["auth"],"operationId":"me","summary":"Return the authenticated principal (Test Auth target)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"username":{"type":"string"},"role":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products":{"get":{"tags":["products"],"operationId":"listProducts","summary":"List products (paginated, filterable, sortable)","parameters":[{"name":"page","in":"query","required":true,"schema":{"type":"integer","minimum":1,"example":1}},{"name":"pageSize","in":"query","required":true,"schema":{"type":"integer","minimum":1,"maximum":100,"example":10}},{"name":"X-Tenant-Id","in":"header","required":true,"schema":{"type":"integer","example":42}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}},{"name":"minPrice","in":"query","required":false,"schema":{"type":"number"}},{"name":"inStock","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","example":"-price"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string","example":"id,name,price"},"description":"Sparse fieldset"},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"],"example":"json"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductList"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["products"],"operationId":"createProduct","summary":"Create a product (admin)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductInput"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Duplicate sku","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products/{id}":{"get":{"tags":["products"],"operationId":"getProduct","summary":"Fetch one product (supports ETag / If-None-Match)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"}}}},"304":{"description":"Not modified"},"400":{"description":"Bad id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["products"],"operationId":"replaceProduct","summary":"Replace a product (admin; supports If-Match optimistic concurrency)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductInput"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"Precondition failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["products"],"operationId":"updateProduct","summary":"Partially update a product (admin)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","example":"Relay Mechanical Keyboard (rev B)"},"price":{"type":"number","minimum":0,"example":139},"category":{"type":"string","example":"keyboards"},"inStock":{"type":"boolean","example":true}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["products"],"operationId":"deleteProduct","summary":"Delete a product (admin)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products/bulk":{"post":{"tags":["products"],"operationId":"bulkCreateProducts","summary":"Bulk create products (admin; JSON array body)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductInput"},"example":[{"name":"Bulk A","price":5,"category":"accessories","sku":"RLY-BLA-701"},{"name":"Bulk B","price":6,"category":"accessories","sku":"RLY-BLB-702"}]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"count":{"type":"integer"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products/{id}/image":{"post":{"tags":["products"],"operationId":"uploadProductImage","summary":"Upload a product image (admin; multipart/form-data, field \"file\")","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","example":"BASE64_OR_DATA_URL"},"filename":{"type":"string","example":"hero.png"},"caption":{"type":"string","example":"hero shot"}}}},"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"caption":{"type":"string"}},"required":["file"]}}}},"responses":{"201":{"description":"Uploaded","content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"integer"},"filename":{"type":"string"},"size":{"type":"integer"},"contentType":{"type":"string"}}}}}},"400":{"description":"Missing/invalid file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/export":{"post":{"tags":["system"],"operationId":"startExport","summary":"Start an async export job (admin) -> 202 Accepted + poll URL","security":[{"bearerAuth":[]}],"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"poll":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/{id}":{"get":{"tags":["system"],"operationId":"getJob","summary":"Poll an async job status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","example":"job_1"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}