{"openapi":"3.1.0","info":{"title":"Playground CMS Agent API","version":"1.0.0","description":"REST API for AI agents to manage content, pages, SEO, and site settings in a multi-tenant CMS. Authenticate with an API key (X-Agent-Key header) or OAuth 2.0 Bearer token. Some operations require elevated trust tiers: tier 0 (default) allows basic content management; tier 1+ unlocks custom CSS, webhooks, and custom domains; tier 2+ unlocks custom JavaScript and HTML injection."},"servers":[{"url":"/api/agent","description":"Agent API"}],"components":{"securitySchemes":{"agentKey":{"type":"apiKey","in":"header","name":"X-Agent-Key","description":"Agent API key (agk_ prefix)"},"bearerAuth":{"type":"http","scheme":"bearer","description":"OAuth 2.0 access token (oat_ prefix)"}},"schemas":{"SuccessResponse":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"data":{},"meta":{"type":"object"}},"required":["ok","data"]},"ErrorResponse":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","properties":{"code":{"type":"string","enum":["MISSING_KEY","INVALID_KEY","KEY_EXPIRED","INSUFFICIENT_SCOPES","TRUST_TIER_REQUIRED","RATE_LIMITED","REQUEST_BUDGET_EXCEEDED","TOKEN_BUDGET_EXCEEDED","INSUFFICIENT_CREDITS","VALIDATION_ERROR","NOT_FOUND","DUPLICATE","CONFLICT","INTERNAL_ERROR"]},"message":{"type":"string"},"field":{"type":"string"}},"required":["code","message"]}},"required":["ok","error"]},"Article":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"content":{"type":"string"},"excerpt":{"type":"string"},"author":{"type":"string"},"category":{"type":"string"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","ARCHIVED","SCHEDULED"]},"scheduledAt":{"type":"string","format":"date-time"},"agentKeyId":{"type":"string"},"featured":{"type":"boolean"},"keywords":{"type":"array","items":{"type":"string"}},"heroImageUrl":{"type":"string"},"heroImageAlt":{"type":"string"},"storyStage":{"type":"string"},"views":{"type":"integer"},"readTime":{"type":"integer"},"publishedAt":{"type":"string","format":"date-time"}}},"Page":{"type":"object","properties":{"id":{"type":"string"},"path":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","DISABLED"]},"editorMode":{"type":"string","enum":["CODE","VISUAL","MARKDOWN"]}}},"Creative":{"type":"object","properties":{"id":{"type":"string"},"path":{"type":"string"},"title":{"type":"string"},"content":{"type":"string","description":"Complete HTML document"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","DISABLED"]},"metadata":{"type":"object","description":"Generation metadata: brief, style, tokensUsed, refinements, etc."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DesignSystem":{"type":"object","properties":{"version":{"type":"integer"},"generatedAt":{"type":"string","format":"date-time"},"brief":{"type":"string"},"tokens":{"type":"object","description":"Design tokens: colors, typography, spacing, borders, shadows, etc."},"css":{"type":"string","description":"Generated CSS custom properties from tokens"}}},"DesignComponent":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["LAYOUT","NAVIGATION","FORM","BUTTON","CARD","FEEDBACK","DATA_DISPLAY","HERO","SECTION","FOOTER","CUSTOM"]},"html":{"type":"string"},"css":{"type":"string"},"variants":{"type":"array","items":{"type":"object"},"description":"Component variants with different styles or configurations"},"props":{"type":"array","items":{"type":"object"},"description":"Configurable props/parameters for the component"},"usage":{"type":"string","description":"Usage instructions and examples"},"aiGenerated":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PaginationMeta":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"},"hasMore":{"type":"boolean"}}}}},"security":[{"agentKey":[]},{"bearerAuth":[]}],"paths":{"/articles":{"get":{"operationId":"listArticles","summary":"List articles","description":"List articles with optional filtering by status, category, and search query. Requires articles:read scope.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","PUBLISHED","ARCHIVED","SCHEDULED"]}},{"name":"category","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search articles by title, content, or excerpt"}],"responses":{"200":{"description":"List of articles with pagination metadata"}}},"post":{"operationId":"createArticle","summary":"Create article","description":"Create a new article. Requires articles:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","slug"],"properties":{"title":{"type":"string"},"slug":{"type":"string"},"content":{"type":"string"},"excerpt":{"type":"string"},"author":{"type":"string"},"category":{"type":"string","default":"NEWS"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","SCHEDULED"],"default":"DRAFT"},"keywords":{"type":"array","items":{"type":"string"}},"heroImageUrl":{"type":"string"},"scheduledAt":{"type":"string","format":"date-time","description":"ISO 8601 date for scheduled publish. Auto-sets status to SCHEDULED."}}}}}},"responses":{"201":{"description":"Article created"},"400":{"description":"Validation error"}}}},"/articles/{id}":{"get":{"operationId":"getArticle","summary":"Get article by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full article object"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateArticle","summary":"Update article","description":"Update article fields. Only provided fields are changed. Pass ifUpdatedAt (ISO 8601) for conflict prevention — returns 409 CONFLICT if record changed. Requires articles:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string"},"content":{"type":"string"},"excerpt":{"type":"string","description":"Renders as <meta name=\"description\"> + og:description + twitter:description on the article page. Without it, Google scrapes body text and CTR suffers."},"category":{"type":"string"},"status":{"type":"string"},"featured":{"type":"boolean"},"keywords":{"type":"array","items":{"type":"string"}},"heroImageUrl":{"type":"string"},"heroImageAlt":{"type":"string"},"faqs":{"type":"array","description":"Emits FAQPage JSON-LD alongside the existing BlogPosting node when non-empty. Triggers FAQ rich-results that expand SERP footprint.","items":{"type":"object","required":["question","answer"],"properties":{"question":{"type":"string"},"answer":{"type":"string"}}}},"scheduledAt":{"type":"string","format":"date-time","description":"ISO 8601 date for scheduled publish"},"ifUpdatedAt":{"type":"string","format":"date-time","description":"Conflict check: reject if record updatedAt differs (409 CONFLICT)"}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"},"409":{"description":"Conflict — record modified since read"}}},"delete":{"operationId":"deleteArticle","summary":"Delete article","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/articles/{id}/story":{"post":{"operationId":"runStoryPipeline","summary":"Run AI story pipeline","description":"Advance article through AI story stages: create → outline → draft → images → publish. Requires journalist:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Article ID (use \"new\" for create)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["create","outline","draft","images","publish"]},"title":{"type":"string","description":"Required for create action"},"concept":{"type":"string"},"category":{"type":"string"},"count":{"type":"integer","minimum":1,"maximum":4,"description":"Image count for images action"}}}}}},"responses":{"200":{"description":"Pipeline stage result"},"201":{"description":"Article created (create action)"}}}},"/pages":{"get":{"operationId":"listPages","summary":"List pages","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","PUBLISHED","DISABLED"]}}],"responses":{"200":{"description":"List of pages"}}},"post":{"operationId":"createPage","summary":"Create page","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["path","title"],"properties":{"path":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"kind":{"type":"string","default":"PAGE"},"status":{"type":"string","default":"DRAFT"},"editorMode":{"type":"string","default":"CODE"}}}}}},"responses":{"201":{"description":"Page created"}}}},"/pages/{id}":{"get":{"operationId":"getPage","summary":"Get page by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full page object"},"404":{"description":"Not found"}}},"patch":{"operationId":"updatePage","summary":"Update page","description":"Update page fields. Pass ifUpdatedAt (ISO 8601) for conflict prevention — returns 409 CONFLICT if record changed. Requires pages:write scope. Note: customJs requires trust tier 2+. `metadata` accepts free-form JSON; three slots have rich-result rendering side effects (see properties.metadata schema).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"status":{"type":"string"},"editorMode":{"type":"string"},"customJs":{"type":"string","nullable":true,"description":"Custom JavaScript for this page. Set null to remove. Requires trust tier 2+."},"metadata":{"type":"object","additionalProperties":true,"description":"Page metadata. Free-form, but three slots trigger rich-result JSON-LD rendering at request time: `description` → meta/og/twitter description tags; `schema` → SoftwareApplication JSON-LD; `faqs` → FAQPage JSON-LD. All opt-in.","properties":{"title":{"type":"string","description":"Overrides page.title in <title> and JSON-LD WebPage.name"},"description":{"type":"string","description":"Renders as <meta name=\"description\"> + og:description + twitter:description. Without this, Google scrapes body text."},"ogImage":{"type":"string","description":"OG image URL. Upload via POST /api/agent/images first."},"schema":{"type":"object","description":"Emits SoftwareApplication JSON-LD when set. Triggers rich-result eligibility for product/tool pages.","required":["applicationCategory"],"properties":{"applicationCategory":{"type":"string","example":"BusinessApplication"},"operatingSystem":{"type":"string","example":"Web Browser"},"offers":{"type":"object","properties":{"price":{"type":"string","example":"0"},"priceCurrency":{"type":"string","example":"USD"}}},"aggregateRating":{"type":"object","description":"Don't fabricate — Google penalizes fake ratings.","properties":{"ratingValue":{"type":"number"},"reviewCount":{"type":"integer"},"bestRating":{"type":"number"},"worstRating":{"type":"number"}}}}},"faqs":{"type":"array","description":"Emits FAQPage JSON-LD when non-empty. Triggers FAQ rich-results that expand SERP footprint.","items":{"type":"object","required":["question","answer"],"properties":{"question":{"type":"string"},"answer":{"type":"string"}}}}}},"ifUpdatedAt":{"type":"string","format":"date-time","description":"Conflict check: reject if record updatedAt differs (409 CONFLICT)"}}}}}},"responses":{"200":{"description":"Updated"},"409":{"description":"Conflict — record modified since read"}}},"delete":{"operationId":"deletePage","summary":"Delete page","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/site":{"get":{"operationId":"getSite","summary":"Get site settings","description":"Returns tenant settings including theme preset, branding, header/footer HTML, custom CSS/JS, contact info, and AI settings. Visual tokens (colors, fonts, spacing, etc.) are managed via the design system API (/design-system). Requires site:read scope.","responses":{"200":{"description":"Tenant settings including theme configuration"}}},"patch":{"operationId":"updateSite","summary":"Update site settings","description":"Update tenant settings. For theme presets, set layoutTemplate. For custom visual tokens (colors, fonts, spacing, etc.), use the design system API (/design-system). Use headerHtml/footerHtml for custom header/footer HTML. Requires site:write scope. Trust tier requirements: customCss requires tier 1+; headerHtml, footerHtml, and customJs require tier 2+.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"timezone":{"type":"string"},"layoutTemplate":{"type":"string","enum":["default","minimal","blank","startuptools","schoolgpt","sxth","drsignout"],"description":"Theme preset — controls layout structure. Visual tokens (colors, fonts, spacing) are managed via the design system API (/design-system)."},"headerTheme":{"type":"string","enum":["none","minimal","full","centered","glass","solid","transparent"]},"footerTheme":{"type":"string","enum":["none","minimal","full","centered"]},"logoLayout":{"type":"string","enum":["icon-with-text","wordmark-only","text-only","icon-only"],"description":"Header brand rendering. icon-with-text (default): image + name + tagline. wordmark-only: image only (use when logoUrl already spells the brand). text-only: name + tagline, no image. icon-only: image, no text."},"headerHtml":{"type":"string","nullable":true,"description":"Custom header HTML — overrides preset header component. Set null to revert. Requires trust tier 2+."},"footerHtml":{"type":"string","nullable":true,"description":"Custom footer HTML — overrides preset footer component. Set null to revert. Requires trust tier 2+."},"customCss":{"type":"string","description":"Custom CSS injected after theme — use to override theme CSS variables or add Tailwind utilities. Requires trust tier 1+."},"customJs":{"type":"string","nullable":true,"description":"Custom JavaScript injected on all pages. Set null to remove. Requires trust tier 2+."},"logoUrl":{"type":"string"},"logoText":{"type":"string"},"tagline":{"type":"string"},"ctaText":{"type":"string","description":"Header CTA button text"},"ctaUrl":{"type":"string","description":"Header CTA button URL"},"faviconUrl":{"type":"string"},"footerColumns":{"type":"array","items":{"type":"object"},"description":"Footer columns with title and links"},"copyrightText":{"type":"string"},"homeTitle":{"type":"string"},"homeDescription":{"type":"string"},"contactEmail":{"type":"string"},"emailFromName":{"type":"string","maxLength":120,"description":"Sender display name for campaign sends. Without this + emailFromAddress, sends fall back to noreply@<slug>.plgd.ai (unverified — bounces)."},"emailFromAddress":{"type":"string","format":"email","description":"Sender address. Should be on the verified Resend sending domain (see POST /email/domains)."},"emailReplyTo":{"type":"string","format":"email","description":"Reply-To address. Defaults to emailFromAddress if omitted."},"previewFrameAncestors":{"type":"array","items":{"type":"string"},"maxItems":20,"description":"Origins allowed to iframe preview URLs (CSP frame-ancestors). Full https origins or https://*.domain wildcards."},"dailyCampaignSendsCap":{"type":"integer","minimum":1,"maximum":1000,"description":"Max distinct campaign dispatches per UTC day. Default 5. Requires trust tier 2+ to modify."},"dailyRecipientsCap":{"type":"integer","minimum":1,"maximum":1000000,"description":"Max recipients across all sends per UTC day. Default 10000. Requires trust tier 2+ to modify."},"googleAnalyticsId":{"type":"string","pattern":"^G-[A-Z0-9]{6,15}$","description":"GA4 measurement ID. Auto-injects gtag snippet on every page."},"googleTagManagerId":{"type":"string","pattern":"^GTM-[A-Z0-9]{5,10}$","description":"GTM container ID. Auto-injects the GTM snippet. Preferred over the legacy googleTagId field."},"metaPixelId":{"type":"string","pattern":"^\\d{15,16}$","description":"Meta Pixel ID. Auto-injects fbq snippet with PageView."},"posthogProjectKey":{"type":"string","pattern":"^phc_[A-Za-z0-9]{20,}$","description":"PostHog project key. Auto-injects snippet (host: us.i.posthog.com)."},"linkedInPartnerId":{"type":"string","pattern":"^\\d{5,12}$","description":"LinkedIn Insight Tag partner ID."},"metaCapiAccessToken":{"type":"string","minLength":8,"maxLength":1000,"description":"Server-side only — Meta Conversion API access token. Write-only via PATCH; never returned in GET responses. Reserved for the upcoming server-side event pipeline (Ask 2 of filing freq-1779898420868-g7029n, not yet wired)."},"googleMeasurementProtocolSecret":{"type":"string","minLength":8,"maxLength":1000,"description":"Server-side only — GA4 Measurement Protocol secret. Write-only via PATCH; never returned in GET responses. Reserved for the upcoming server-side event pipeline."}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Validation error (invalid email, name too long, etc.)"}}}},"/navigation":{"get":{"operationId":"listNavigation","summary":"List navigation items","description":"List all navigation menu items ordered by position. Requires navigation:read scope.","responses":{"200":{"description":"Array of navigation items"}}},"post":{"operationId":"createNavigationItem","summary":"Create navigation item","description":"Add a navigation menu item. Requires navigation:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","path"],"properties":{"label":{"type":"string"},"path":{"type":"string"},"description":{"type":"string"},"parentId":{"type":"string"},"order":{"type":"integer"},"isVisible":{"type":"boolean","default":true}}}}}},"responses":{"201":{"description":"Created"}}},"patch":{"operationId":"updateNavigationItem","summary":"Update navigation item","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"path":{"type":"string"},"description":{"type":"string"},"parentId":{"type":"string","nullable":true},"order":{"type":"integer"},"isVisible":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteNavigationItem","summary":"Delete navigation item","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/ideas":{"get":{"operationId":"listIdeas","summary":"List idea sessions","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Idea sessions with nested blog ideas"}}},"post":{"operationId":"generateIdeas","summary":"Generate article ideas with AI","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string"},"category":{"type":"string"},"provider":{"type":"string","enum":["claude","perplexity"],"default":"claude"},"numberOfIdeas":{"type":"integer","minimum":1,"maximum":15,"default":5}}}}}},"responses":{"201":{"description":"Generated ideas"}}}},"/seo/audit":{"get":{"operationId":"seoAudit","summary":"Run SEO audit","description":"Audit published articles for SEO issues. Returns retrieval, AEO, geo, and technical scores. Requires seo:read scope.","parameters":[{"name":"slug","in":"query","schema":{"type":"string"},"description":"Audit specific article"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":200}}],"responses":{"200":{"description":"Audit results with summary and per-article scores"}}}},"/seo/generate":{"post":{"operationId":"seoGenerate","summary":"Generate SEO metadata","description":"Generate alternative headline, FAQs, keywords, and location metadata for an article. Requires seo:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["articleId"],"properties":{"articleId":{"type":"string"}}}}}},"responses":{"200":{"description":"Generated SEO metadata"}}}},"/images/suggest":{"post":{"operationId":"suggestImages","summary":"Get image suggestions","description":"Search stock photos (Unsplash) or generate AI images (DALL-E). Requires images:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mode","summary"],"properties":{"mode":{"type":"string","enum":["stock","ai"]},"summary":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}},"count":{"type":"integer"}}}}}},"responses":{"200":{"description":"Image suggestions"}}}},"/usage":{"get":{"operationId":"getUsage","summary":"Get API usage stats","parameters":[{"name":"month","in":"query","schema":{"type":"string"},"description":"YYYY-MM format"},{"name":"logs","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Usage stats and recent logs"}}}},"/whoami":{"get":{"operationId":"whoami","summary":"Get current key info","description":"Returns key details, scopes, tenant info, trust tier, and current usage. No special scopes required.","responses":{"200":{"description":"Key info and usage","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"data":{"type":"object","properties":{"keyId":{"type":"string"},"tenantId":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"trustTier":{"type":"integer","description":"Trust tier level (0 = default, 1 = elevated, 2 = full). Determines access to privileged operations like custom CSS/JS injection, webhooks, and domain management."},"usage":{"type":"object"}}}}}}}}}}},"/theme-info":{"get":{"operationId":"getThemeInfo","summary":"Get active theme CSS variables","description":"Returns all CSS variables with current values, utility classes, font info, and color scheme. Requires site:read scope.","responses":{"200":{"description":"Theme variables, fonts, and utility classes"}}}},"/themes":{"get":{"operationId":"listThemes","summary":"List preset themes","description":"List all available preset themes with color summaries, fonts, and header/footer variants. Requires site:read scope.","responses":{"200":{"description":"Array of theme summaries"}}}},"/images":{"get":{"operationId":"listImages","summary":"List uploaded images","description":"List images uploaded by your tenant. Filterable by tag. Requires images:read scope.","parameters":[{"name":"tag","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Array of images with pagination meta"}}},"post":{"operationId":"uploadImage","summary":"Upload an image","description":"Upload an image. Accepts multipart or JSON+base64. Idempotent on SHA256 — re-uploading the same bytes returns the existing row with status 200 (vs 201 for fresh). Max 10MB. Cost 5¢. Requires images:write scope.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"alt":{"type":"string","maxLength":500},"tags":{"type":"string","description":"JSON array or comma-separated"},"externalId":{"type":"string","maxLength":200}}}},"application/json":{"schema":{"type":"object","required":["data","mimeType"],"properties":{"data":{"type":"string","description":"Base64-encoded bytes (data: prefix accepted)"},"mimeType":{"type":"string","enum":["image/jpeg","image/png","image/webp","image/gif","image/svg+xml"]},"alt":{"type":"string","maxLength":500},"tags":{"type":"array","items":{"type":"string"}},"externalId":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Idempotent hit — same bytes already uploaded; existing row returned"},"201":{"description":"Image uploaded"},"400":{"description":"Validation error (unsupported mime, too large, etc.)"},"409":{"description":"Duplicate externalId for this tenant"}}}},"/images/{id}":{"get":{"operationId":"getImage","summary":"Read one image","description":"Read a single image by id. Requires images:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Image record"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteImage","summary":"Delete an image","description":"Removes the image row. The underlying blob is reclaimed by a cleanup sweep. Requires images:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/integrations/google-analytics/connect":{"post":{"operationId":"connectGoogleAnalytics","summary":"Connect a GA4 property via service-account JSON","description":"Validates the SA JSON, round-trips a one-day ping to confirm read access, then stores the credentials on the tenant. SA JSON is NEVER returned by any subsequent GET. Requires analytics:write scope. Cost: 2¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["serviceAccountJson","propertyId"],"properties":{"serviceAccountJson":{"type":"object","description":"The full GCP service-account JSON object.","additionalProperties":true},"propertyId":{"type":"string","description":"Numeric GA4 property id (\"123456789\") or \"properties/123456789\" form."}}}}}},"responses":{"200":{"description":"{ connected: true, propertyId, lastValidatedAt }"},"400":{"description":"Invalid SA JSON / propertyId or SA lacks GA4 access"}}}},"/integrations/google-analytics/status":{"get":{"operationId":"getGoogleAnalyticsStatus","summary":"GA4 connection status","description":"Returns { connected, propertyId, lastValidatedAt }. Never includes the stored SA JSON. Requires analytics:read.","responses":{"200":{"description":"Connection status"}}}},"/integrations/google-analytics":{"delete":{"operationId":"disconnectGoogleAnalytics","summary":"Disconnect GA4 — clears stored credentials and propertyId","description":"Idempotent. Requires analytics:write.","responses":{"200":{"description":"{ connected: false }"}}}},"/integrations/google-analytics/sessions":{"get":{"operationId":"getGa4Sessions","summary":"Sessions + users with daily timeseries and prior-period delta","description":"Two-call: primary range with daily timeseries + prior range total for delta_pct. Requires analytics:read. Cost: 1¢.","parameters":[{"name":"range","in":"query","schema":{"type":"string","enum":["today","yesterday","7d","30d","mtd","custom"],"default":"7d"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"Required when range=custom"},{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Required when range=custom"}],"responses":{"200":{"description":"{ data: { total, prior_total, delta_pct, timeseries }, meta: { range } }"},"412":{"description":"Tenant is not connected to GA4 — POST /connect first"}}}},"/integrations/google-analytics/by-channel":{"get":{"operationId":"getGa4ByChannel","summary":"Sessions broken down by sessionDefaultChannelGroup","description":"Returns one row per channel (Paid Social, Direct, Organic Search, etc.) with sessions, users, engagement_rate, and conversions. Requires analytics:read.","parameters":[{"name":"range","in":"query","schema":{"type":"string","enum":["today","yesterday","7d","30d","mtd","custom"],"default":"7d"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Array of channel rows sorted by sessions desc"}}}},"/integrations/google-analytics/top-pages":{"get":{"operationId":"getGa4TopPages","summary":"Top pages by sessions","description":"Sorted by sessions desc. limit defaults to 25, caps at 100. avg_engagement_time_sec is userEngagementDuration / sessions, rounded. Requires analytics:read.","parameters":[{"name":"range","in":"query","schema":{"type":"string","enum":["today","yesterday","7d","30d","mtd","custom"],"default":"7d"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"Array of page rows sorted by sessions desc"}}}},"/sequences":{"get":{"operationId":"listSequences","summary":"List lifecycle sequences","description":"List this tenant's lifecycle drip sequences. Requires sequences:read scope.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Array of sequences with their steps"}}},"post":{"operationId":"createSequence","summary":"Create a lifecycle sequence","description":"Create a drip sequence with up to 20 steps. Step offsets are relative to enrollment time. Requires sequences:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","steps"],"properties":{"name":{"type":"string"},"externalId":{"type":"string","maxLength":200,"description":"Integrator-supplied correlation id, unique per tenant"},"offsetMode":{"type":"string","enum":["cumulative","absolute"],"default":"cumulative","description":"Step timing semantics. `absolute` measures each step's offset from enrollment.enrolledAt; `cumulative` (default, back-compat) from the previous step's processing time. Snapshotted per-enrollment at enroll time so changing the mode mid-flight only affects new enrollments."},"steps":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"object","required":["subject","body"],"properties":{"key":{"type":"string","maxLength":80,"description":"Step key for analytics correlation (welcome, discover, etc.)"},"offsetDays":{"type":"integer","minimum":0,"description":"Days after enrollment to send. Exclusive with offsetHours."},"offsetHours":{"type":"integer","minimum":0,"description":"Hours after enrollment. Use 0 for send-immediately."},"subject":{"type":"string","description":"May reference {{var.<name>}} merge variables."},"body":{"type":"string","description":"HTML body. May reference {{var.<name>}}."},"preheader":{"type":"string","maxLength":200}}}}}}}}},"responses":{"201":{"description":"Sequence created"},"400":{"description":"Validation error"},"409":{"description":"Duplicate externalId for this tenant"}}}},"/sequences/{id}":{"get":{"operationId":"getSequence","summary":"Read one sequence","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sequence with steps"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateSequence","summary":"Update a sequence","description":"Update name, status, or replace the step set. Replacing steps[] propagates to in-flight enrollments on their next scheduled fire. Per-enrollment variables stay frozen.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","stopped"]},"offsetMode":{"type":"string","enum":["cumulative","absolute"],"description":"Change the sequence's timing semantics. Only NEW enrollments after this PATCH pick up the new mode — in-flight enrollments keep their snapshot."},"steps":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Updated sequence"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteSequence","summary":"Soft-delete a sequence","description":"Stops the sequence and cancels all in-flight enrollments. Message rows remain queryable for analytics.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/sequences/{id}/send-test":{"post":{"operationId":"sendSequenceTest","summary":"Test-send a sequence to one email address","description":"Collapses the cadence and sends every step IMMEDIATELY to the supplied email. For previewing the full drip before going live. Same render path as production sends (integrator {{var.*}} + system vars). Charges 1¢ per step. Skips Message persistence — Resend dashboard is the audit trail. Requires sequences:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Recipient email address."},"variables":{"type":"object","additionalProperties":true,"description":"Merge variables (same shape as enroll). first_name/last_name fall back to \"Test\"/\"Recipient\" if not supplied."}}}}}},"responses":{"200":{"description":"{ sequenceId, email, sent: N, failed: N, steps: [...] }"},"400":{"description":"Validation error (missing email, no steps, no sender identity, too many steps)"},"404":{"description":"Sequence not found"}}}},"/sequences/{id}/enroll":{"post":{"operationId":"enrollInSequence","summary":"Enroll a contact in a sequence","description":"The core lifecycle primitive. Idempotent on externalId (per-sequence) and on (sequence, contact) — re-POSTing returns the existing enrollment with status 200. Suppressed contacts return 200 with skipped: true. Requires sequences:enroll scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contactId":{"type":"string"},"email":{"type":"string","format":"email"},"externalId":{"type":"string","maxLength":200},"variables":{"type":"object","additionalProperties":true,"description":"Per-recipient merge data; frozen at enroll time. Referenced from step copy as {{var.<key>}}."},"startAt":{"type":"string","format":"date-time","description":"When enrollment \"starts\" for offset math. Defaults to now."},"testRecipient":{"type":"string","format":"email","description":"Optional. When set, every step send for this enrollment is diverted to this address. Real enrollment + cadence + webhooks + supersede + suppression — full pipeline behavior, just delivered to a test inbox. Use to QA the full flow before going live. sequence:* webhook payloads include testRecipient."}}}}}},"responses":{"200":{"description":"Idempotent hit — existing enrollment returned (or contact suppressed)"},"201":{"description":"New enrollment created"},"400":{"description":"Validation error (no contactId or email, sequence not active, etc.)"},"404":{"description":"Sequence or contact not found"}}}},"/sequences/{id}/unenroll":{"post":{"operationId":"unenrollFromSequence","summary":"Cancel active enrollments","description":"Cancels active enrollment(s) in this sequence. Resolves by enrollmentId | contactId | email. Idempotent — no-op if not active. Use for supersede rules and stop-on-conversion. Requires sequences:enroll scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enrollmentId":{"type":"string"},"contactId":{"type":"string"},"email":{"type":"string","format":"email"},"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Cancelled — { stopped: N, enrollmentIds: [...] } or skipped: true"}}}},"/sequences/{id}/enrollments":{"get":{"operationId":"listEnrollments","summary":"List enrollments for a sequence","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","completed","stopped","failed"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Array of enrollments"}}}},"/sequences/{id}/enrollments/{enrollmentId}":{"get":{"operationId":"getEnrollment","summary":"Read one enrollment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"enrollmentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Enrollment with variables"},"404":{"description":"Not found"}}}},"/feature-requests":{"get":{"operationId":"listFeatureRequests","summary":"List your feature requests","description":"List feature requests you have filed (tenant-scoped). Free. Requires feature-requests:read scope.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","TRIAGED","IN_PROGRESS","SHIPPED","WONTFIX","DUPLICATE"]}},{"name":"category","in":"query","schema":{"type":"string","enum":["ENDPOINT","SCOPE","SCHEMA","BUG","OTHER"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Array of feature requests with pagination meta"}}},"post":{"operationId":"createFeatureRequest","summary":"File a feature request","description":"File a gap, bug, or wishlist item with the plgd operator. Free. ENDPOINT/SCOPE/SCHEMA/BUG categories page the operator; OTHER does not. Requires feature-requests:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","body"],"properties":{"title":{"type":"string","maxLength":200,"description":"Short summary (one line)"},"body":{"type":"string","maxLength":10000,"description":"Full description — what you tried, what you expected, what happened"},"category":{"type":"string","enum":["ENDPOINT","SCOPE","SCHEMA","BUG","OTHER"],"default":"OTHER","description":"ENDPOINT/SCOPE/SCHEMA = capability gap; BUG = something is broken; OTHER = wishlist"},"urgency":{"type":"string","enum":["LOW","MEDIUM","HIGH","BLOCKING"],"default":"MEDIUM","description":"Use BLOCKING only if this actually stops your flow"},"externalId":{"type":"string","maxLength":200,"description":"Your stable ID — idempotent per-tenant"}}}}}},"responses":{"201":{"description":"Feature request filed"},"400":{"description":"Validation error"},"409":{"description":"Duplicate externalId for this tenant"}}}},"/feature-requests/{id}":{"get":{"operationId":"getFeatureRequest","summary":"Read one feature request","description":"Read a single filing (own tenant only). Useful for polling status. Requires feature-requests:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Feature request with current status + resolution if closed"},"404":{"description":"Not found (or not yours)"}}}},"/redirects":{"get":{"operationId":"listRedirects","summary":"List redirects","description":"List all URL redirects. Requires redirects:read scope.","parameters":[{"name":"active","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Filter by active status"}],"responses":{"200":{"description":"Array of redirects"}}},"post":{"operationId":"createRedirect","summary":"Create/upsert redirect","description":"Create or update a URL redirect. Upserts by fromPath. Requires redirects:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fromPath","toPath"],"properties":{"fromPath":{"type":"string","description":"Source path (e.g. /old-page)"},"toPath":{"type":"string","description":"Target path or URL"},"statusCode":{"type":"integer","enum":[301,302,307,308],"default":301},"isActive":{"type":"boolean","default":true}}}}}},"responses":{"201":{"description":"Redirect created/updated"}}},"delete":{"operationId":"deleteRedirect","summary":"Delete redirect","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/articles/batch":{"post":{"operationId":"batchCreateArticles","summary":"Batch create articles","description":"Create multiple articles in one call (max 20). Each article costs 2¢. Requires articles:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["articles"],"properties":{"articles":{"type":"array","maxItems":20,"items":{"type":"object","required":["title","slug"],"properties":{"title":{"type":"string"},"slug":{"type":"string"},"content":{"type":"string"},"excerpt":{"type":"string"},"category":{"type":"string"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","SCHEDULED"]},"scheduledAt":{"type":"string","format":"date-time"}}}}}}}}},"responses":{"201":{"description":"Articles created"},"402":{"description":"Insufficient credits"}}}},"/pages/{id}/render":{"get":{"operationId":"renderPage","summary":"Render page HTML","description":"Returns full HTML document with theme CSS, header, content, and footer. Content-Type: text/html. Requires pages:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full HTML document"},"404":{"description":"Not found"}}}},"/pages/{id}/revisions":{"get":{"operationId":"listRevisions","summary":"List page revisions","description":"List all revisions for a page with version numbers and change notes. Requires pages:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of revisions with currentVersion in meta"}}},"post":{"operationId":"restoreRevision","summary":"Restore page to previous version","description":"Restore a page to a specific version. Saves current state as a new revision first. Requires pages:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"query","required":true,"schema":{"type":"integer"},"description":"Version number to restore"}],"responses":{"200":{"description":"Restored with new version number"},"404":{"description":"Page or version not found"}}}},"/contacts":{"get":{"operationId":"listContacts","summary":"List contacts","description":"List email contacts with filtering and search. Requires contacts:read scope.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"status","in":"query","schema":{"type":"string","enum":["ACTIVE","UNSUBSCRIBED","BOUNCED"]}},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag"},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search by email, first name, or last name"}],"responses":{"200":{"description":"List of contacts with pagination"}}},"post":{"operationId":"createContact","summary":"Create contact","description":"Create an email contact. Requires contacts:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"status":{"type":"string","default":"ACTIVE"},"source":{"type":"string","default":"agent-api"},"tags":{"type":"array","items":{"type":"string"}},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"Contact created"},"409":{"description":"Duplicate email"}}}},"/campaigns":{"get":{"operationId":"listCampaigns","summary":"List campaigns","description":"List email campaigns. Requires campaigns:read scope.","parameters":[{"name":"type","in":"query","schema":{"type":"string"},"description":"Filter by type (BROADCAST, etc.)"},{"name":"status","in":"query","schema":{"type":"string"},"description":"Filter by status (DRAFT, SCHEDULED, SENT, etc.)"}],"responses":{"200":{"description":"List of campaigns"}}},"post":{"operationId":"createCampaign","summary":"Create campaign (defaults to draft)","description":"Create an email campaign. **Status defaults to `draft`** so a human can review + edit before send. Pair with PATCH /campaigns/{id} (edit draft) and POST /campaigns/{id}/send (dispatch). Requires campaigns:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","emailSubject","emailBody"],"properties":{"name":{"type":"string"},"emailSubject":{"type":"string"},"emailBody":{"type":"string"},"description":{"type":"string"},"emailPreheader":{"type":"string"},"type":{"type":"string","default":"BROADCAST"},"audienceType":{"type":"string","default":"all_active"},"audienceFilters":{"type":"object"},"scheduledAt":{"type":"string","format":"date-time"},"externalId":{"type":"string","maxLength":200,"description":"Integrator-supplied stable identifier echoed in webhook payloads and GET responses."}}}}}},"responses":{"201":{"description":"Campaign created (status=draft unless scheduledAt is set)"}}}},"/campaigns/{id}":{"get":{"operationId":"getCampaign","summary":"Get a campaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateCampaign","summary":"Edit a campaign draft","description":"Allowed only when status=\"draft\". Returns 409 CONFLICT if the campaign has moved past draft (sent / scheduled / sending). Pass scheduledAt:null to unset a schedule and return to draft.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"emailSubject":{"type":"string"},"emailBody":{"type":"string"},"emailPreheader":{"type":"string"},"audienceType":{"type":"string"},"audienceFilters":{"type":"object"},"scheduledAt":{"type":"string","format":"date-time","nullable":true},"externalId":{"type":"string","nullable":true,"maxLength":200}}}}}},"responses":{"200":{"description":"Updated draft"},"404":{"description":"Not found"},"409":{"description":"Campaign is past draft"}}}},"/campaigns/{id}/send":{"post":{"operationId":"sendCampaign","summary":"Dispatch a draft campaign (canonical)","description":"Send a draft (or scheduled-not-yet-sent) campaign to its audience. Canonical send path paired with the draft+PATCH flow. /campaigns/{id}/send-now stays as an alias for ≥1 release. Requires campaigns:send scope. Cost: 2¢ + 1¢ per recipient.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Send dispatched: { campaignId, recipients, sent, failed }"},"402":{"description":"Insufficient credits for the recipient count"},"404":{"description":"Campaign not found"}}}},"/articles/{id}/analytics":{"get":{"operationId":"getArticleAnalytics","summary":"Get article analytics","description":"Get article view stats and trends. Use id=\"top\" for top articles by views. Requires analytics:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Article ID or \"top\""},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100},"description":"Max results when id=\"top\""}],"responses":{"200":{"description":"Analytics data with trends"},"404":{"description":"Article not found"}}}},"/domains":{"get":{"operationId":"listDomains","summary":"List domains","description":"List hostnames and canonical host. Requires site:read scope.","responses":{"200":{"description":"Hostnames and canonical host"}}},"post":{"operationId":"addDomain","summary":"Add domain","description":"Add a hostname to the tenant. Requires domains:write scope. Requires trust tier 1+.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hostname"],"properties":{"hostname":{"type":"string","description":"Hostname to add (e.g. mysite.example.com)"}}}}}},"responses":{"201":{"description":"Domain added"},"409":{"description":"Hostname already in use"}}},"delete":{"operationId":"removeDomain","summary":"Remove domain","description":"Remove a hostname from the tenant. Cannot remove the last one. Requires domains:write scope. Requires trust tier 1+.","parameters":[{"name":"hostname","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Domain removed"},"400":{"description":"Cannot remove last hostname"}}}},"/sitemap-entries":{"get":{"operationId":"getSitemap","summary":"Get sitemap state","description":"Get current sitemap state: noIndex flag, published articles, and pages. Requires seo:read scope.","responses":{"200":{"description":"Sitemap entries with noIndex status"}}},"patch":{"operationId":"updateSitemap","summary":"Update sitemap settings","description":"Toggle noIndex for entire site or set blogUrlPattern. Requires seo:write scope.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"noIndex":{"type":"boolean","description":"Set true to add noindex to entire site"},"blogUrlPattern":{"type":"string","enum":["blog","articles","news","posts"],"description":"Blog URL prefix"}}}}}},"responses":{"200":{"description":"Updated"}}}},"/webhooks":{"get":{"operationId":"listWebhooks","summary":"List webhook subscriptions","description":"List webhook subscriptions with delivery counts. Requires webhooks:read scope.","responses":{"200":{"description":"Array of webhook subscriptions"}}},"post":{"operationId":"createWebhook","summary":"Create webhook subscription","description":"Subscribe to events. Returns a signing secret (shown only once). Requires webhooks:write scope. Requires trust tier 1+.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","description":"Delivery URL (HTTPS recommended)"},"events":{"type":"array","items":{"type":"string","enum":["article:published","article:updated","article:deleted","page:published","page:updated","page:deleted","contact:created","contact:updated","form:submitted","seo:alert","job:completed","job:failed"]},"description":"Event types to subscribe to"},"headers":{"type":"object","description":"Custom headers for delivery"},"isActive":{"type":"boolean","default":true},"maxRetries":{"type":"integer","default":3}}}}}},"responses":{"201":{"description":"Subscription created with signing secret"}}},"delete":{"operationId":"deleteWebhook","summary":"Delete webhook subscription","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/forms":{"get":{"operationId":"listForms","summary":"List forms","description":"List forms with submission counts. Requires forms:read scope.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["ACTIVE","INACTIVE","ARCHIVED"]}}],"responses":{"200":{"description":"Array of forms"}}},"post":{"operationId":"createForm","summary":"Create form","description":"Create a form with field definitions. Requires forms:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","slug","fields"],"properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Unique per tenant"},"description":{"type":"string"},"fields":{"type":"array","items":{"type":"object","required":["name","type"],"properties":{"name":{"type":"string"},"type":{"type":"string","enum":["text","email","textarea","select","checkbox","number","tel","url","date","hidden"]},"label":{"type":"string"},"required":{"type":"boolean"},"placeholder":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}}}},"submitButtonText":{"type":"string","default":"Submit"},"successMessage":{"type":"string"},"redirectUrl":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE"],"default":"ACTIVE"},"honeypotEnabled":{"type":"boolean","default":true}}}}}},"responses":{"201":{"description":"Form created"},"409":{"description":"Duplicate slug"}}}},"/forms/{id}":{"get":{"operationId":"getForm","summary":"Get form by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Form with submission count"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateForm","summary":"Update form","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string"},"fields":{"type":"array","items":{"type":"object"}},"submitButtonText":{"type":"string"},"successMessage":{"type":"string"},"redirectUrl":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","ARCHIVED"]},"honeypotEnabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteForm","summary":"Delete form","description":"Delete form and all submissions. Requires forms:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/forms/{id}/submissions":{"get":{"operationId":"listFormSubmissions","summary":"List form submissions","description":"List submissions for a form with pagination. Requires forms:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Array of submissions with pagination"}}},"post":{"operationId":"submitForm","summary":"Submit form data","description":"Submit data to a form. Auto-creates a contact if email field is present. Triggers form:submitted webhook. Requires forms:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Form field values keyed by field name","additionalProperties":true}}}},"responses":{"201":{"description":"Submission created"},"400":{"description":"Validation error or spam detected"},"404":{"description":"Form not found or inactive"}}}},"/creatives":{"get":{"operationId":"listCreatives","summary":"List creative pages","description":"List AI-generated creative pages. Requires creatives:read scope.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","PUBLISHED","DISABLED"]}}],"responses":{"200":{"description":"Array of creative pages"}}}},"/creatives/generate":{"post":{"operationId":"generateCreative","summary":"Generate AI creative page","description":"Generate a high-design interactive landing page from a natural language brief. Uses Claude to produce complete HTML with scroll animations, gradient text, glassmorphism, and more. Set includeImages: true to generate AI images via fal.ai (+15¢ surcharge, paid/test mode only). Requires creatives:write scope. Cost: 25¢ (40¢ with images).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Natural language brief describing the page"},"style":{"type":"string","description":"Visual style (e.g. minimalist, brutalist, futuristic)"},"headline":{"type":"string"},"subheadline":{"type":"string"},"ctaText":{"type":"string","description":"Call-to-action button text"},"ctaUrl":{"type":"string","description":"Call-to-action button URL"},"sections":{"type":"array","items":{"type":"string"},"description":"Sections to include"},"path":{"type":"string","description":"URL path (auto-generated if omitted)"},"status":{"type":"string","enum":["DRAFT","PUBLISHED"],"default":"DRAFT"},"includeNav":{"type":"boolean","description":"Include fixed navigation bar"},"includeFooter":{"type":"boolean","description":"Include footer with links"},"includeImages":{"type":"boolean","description":"Generate AI images via fal.ai (+15¢ surcharge, paid/test mode only)"},"advancedTypography":{"type":"boolean","description":"Enable Pretext-powered advanced text effects: text flowing around images, Moses effect, kinetic typography"}}}}}},"responses":{"201":{"description":"Creative page generated and stored"},"402":{"description":"Insufficient credits"}}}},"/creatives/{id}":{"get":{"operationId":"getCreative","summary":"Get creative page","description":"Get a creative page by ID with full HTML content and generation metadata. Requires creatives:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Creative page with HTML and metadata"},"404":{"description":"Not found or not a creative"}}}},"/creatives/{id}/refine":{"post":{"operationId":"refineCreative","summary":"Refine creative page","description":"Refine an existing creative page with natural language feedback. Preserves design quality while applying changes. Requires creatives:write scope. Cost: 15¢.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["feedback"],"properties":{"feedback":{"type":"string","description":"Natural language feedback describing changes to apply"}}}}}},"responses":{"200":{"description":"Creative refined"},"402":{"description":"Insufficient credits"},"404":{"description":"Not found or not a creative"}}}},"/jobs":{"get":{"operationId":"listJobs","summary":"List async jobs","description":"List async AI jobs with optional status filter. Requires jobs:read scope.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["PENDING","PROCESSING","COMPLETED","FAILED"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Array of jobs"}}},"post":{"operationId":"createJob","summary":"Create async AI job","description":"Queue an async AI job. Processed by cron worker. Poll GET /jobs/{id} for status. Requires jobs:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["story:create","story:outline","story:draft","story:images","seo:generate","ideas:generate","creative:generate","creative:refine"],"description":"Job action to execute"},"resourceId":{"type":"string","description":"Target resource ID (e.g. article ID)"},"payload":{"type":"object","description":"Job-specific parameters"}}}}}},"responses":{"201":{"description":"Job queued"}}}},"/jobs/{id}":{"get":{"operationId":"getJob","summary":"Get job status","description":"Get async job status and result. Poll until status is COMPLETED or FAILED. Requires jobs:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job with status and result"},"404":{"description":"Not found"}}}},"/design-system":{"get":{"operationId":"getDesignSystem","summary":"Get design system","description":"Get the tenant's design system including tokens and generated CSS. Requires design:read scope.","responses":{"200":{"description":"Design system with tokens and CSS"},"404":{"description":"No design system configured"}}},"patch":{"operationId":"updateDesignSystem","summary":"Update design system tokens","description":"Update design system tokens. CSS is regenerated automatically. Requires design:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tokens"],"properties":{"tokens":{"type":"object","description":"Design tokens to merge/update: colors, typography, spacing, borders, shadows, etc."}}}}}},"responses":{"200":{"description":"Design system updated with regenerated CSS"},"404":{"description":"No design system configured"}}},"delete":{"operationId":"deleteDesignSystem","summary":"Remove design system","description":"Remove the tenant's design system. Requires design:write scope.","responses":{"200":{"description":"Design system removed"},"404":{"description":"No design system configured"}}}},"/design-system/generate":{"post":{"operationId":"generateDesignSystem","summary":"Generate design system","description":"Generate a complete design system from a natural language brief using AI. Produces tokens (colors, typography, spacing, etc.) and CSS custom properties. Requires design:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["brief"],"properties":{"brief":{"type":"string","description":"Natural language brief describing the desired design system (e.g. \"Modern SaaS with blue primary, clean typography\")"},"generateShowcase":{"type":"boolean","description":"If true, also generate an AI-crafted showcase page at /design-system-showcase (+25¢). A static reference is always available at /design-system."}}}}}},"responses":{"201":{"description":"Design system generated"},"402":{"description":"Insufficient credits"}}}},"/components":{"get":{"operationId":"listComponents","summary":"List components","description":"List design components with optional filtering by category and search. Requires design:read scope.","parameters":[{"name":"category","in":"query","schema":{"type":"string","enum":["LAYOUT","NAVIGATION","FORM","BUTTON","CARD","FEEDBACK","DATA_DISPLAY","HERO","SECTION","FOOTER","CUSTOM"]},"description":"Filter by component category"},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search components by name, description, or tags"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"List of components with pagination metadata"}}},"post":{"operationId":"createComponent","summary":"Create component","description":"Create a design component. Requires design:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","html"],"properties":{"name":{"type":"string"},"html":{"type":"string","description":"Component HTML markup"},"slug":{"type":"string","description":"URL-friendly identifier (auto-generated from name if omitted)"},"description":{"type":"string"},"category":{"type":"string","enum":["LAYOUT","NAVIGATION","FORM","BUTTON","CARD","FEEDBACK","DATA_DISPLAY","HERO","SECTION","FOOTER","CUSTOM"],"default":"CUSTOM"},"css":{"type":"string","description":"Component-scoped CSS"},"variants":{"type":"array","items":{"type":"object"},"description":"Component variants with different styles or configurations"},"props":{"type":"array","items":{"type":"object"},"description":"Configurable props/parameters for the component"},"usage":{"type":"string","description":"Usage instructions and examples"},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Component created"},"409":{"description":"Duplicate slug"}}}},"/components/generate":{"post":{"operationId":"generateComponent","summary":"AI generate component","description":"Generate a design component from a natural language prompt using AI. Uses the tenant's design system tokens if available. Requires design:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Natural language description of the component to generate"},"category":{"type":"string","enum":["LAYOUT","NAVIGATION","FORM","BUTTON","CARD","FEEDBACK","DATA_DISPLAY","HERO","SECTION","FOOTER","CUSTOM"]},"name":{"type":"string","description":"Component name (auto-generated if omitted)"},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Component generated"},"402":{"description":"Insufficient credits"}}}},"/components/{id}":{"get":{"operationId":"getComponent","summary":"Get component by ID","description":"Get a design component with full HTML, CSS, variants, and props. Requires design:read scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full component object"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateComponent","summary":"Update component","description":"Update component fields. Only provided fields are changed. Requires design:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["LAYOUT","NAVIGATION","FORM","BUTTON","CARD","FEEDBACK","DATA_DISPLAY","HERO","SECTION","FOOTER","CUSTOM"]},"html":{"type":"string"},"css":{"type":"string"},"variants":{"type":"array","items":{"type":"object"}},"props":{"type":"array","items":{"type":"object"}},"usage":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"},"409":{"description":"Duplicate slug"}}},"delete":{"operationId":"deleteComponent","summary":"Delete component","description":"Delete a design component. Requires design:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/snippets":{"get":{"operationId":"listSnippets","summary":"List reusable content snippets","description":"List tenant-scoped, named blocks of HTML/markdown referenced across pages and emails. Requires snippets:read scope.","responses":{"200":{"description":"Snippets list"}}},"post":{"operationId":"upsertSnippet","summary":"Create or upsert a snippet","description":"Upsert by (tenantId, name). Useful for shared CTAs, disclaimers, author bios. Requires snippets:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","label","content"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9 _-]{1,80}$","description":"Unique within tenant; used as the embed key"},"label":{"type":"string","description":"Human-readable label"},"content":{"type":"string","description":"HTML/markdown body"},"description":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Snippet created or updated"},"400":{"description":"Validation error"}}}},"/email/layouts":{"get":{"operationId":"listEmailLayouts","summary":"List reusable email layouts (shells)","description":"A layout wraps every campaign email with consistent header/footer/styles. Requires email-layouts:read scope.","responses":{"200":{"description":"Layouts list"}}},"post":{"operationId":"upsertEmailLayout","summary":"Create or upsert an email layout","description":"Upsert by (tenantId, name). wrapperHtml must contain a {{content}} placeholder. Response meta.warnings flags client-render pitfalls (NO_DOCTYPE, NO_TABLE_LAYOUT, FLEX_OR_GRID, NO_UNSUBSCRIBE, NO_PREHEADER, etc). CSS is inlined at send time via juice. Requires email-layouts:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","wrapperHtml"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9 _-]{1,80}$"},"wrapperHtml":{"type":"string","description":"Email shell HTML with {{content}} placeholder; add a {{preheader}} placeholder in a hidden div for per-message preview text"},"headerHtml":{"type":"string","nullable":true},"footerHtml":{"type":"string","nullable":true,"description":"Should contain {{unsubscribe_url}} for marketing sends"},"styles":{"type":"string","nullable":true,"description":"CSS — inlined into element style attrs at send time; media queries preserved"},"description":{"type":"string","nullable":true},"isDefault":{"type":"boolean","description":"Marking default unsets default on others"},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Layout updated"},"201":{"description":"Layout created"},"400":{"description":"Validation error (missing {{content}} placeholder, invalid name, etc.)"}}}},"/email/mailboxes":{"get":{"operationId":"listMailboxes","summary":"List provisioned Migadu mailboxes","description":"Returns mailboxes across the tenant's verified email domains. Requires email-mailboxes:read scope.","responses":{"200":{"description":"Mailboxes list"}}},"post":{"operationId":"createMailbox","summary":"Provision a new Migadu mailbox","description":"Creates a real mailbox (e.g. hello@theirdomain.com) on a verified tenant domain. Password is returned ONCE in the response — store it immediately. RFC 2142 reserved aliases (postmaster, abuse, admin, noreply, etc) are blocked. Requires email-mailboxes:write scope. Cost: 25¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domainId","localPart"],"properties":{"domainId":{"type":"string","description":"ID of a verified EmailDomain owned by this tenant"},"localPart":{"type":"string","description":"Part before the @ — 1-64 lowercase alphanumerics/dots/dashes/underscores; no leading/trailing punctuation; no consecutive dots"},"displayName":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Mailbox created with one-time password"},"400":{"description":"Validation error (reserved localPart, malformed, domain not verified)"},"404":{"description":"Domain not found"},"502":{"description":"Migadu provisioning failed"}}}},"/email/suppressions":{"get":{"operationId":"listSuppressions","summary":"List suppressed email addresses","description":"Bounced, unsubscribed, or complained addresses. Always consult before bulk-adding recipients to a campaign. Requires email-suppressions:read scope.","parameters":[{"name":"reason","in":"query","schema":{"type":"string","enum":["bounced","unsubscribed","complained"]}},{"name":"email","in":"query","schema":{"type":"string","format":"email"},"description":"Filter to exact email"},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Paginated suppressions list"}}}},"/email/templates":{"get":{"operationId":"listEmailTemplates","summary":"List email templates","description":"Paginated list. Requires email-templates:read scope.","parameters":[{"name":"category","in":"query","schema":{"type":"string","enum":["transactional","lifecycle","marketing"]}},{"name":"isActive","in":"query","schema":{"type":"boolean"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"Templates list"}}},"post":{"operationId":"createEmailTemplate","summary":"Create an email template (hand-written HTML)","description":"Plain CRUD; for AI-drafted templates use POST /email/templates/generate. Variables in the body use {{name}} syntax and are auto-extracted into the `variables` array. Requires email-templates:write scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","category"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9 _-]{1,80}$"},"subject":{"type":"string","maxLength":200},"body":{"type":"string","description":"HTML body; variables use {{name}} syntax"},"category":{"type":"string","enum":["transactional","lifecycle","marketing"]},"preheader":{"type":"string","maxLength":200,"description":"Inbox preview text (≤90 chars typically shown)"},"variables":{"type":"array","items":{"type":"string"},"description":"Auto-extracted from body if omitted"},"isActive":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Template created"},"400":{"description":"Validation error"}}}},"/email/templates/{id}":{"get":{"operationId":"getEmailTemplate","summary":"Get one email template","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateEmailTemplate","summary":"Update an email template","description":"Partial update. Requires email-templates:write scope.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9 _-]{1,80}$"},"subject":{"type":"string","maxLength":200},"body":{"type":"string"},"category":{"type":"string","enum":["transactional","lifecycle","marketing"]},"preheader":{"type":"string","maxLength":200},"variables":{"type":"array","items":{"type":"string"}},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Validation error"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteEmailTemplate","summary":"Delete an email template","description":"409 if the template is in use by an active/scheduled/sending campaign or automation.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"},"409":{"description":"Template is in use"}}}},"/email/domains":{"get":{"operationId":"getEmailDomain","summary":"Get sending-domain status + DNS records","description":"Re-verifies upstream with Resend and mirrors status back onto the tenant. Requires email-domains:read scope.","responses":{"200":{"description":"Domain status (or null if not provisioned)"}}},"post":{"operationId":"createEmailDomain","summary":"Provision a Resend sending domain","description":"Returns DNS records the user must add at their registrar. Distinct from /domains, which manages routing hostnames. Required for campaign sends to deliver. Requires email-domains:write scope. Cost: 5¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","description":"Subdomain on a domain the user controls (e.g. \"mail.acme.com\"). Reserved roots (plgd.ai, localhost, example.com) are blocked."}}}}}},"responses":{"201":{"description":"Domain provisioned; returns DNS records"},"400":{"description":"Validation error or already provisioned"},"502":{"description":"Resend upstream failure"}}}},"/translations/translate":{"post":{"operationId":"translateText","summary":"Translate text via TranslationMemory + Sonnet","description":"Cache hits skip the model. Use IETF locale tags (e.g. \"es-ES\"). Requires translations:write scope. Cost: 15¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text","targetLocale"],"properties":{"text":{"type":"string","maxLength":20000},"targetLocale":{"type":"string","description":"e.g. \"es-ES\", \"fr-FR\""},"sourceLocale":{"type":"string","description":"Defaults to tenant locale"},"context":{"type":"object","description":"Optional translation context (e.g. { domain: \"marketing\" })"}}}}}},"responses":{"200":{"description":"Translated text with cached flag"},"400":{"description":"Validation error"}}}},"/seo/suggest-links":{"post":{"operationId":"suggestInternalLinks","summary":"Suggest internal links (articles + pages)","description":"Score the tenant's published content for relevance to a source article OR page. Returns suggestions with the in-body context where each link fits. Output identifiers are clamped to the candidate set as defense-in-depth against model misbehavior. Two body shapes accepted (legacy + general — see schema). Requires seo:write scope. Cost: 15¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provide EITHER `articleId` (legacy, articles-only) OR `source: { type, id }` (new, cross-content).","properties":{"articleId":{"type":"string","description":"Legacy: source article id. When set, targetTypes defaults to [\"articles\"] to preserve prior behavior."},"source":{"type":"object","description":"New: structured source. Use for pages or to opt into cross-content suggestions.","required":["type","id"],"properties":{"type":{"type":"string","enum":["article","page"]},"id":{"type":"string"}}},"targetTypes":{"type":"array","items":{"type":"string","enum":["articles","pages"]},"description":"Which content types to suggest from. Default: both when using `source`; articles-only when using legacy `articleId`."},"maxResults":{"type":"integer","minimum":3,"maximum":20,"default":8}}}}}},"responses":{"200":{"description":"Suggestions array. Each item has type (\"article\" | \"page\"), title, url, relevance (\"high\" | \"medium\" | \"low\"), context (5-15 word phrase from source body where link fits). Articles also expose `slug`; pages expose `path`."},"404":{"description":"Source article or page not found"}}}},"/feed":{"get":{"operationId":"readFeed","summary":"Pull-based unified feed of tenant events","description":"Stateless agents catch up on \"what happened since I last checked\" with this endpoint instead of hosting a webhook URL. Every event dispatched through our webhook system also writes here. Reverse-chronological (newest first), cursor-paginated.","parameters":[{"name":"since","in":"query","schema":{"type":"string"},"description":"Opaque cursor from a prior response's meta.nextCursor"},{"name":"type","in":"query","schema":{"type":"array","items":{"type":"string"}},"description":"Repeatable. Filter to specific event types."},{"name":"source","in":"query","schema":{"type":"array","items":{"type":"string"}},"description":"Repeatable. Filter by source."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"{ ok, data: [{ id, type, source, occurredAt, payload, externalId? }], meta: { count, hasMore, nextCursor } }"},"400":{"description":"Invalid cursor"}}}},"/brand":{"get":{"operationId":"getBrand","summary":"Read the tenant brand profile + derived legacy fields","responses":{"200":{"description":"{ brandProfile, derived: { tagline, aiVoiceTone, aiAudience, emailFromName, emailFromAddress, emailReplyTo } }"}}},"put":{"operationId":"putBrand","summary":"Single-call brand identity push (fans out)","description":"Updates Tenant.brandProfile (canonical) + fans out to tagline, aiVoiceTone (composed from voice + tone), aiAudience, sender identity. When emailLayoutHtml is set, upserts the default EmailLayout. Resolves {{brand.*}} tokens in email layouts at render time. Cost: 5¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"tagline":{"type":"string","maxLength":200},"voice":{"type":"string"},"tone":{"type":"string"},"audience":{"type":"string","maxLength":500},"founderName":{"type":"string"},"founderTitle":{"type":"string"},"sender":{"type":"object","properties":{"fromName":{"type":"string","maxLength":120},"fromAddress":{"type":"string","format":"email"},"replyTo":{"type":"string","format":"email"}}},"emailLayoutHtml":{"type":"string","description":"Optional. Must contain {{content}}."},"emailLayoutName":{"type":"string","default":"default"}},"additionalProperties":true}}}},"responses":{"200":{"description":"{ brandProfile, layoutId, applied: {...} }"},"400":{"description":"Validation error (invalid email, missing {{content}}, etc.)"}}}},"/contacts/bulk-tag":{"post":{"operationId":"bulkTagContacts","summary":"Add or remove tags on up to 1000 contacts in one call","description":"Resolve targets via contactIds OR emails. Re-tagging the same pair is a no-op (skipDuplicates against the unique index). mode=\"remove\" deletes matching tag rows. Cost: 5¢ flat per call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"contactIds":{"type":"array","items":{"type":"string"}},"emails":{"type":"array","items":{"type":"string","format":"email"}},"tags":{"type":"array","items":{"type":"string"},"maxItems":50},"mode":{"type":"string","enum":["add","remove"],"default":"add"}}}}}},"responses":{"200":{"description":"{ matched, tagsAdded, tagsRemoved, mode }"},"400":{"description":"Validation error (no targets, too many targets, too many tags)"}}}},"/contacts/bulk-update-properties":{"post":{"operationId":"bulkUpdateContactProperties","summary":"Merge or replace metadata on up to 1000 contacts in one call","description":"mode=\"merge\" (default) does shallow top-level merge; mode=\"replace\" overwrites. Resolve targets via contactIds OR emails. Cost: 5¢ flat per call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["properties"],"properties":{"contactIds":{"type":"array","items":{"type":"string"}},"emails":{"type":"array","items":{"type":"string","format":"email"}},"properties":{"type":"object","additionalProperties":true},"mode":{"type":"string","enum":["merge","replace"],"default":"merge"}}}}}},"responses":{"200":{"description":"{ matched, updated, mode }"},"400":{"description":"Validation error"}}}},"/preview":{"post":{"operationId":"createPreviewUrl","summary":"Mint a signed preview URL (iframe-embeddable)","description":"Returns { url, token, expiresAt } for a short-lived HMAC-signed URL that renders the resource as iframe-ready HTML. Cross-origin embedding gated by the tenant's `previewFrameAncestors` (Content-Security-Policy: frame-ancestors). No API key needed in the iframe markup — the token authenticates. Default TTL 24h, max 7d.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","id"],"properties":{"kind":{"type":"string","enum":["article","landing-page","email-template"]},"id":{"type":"string"},"ttlSeconds":{"type":"integer","minimum":60,"maximum":604800}}}}}},"responses":{"201":{"description":"{ url, token, expiresAt, expiresAtIso, kind, id }"},"400":{"description":"Validation error"},"404":{"description":"Resource not found in this tenant"}}}},"/marketing/campaigns":{"get":{"operationId":"listMarketingCampaigns","summary":"List marketing campaigns","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Paginated campaigns"}}},"post":{"operationId":"generateMarketingCampaign","summary":"Generate an end-to-end ad campaign (strategy + variants + landing page)","description":"Async. Returns immediately with a job ID; poll GET /marketing/campaigns/{id} until status=\"READY\". Provide destinationUrl (ad funnels there) OR productOffer (landing page IS the sales page). If neither, response will include clarifying questions. Cost: 150¢.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","maxLength":2000},"destinationUrl":{"type":"string","format":"uri"},"productOffer":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","description":"course | coaching | product | service | subscription"},"priceCents":{"type":"integer"},"description":{"type":"string"}}},"conversionCta":{"type":"string","maxLength":80}}}}}},"responses":{"202":{"description":"Job enqueued (status RUNNING) or clarifying questions returned (status NEEDS_INPUT)"},"400":{"description":"Validation error"}}}},"/marketing/campaigns/{id}":{"get":{"operationId":"getMarketingCampaign","summary":"Get a marketing campaign with strategy, variants, and landing page","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign details (variants include imageUrl, destinationUrl with UTMs baked in)"},"404":{"description":"Not found"}}}},"/marketing/campaigns/{id}/answer":{"post":{"operationId":"answerMarketingCampaign","summary":"Submit clarifying-question answers and kick off generation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["answers"],"properties":{"answers":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"responses":{"202":{"description":"Job enqueued"},"409":{"description":"Campaign already past NEEDS_INPUT"}}}},"/marketing/campaigns/{id}/analytics":{"get":{"operationId":"marketingCampaignAnalytics","summary":"Funnel rollup for a marketing campaign","description":"Returns total + per-variant counts for page_view, engage, cta_click, conversion, plus CTR + conversion rate + revenue. Events are produced by the tracker auto-injected into the generated landing page.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Funnel rollup"},"404":{"description":"Not found"}}}},"/pay":{"post":{"operationId":"payWithUsdc","summary":"Top up credits via x402 USDC payment on Base","description":"Submit an on-chain USDC payment proof (Base mainnet) to credit the tenant's balance. Pairs with the 402 response returned when credits are depleted — that response includes the receive wallet and required amount in its `error.details.x402` block. 1 USDC = 100 cents. Idempotent: the same txHash returns 409 on retry.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["txHash"],"properties":{"txHash":{"type":"string","pattern":"^0x[a-fA-F0-9]{64}$","description":"Base mainnet transaction hash for the USDC transfer to PLGD_WALLET_ADDRESS"}}}}}},"responses":{"200":{"description":"Payment verified; credit balance incremented"},"400":{"description":"Validation error (bad txHash, tx not found, not a USDC transfer to our wallet, too few confirmations)"},"409":{"description":"Duplicate — this txHash was already used to top up this tenant"}}}},"/content/audit":{"post":{"operationId":"auditContentQuality","summary":"Editorial quality audit","description":"Distinct from /seo/audit (technical SEO). Scores clarity, hook, structure, completeness via Sonnet. Omit articleId to audit the 10 most recent published articles in parallel. Requires articles:read scope. Cost: 20¢.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"articleId":{"type":"string","description":"Omit to audit the 10 most recent published articles"}}}}}},"responses":{"200":{"description":"Per-article qualityScore + issues with suggestions"},"404":{"description":"Article not found"}}}}},"x-partner":{"/partner/tenants":{"get":{"operationId":"listPartnerTenants","summary":"List partner's tenants","description":"Auth: X-Partner-Key. Paginated.","security":[],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"Paginated tenants"},"401":{"description":"Missing/invalid partner key"}}},"post":{"operationId":"provisionTenant","summary":"Provision a new tenant under the partner","description":"Auth: X-Partner-Key. Returns tenant + first agent key (raw key ONCE). Partner-provisioned tenants default to trust tier 1, dailyCampaignSendsCap 50, dailyRecipientsCap 100,000. Hostname: `{slug}.{partner.defaultHostnameApex}`.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":3,"maxLength":100},"slug":{"type":"string","description":"Auto-derived from name if omitted"},"ownerEmail":{"type":"string","format":"email"},"externalId":{"type":"string","description":"Your stable user id; echoed in webhooks"},"mode":{"type":"string","enum":["full","blog","email","minimal"],"default":"full"},"scopes":{"type":"array","items":{"type":"string"},"description":"Overrides partner.defaultScopes"}}}}}},"responses":{"201":{"description":"Tenant + agent key"},"400":{"description":"Validation error"},"409":{"description":"Slug/hostname conflict"}}}},"/partner/tenants/{id}":{"get":{"operationId":"getPartnerTenant","summary":"Read tenant + usage summary","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tenant detail"},"404":{"description":"Not found under this partner"}}},"patch":{"operationId":"updatePartnerTenant","summary":"Update tenant fields","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"contactEmail":{"type":"string","format":"email"},"trustTier":{"type":"integer","enum":[0,1,2,3]},"dailyCampaignSendsCap":{"type":"integer","minimum":1,"maximum":1000},"dailyRecipientsCap":{"type":"integer","minimum":1,"maximum":1000000}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"operationId":"deletePartnerTenant","summary":"Soft-delete (keys deactivated, hostnames cleared)","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deactivated"},"404":{"description":"Not found"}}}},"/partner/tenants/{id}/agent-keys":{"get":{"operationId":"listTenantAgentKeys","summary":"List a tenant's agent keys","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Keys (no raw values)"}}},"post":{"operationId":"mintTenantAgentKey","summary":"Mint additional agent key for a tenant","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"testMode":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"}}}}}},"responses":{"201":{"description":"Raw key returned ONCE"}}}},"/partner/tenants/{id}/agent-keys/{keyId}":{"delete":{"operationId":"deactivateTenantAgentKey","summary":"Deactivate a key (isActive=false)","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deactivated"}}}},"/partner/tenants/{id}/agent-keys/{keyId}/rotate":{"post":{"operationId":"rotateTenantAgentKey","summary":"Rotate key material — returns new raw key ONCE","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rotated"}}}},"/partner/webhooks":{"get":{"operationId":"getPartnerWebhook","summary":"Read current partner webhook config","security":[],"responses":{"200":{"description":"{ configured, url, events }"}}},"put":{"operationId":"setPartnerWebhook","summary":"Set/update partner webhook (mints/rotates secret)","description":"Single URL receives events across all partner tenants. Payload envelope has `data.tenantId`. Signed with X-Plgd-Signature: sha256=<hmac>. Secret returned ONCE on first PUT (or when rotateSecret: true).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"https:// only"},"events":{"type":"array","items":{"type":"string","enum":["article:published","article:updated","article:deleted","contact:created","contact:updated","form:submitted","campaign:sent","email:opened","email:clicked","signup:milestone","seo:alert"]}},"rotateSecret":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated. `secret` present only on creation/rotation."}}}},"/partner/usage":{"get":{"operationId":"partnerUsage","summary":"Aggregate usage rollup across all partner tenants","security":[],"parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Default: 30 days ago"},{"name":"groupBy","in":"query","schema":{"type":"string","enum":["tenant","day"]}}],"responses":{"200":{"description":"{ partnerName, creditBalanceCents, tenantsCount, totals, byTenant, byDay }"}}}}},"x-signup":{"/api/signup":{"post":{"operationId":"signup","summary":"Self-service signup (no auth required)","description":"Creates a tenant, agent API key, and OAuth client in one request. Returns everything needed to start using the API immediately.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":3,"maxLength":100,"description":"Site/project name. Used to generate tenant ID and default hostname."},"hostname":{"type":"string","description":"Custom hostname (e.g. \"mysite.plgd.ai\"). Auto-generated from name if omitted."},"scopes":{"type":"array","items":{"type":"string"},"description":"Requested API scopes. Defaults to all scopes."},"timezone":{"type":"string","description":"Timezone (e.g. \"America/New_York\"). Defaults to \"America/Chicago\"."},"mode":{"type":"string","enum":["full","blog","email","minimal"],"default":"full","description":"Controls which default SiteRoutes get pre-provisioned. \"full\" creates /, /about, /blog, /contact. \"blog\" creates / and /blog. \"email\" creates only /. \"minimal\" creates no routes."},"email":{"type":"string","format":"email","description":"Contact email. Becomes OWNER only when matched by an authenticated NextAuth session (anonymous callers cannot squat ownership)."},"ownerName":{"type":"string","maxLength":100}}}}}},"responses":{"201":{"description":"Tenant created with API key and OAuth credentials"},"400":{"description":"Validation error"},"409":{"description":"Duplicate tenant name or hostname"},"429":{"description":"Rate limited (max 5 signups per hour per IP)"}}}}}}