{"openapi":"3.1.0","info":{"title":"Beru Portfolio Webhook API","version":"1.0.0","description":"Webhook endpoints for automated blog draft creation and image upload from external AI microservice."},"servers":[{"url":"https://beru.io.vn"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Webhook secret token. Pass as: Authorization: Bearer <WEBHOOK_SECRET>"},"SeoAgentAuth":{"type":"http","scheme":"bearer","description":"Read-only SEO agent token. Pass as: Authorization: Bearer <SEO_AGENT_API_KEY>"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"UploadResponse":{"type":"object","properties":{"url":{"type":"string","example":"/uploads/blog/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp"}},"required":["url"]},"BlogRequest":{"type":"object","properties":{"title":{"type":"string","maxLength":200,"description":"Post title (required)","example":"Getting Started with Next.js 15"},"content":{"type":"string","description":"Markdown content (required)","example":"# Introduction\\n\\nNext.js 15 brings exciting new features..."},"excerpt":{"type":"string","maxLength":500,"description":"Short summary for listing cards","example":"A quick guide to the latest Next.js features."},"coverImage":{"type":"string","description":"Cover image URL. Relative path (/uploads/...) or absolute https URL.","example":"/uploads/blog/cover.webp"},"tags":{"type":"array","items":{"type":"string","maxLength":30},"maxItems":10,"description":"Tags for categorization","example":["nextjs","react","tutorial"]},"slug":{"type":"string","maxLength":100,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","description":"URL slug. Auto-generated from title if omitted.","example":"getting-started-with-nextjs-15"}},"required":["title","content"]},"BlogResponse":{"type":"object","properties":{"id":{"type":"string","example":"cm1abc2def3ghi4jk"},"slug":{"type":"string","example":"getting-started-with-nextjs-15"},"adminUrl":{"type":"string","example":"/admin/blog/cm1abc2def3ghi4jk/edit"}},"required":["id","slug","adminUrl"]}}},"paths":{"/api/agent/seo":{"get":{"summary":"Get production SEO overview","description":"Returns Search Console, GA4 landing-page metrics, and SEO opportunities for beru.io.vn.","operationId":"getSeoOverview","tags":["SEO Agent"],"security":[{"SeoAgentAuth":[]}],"parameters":[{"name":"range","in":"query","schema":{"type":"string","enum":["7d","28d","90d"],"default":"28d"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/agent/seo/audit":{"post":{"summary":"Audit a live beru.io.vn URL","description":"Runs a read-only technical SEO audit. URLs outside beru.io.vn are rejected.","operationId":"auditSeoUrl","tags":["SEO Agent"],"security":[{"SeoAgentAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/upload":{"post":{"summary":"Upload image","description":"Upload an image file. Automatically resized (max 2000px width) and converted to WebP.","operationId":"uploadImage","tags":["Upload"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"binary","description":"Image file (JPG, PNG, WebP, GIF). Max 5MB."},"category":{"type":"string","enum":["blog","portfolio","services","testimonials","general"],"default":"blog","description":"Upload category (determines subdirectory)."}},"required":["image"]}}}},"responses":{"201":{"description":"Image uploaded successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"400":{"description":"Bad request (no file, invalid type, invalid category, magic byte mismatch)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid Bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"File too large (max 5MB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (20 req/min)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/blog":{"get":{"summary":"List blog","tags":["Blog"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create or upsert blog (by slug)","operationId":"upsertBlog","tags":["Blog"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/blog/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Get one blog","tags":["Blog"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Update blog (partial)","tags":["Blog"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update blog (partial)","tags":["Blog"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete blog","tags":["Blog"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/projects":{"get":{"summary":"List projects","tags":["Projects"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create or upsert projects (by slug)","operationId":"upsertProjects","tags":["Projects"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/projects/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Get one projects","tags":["Projects"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Update projects (partial)","tags":["Projects"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update projects (partial)","tags":["Projects"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete projects","tags":["Projects"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/services":{"get":{"summary":"List services","tags":["Services"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create or upsert services (by slug)","operationId":"upsertServices","tags":["Services"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/services/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Get one services","tags":["Services"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Update services (partial)","tags":["Services"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update services (partial)","tags":["Services"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete services","tags":["Services"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/testimonials":{"get":{"summary":"List testimonials","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create testimonials","operationId":"createTestimonials","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/testimonials/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Get one testimonials","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Update testimonials (partial)","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update testimonials (partial)","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete testimonials","tags":["Testimonials"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhook/settings":{"get":{"summary":"Get site settings","tags":["Settings"],"security":[{"BearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Merge site settings","tags":["Settings"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Merge site settings","tags":["Settings"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}