{
  "openapi": "3.1.0",
  "info": {
    "title": "WorkspaceCMS Tenant API",
    "version": "1.2.0",
    "termsOfService": "https://app.1digital.ai/legal/billing-terms#developer-api",
    "license": {
      "name": "Proprietary — © 2026 1Digital Agency",
      "url": "https://app.1digital.ai/legal/billing-terms#developer-api"
    },
    "description": "Read-only REST API for accessing CMS content (pages, blog posts, assets, SEO settings, and redirects) for any tenant. Designed for headless frontends, BigCommerce/Shopify integrations, and third-party tools.\n\n## Plan eligibility\nThe Developer API is included with the **Premium** plan. Lower-tier accounts may view this spec but cannot generate or use API keys. Visit the dashboard's billing page to upgrade.\n\n## Rate limits\nEvery successful response (and every 429) carries six standard headers so SDKs can throttle automatically:\n- `X-RateLimit-Limit-Minute` — 600 requests per minute per key\n- `X-RateLimit-Remaining-Minute` — requests remaining in the current minute window\n- `X-RateLimit-Reset-Minute` — epoch seconds when the minute window resets\n- `X-RateLimit-Limit-Day` — 50,000 requests per day per tenant\n- `X-RateLimit-Remaining-Day` — requests remaining today\n- `X-RateLimit-Reset-Day` — epoch seconds when the day window resets (00:00 UTC)\n\nWhen limits are exceeded the response is `429 Too Many Requests` with `Retry-After` set to the seconds until the relevant window resets. Need higher limits? Contact us for an Enterprise quote.\n\n## Key management\n- Up to 5 active keys per tenant\n- Keys are issued in `wcms_live_*` format and shown ONCE — store them securely\n- Keys can be rotated with a 7-day grace period on the old key\n- Downgrading from Premium revokes Developer API access immediately; keys persist for 90 days in case of re-upgrade",
    "contact": {
      "name": "1Digital Agency",
      "url": "https://app.1digital.ai"
    }
  },
  "servers": [
    {
      "url": "https://app.1digital.ai",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "wcms_live_<32 hex chars>",
        "description": "API key obtained from the 1Digital dashboard. Keys are tenant-scoped — a key for Tenant A cannot access Tenant B's data."
      }
    },
    "parameters": {
      "tenantSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "The tenant's URL-safe slug (e.g. `my-company`). Found in the dashboard URL at `app.1digital.ai/t/{slug}/...`.",
        "schema": {
          "type": "string",
          "example": "my-tenant"
        }
      },
      "pageParam": {
        "name": "page",
        "in": "query",
        "description": "Page number for pagination (1-indexed).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1,
          "example": 1
        }
      },
      "perPageParam": {
        "name": "per_page",
        "in": "query",
        "description": "Number of results per page. Maximum 100.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20,
          "example": 20
        }
      },
      "fieldsParam": {
        "name": "fields",
        "in": "query",
        "description": "Comma-separated list of fields to include in each object. Omit to return all fields. Example: `slug,title,updated_at`.",
        "schema": {
          "type": "string",
          "example": "slug,title,updated_at"
        }
      }
    },
    "schemas": {
      "Meta": {
        "type": "object",
        "description": "Pagination metadata included on all list responses.",
        "required": ["page", "per_page", "total"],
        "properties": {
          "page": {
            "type": "integer",
            "description": "Current page number.",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "description": "Number of results per page.",
            "example": 20
          },
          "total": {
            "type": "integer",
            "description": "Total number of matching records across all pages.",
            "example": 142
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error", "message"],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unauthorized",
              "tier_downgraded",
              "tier_required",
              "too_many_keys",
              "not_found",
              "rate_limited",
              "internal_error"
            ],
            "description": "Machine-readable error code.",
            "example": "unauthorized"
          },
          "message": {
            "type": "string",
            "description": "Human-readable description of the error.",
            "example": "Invalid or revoked API key"
          },
          "scope": {
            "type": "string",
            "enum": ["minute", "day"],
            "description": "Present on `rate_limited` errors. Indicates which limit window was exceeded."
          },
          "limit": {
            "type": "integer",
            "description": "Present on `rate_limited` errors. The limit that was hit."
          },
          "retry_after_seconds": {
            "type": "integer",
            "description": "Present on `rate_limited` errors. Seconds until the affected window resets — matches the `Retry-After` header.",
            "example": 23
          },
          "enterprise_quote_url": {
            "type": "string",
            "format": "uri",
            "description": "Present on `rate_limited` errors. Link to request higher limits.",
            "example": "https://app.1digital.ai/contact?topic=api-enterprise"
          },
          "upgrade_url": {
            "type": "string",
            "description": "Present on `tier_required` and `tier_downgraded` errors. Path to the tenant's billing page with the Developer API row highlighted.",
            "example": "/t/my-tenant/billing?highlight=developer-api"
          },
          "required_tier": {
            "type": "string",
            "description": "Present on `tier_required` errors. The internal plan slug required for access.",
            "example": "white_glove"
          },
          "current_tier": {
            "type": "string",
            "nullable": true,
            "description": "Present on `tier_required` errors. The caller tenant's current plan slug.",
            "example": "essentials"
          },
          "max_active_keys": {
            "type": "integer",
            "description": "Present on `too_many_keys` errors. The active-key cap per tenant.",
            "example": 5
          }
        }
      },
      "Page": {
        "type": "object",
        "description": "A published CMS page.",
        "required": ["id", "slug", "title", "status", "created_at", "updated_at"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier.",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "slug": {
            "type": "string",
            "description": "URL-safe slug used to identify the page.",
            "example": "about-us"
          },
          "title": {
            "type": "string",
            "description": "Page title.",
            "example": "About Us"
          },
          "body": {
            "type": "string",
            "nullable": true,
            "description": "HTML body content of the page.",
            "example": "<p>We help businesses grow online.</p>"
          },
          "layout": {
            "type": "string",
            "nullable": true,
            "description": "Layout identifier used to render the page.",
            "example": "default"
          },
          "blocks": {
            "type": "object",
            "nullable": true,
            "description": "Structured block content (only included on single-item responses, not list responses).",
            "additionalProperties": true
          },
          "seo_title": {
            "type": "string",
            "nullable": true,
            "description": "Meta title for SEO (`<title>` tag). Falls back to `title` if absent.",
            "example": "About Us | Acme Co"
          },
          "seo_description": {
            "type": "string",
            "nullable": true,
            "description": "Meta description for SEO.",
            "example": "Learn more about Acme Co and our mission."
          },
          "og_description": {
            "type": "string",
            "nullable": true,
            "description": "Open Graph description for social sharing.",
            "example": "We help businesses grow online."
          },
          "og_image_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Open Graph image URL for social sharing.",
            "example": "https://cdn.example.com/og-about.jpg"
          },
          "canonical_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Canonical URL override for SEO. Uses the page's own URL if absent.",
            "example": "https://www.acmeco.com/about-us"
          },
          "robots": {
            "type": "string",
            "nullable": true,
            "description": "Robots meta tag value.",
            "example": "index, follow"
          },
          "status": {
            "type": "string",
            "enum": ["published"],
            "description": "Publication status. Only `published` pages are returned by the API.",
            "example": "published"
          },
          "published_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "ISO 8601 timestamp when the page was published.",
            "example": "2026-06-01T12:00:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the page was created.",
            "example": "2026-05-15T09:30:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the page was last updated.",
            "example": "2026-06-23T14:00:00Z"
          }
        }
      },
      "Post": {
        "type": "object",
        "description": "A published blog post.",
        "required": ["id", "slug", "title", "status", "created_at", "updated_at"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier.",
            "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
          },
          "slug": {
            "type": "string",
            "description": "URL-safe slug used to identify the post.",
            "example": "how-to-grow-your-business"
          },
          "title": {
            "type": "string",
            "description": "Post title.",
            "example": "How to Grow Your Business in 2026"
          },
          "body": {
            "type": "string",
            "nullable": true,
            "description": "HTML body content of the post.",
            "example": "<p>Growing a business requires...</p>"
          },
          "excerpt": {
            "type": "string",
            "nullable": true,
            "description": "Short summary of the post, suitable for listing views.",
            "example": "A practical guide to scaling your business sustainably."
          },
          "author_name": {
            "type": "string",
            "nullable": true,
            "description": "Display name of the post's author.",
            "example": "Jane Smith"
          },
          "featured_image_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "URL of the post's featured/hero image.",
            "example": "https://cdn.example.com/posts/grow-business.jpg"
          },
          "featured": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the post is marked as featured.",
            "example": true
          },
          "meta": {
            "type": "object",
            "nullable": true,
            "description": "Arbitrary metadata object (only included on single-item responses).",
            "additionalProperties": true
          },
          "seo_title": {
            "type": "string",
            "nullable": true,
            "description": "Meta title for SEO.",
            "example": "How to Grow Your Business in 2026 | Acme Blog"
          },
          "seo_description": {
            "type": "string",
            "nullable": true,
            "description": "Meta description for SEO.",
            "example": "Practical tips for growing your business sustainably in 2026."
          },
          "canonical_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Canonical URL override for SEO.",
            "example": "https://www.acmeco.com/blog/how-to-grow-your-business"
          },
          "robots": {
            "type": "string",
            "nullable": true,
            "description": "Robots meta tag value.",
            "example": "index, follow"
          },
          "status": {
            "type": "string",
            "enum": ["published"],
            "description": "Publication status. Only `published` posts are returned by the API.",
            "example": "published"
          },
          "scheduled_for": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "ISO 8601 timestamp when the post is scheduled to publish (may be null for immediately-published posts).",
            "example": null
          },
          "published_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "ISO 8601 timestamp when the post was published.",
            "example": "2026-06-10T08:00:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the post was created.",
            "example": "2026-06-05T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the post was last updated.",
            "example": "2026-06-23T14:00:00Z"
          }
        }
      },
      "Asset": {
        "type": "object",
        "description": "A media or file asset uploaded by the tenant.",
        "required": ["id", "storage_path", "public_url", "mime_type", "size_bytes", "created_at"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "storage_path": {
            "type": "string",
            "description": "Internal storage path of the asset (relative to tenant's storage root).",
            "example": "images/hero-banner.jpg"
          },
          "public_url": {
            "type": "string",
            "format": "uri",
            "description": "Publicly accessible URL for the asset.",
            "example": "https://cdn.1digital.ai/assets/my-tenant/images/hero-banner.jpg"
          },
          "mime_type": {
            "type": "string",
            "description": "MIME type of the asset.",
            "example": "image/jpeg"
          },
          "size_bytes": {
            "type": "integer",
            "description": "File size in bytes.",
            "example": 204800
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the asset was uploaded.",
            "example": "2026-06-01T10:00:00Z"
          }
        }
      },
      "SeoSettings": {
        "type": "object",
        "description": "Global SEO settings for the tenant. Keys and values are tenant-defined; common keys are listed below.",
        "additionalProperties": true,
        "properties": {
          "default_title": {
            "type": "string",
            "nullable": true,
            "description": "Default `<title>` tag used when a page has no `seo_title`.",
            "example": "Acme Co — Digital Marketing Agency"
          },
          "default_description": {
            "type": "string",
            "nullable": true,
            "description": "Default meta description used when a page has no `seo_description`.",
            "example": "Acme Co helps businesses grow online."
          },
          "default_og_image": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Default Open Graph image URL.",
            "example": "https://cdn.1digital.ai/assets/my-tenant/og-default.jpg"
          },
          "robots": {
            "type": "string",
            "nullable": true,
            "description": "Global robots directive.",
            "example": "index, follow"
          },
          "google_analytics_id": {
            "type": "string",
            "nullable": true,
            "description": "Google Analytics measurement ID.",
            "example": "G-XXXXXXXXXX"
          },
          "google_tag_manager_id": {
            "type": "string",
            "nullable": true,
            "description": "Google Tag Manager container ID.",
            "example": "GTM-XXXXXXX"
          }
        }
      },
      "Redirect": {
        "type": "object",
        "description": "A URL redirect rule.",
        "required": ["id", "from_path", "to_path", "status_code", "created_at"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier.",
            "example": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
          },
          "from_path": {
            "type": "string",
            "description": "Source path (relative, starting with `/`).",
            "example": "/old-page"
          },
          "to_path": {
            "type": "string",
            "description": "Destination path or full URL.",
            "example": "/new-page"
          },
          "status_code": {
            "type": "integer",
            "enum": [301, 302, 307, 308],
            "description": "HTTP redirect status code.",
            "example": 301
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the redirect was created.",
            "example": "2026-05-20T08:00:00Z"
          }
        }
      },
      "PageListResponse": {
        "type": "object",
        "required": ["data", "meta"],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Page"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "PageItemResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "PostListResponse": {
        "type": "object",
        "required": ["data", "meta"],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "PostItemResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Post"
          }
        }
      },
      "AssetListResponse": {
        "type": "object",
        "required": ["data", "meta"],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Asset"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "SeoItemResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SeoSettings"
          }
        }
      },
      "RedirectListResponse": {
        "type": "object",
        "required": ["data", "meta"],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Redirect"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "unauthorized",
              "message": "Invalid or revoked API key"
            }
          }
        }
      },
      "NotFound": {
        "description": "The requested tenant slug or resource slug does not exist, or the item is not published.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "not_found",
              "message": "Page not found"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The tenant's plan does not include Developer API access (or access was lost via downgrade). Body indicates whether the key is unusable until billing is restored (`tier_downgraded`) or whether the tenant must upgrade to Premium first (`tier_required`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "tier_downgraded": {
                "summary": "Tenant downgraded from Premium",
                "value": {
                  "error": "tier_downgraded",
                  "message": "Developer API requires Premium. Contact billing to restore access.",
                  "upgrade_url": "/t/my-tenant/billing?highlight=developer-api"
                }
              },
              "tier_required": {
                "summary": "Tenant has never had Premium",
                "value": {
                  "error": "tier_required",
                  "message": "Developer API requires Premium.",
                  "required_tier": "white_glove",
                  "current_tier": "essentials",
                  "upgrade_url": "/t/my-tenant/billing?highlight=developer-api"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key (600 req/min) or per-tenant (50,000 req/day) rate limit was exceeded. Inspect `scope` and `Retry-After` to choose a backoff.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the affected window resets.",
            "schema": { "type": "integer", "example": 23 }
          },
          "X-RateLimit-Limit-Minute": {
            "description": "Per-key minute limit (600).",
            "schema": { "type": "integer", "example": 600 }
          },
          "X-RateLimit-Remaining-Minute": {
            "description": "Requests remaining in the current minute window.",
            "schema": { "type": "integer", "example": 0 }
          },
          "X-RateLimit-Reset-Minute": {
            "description": "Epoch seconds when the minute window resets.",
            "schema": { "type": "integer", "example": 1719331260 }
          },
          "X-RateLimit-Limit-Day": {
            "description": "Per-tenant daily limit (50,000).",
            "schema": { "type": "integer", "example": 50000 }
          },
          "X-RateLimit-Remaining-Day": {
            "description": "Requests remaining today.",
            "schema": { "type": "integer", "example": 0 }
          },
          "X-RateLimit-Reset-Day": {
            "description": "Epoch seconds when the day window resets (00:00 UTC).",
            "schema": { "type": "integer", "example": 1719360000 }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "rate_limited",
              "scope": "minute",
              "limit": 600,
              "retry_after_seconds": 23,
              "message": "Rate limit exceeded. Retry in 23s.",
              "enterprise_quote_url": "https://app.1digital.ai/contact?topic=api-enterprise"
            }
          }
        }
      },
      "InternalError": {
        "description": "An unexpected server-side error occurred.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "internal_error",
              "message": "Failed to fetch pages"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/{slug}/pages": {
      "get": {
        "operationId": "listPages",
        "summary": "List published pages",
        "description": "Returns all published CMS pages for the tenant, ordered by creation date (newest first). Drafts are never included.",
        "tags": ["Pages"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          { "$ref": "#/components/parameters/pageParam" },
          { "$ref": "#/components/parameters/perPageParam" },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of published pages.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PageListResponse" },
                "example": {
                  "data": [
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "slug": "about-us",
                      "title": "About Us",
                      "body": "<p>We help businesses grow online.</p>",
                      "layout": "default",
                      "seo_title": "About Us | Acme Co",
                      "seo_description": "Learn more about Acme Co and our mission.",
                      "og_description": null,
                      "og_image_url": null,
                      "canonical_url": null,
                      "robots": "index, follow",
                      "status": "published",
                      "published_at": "2026-06-01T12:00:00Z",
                      "created_at": "2026-05-15T09:30:00Z",
                      "updated_at": "2026-06-23T14:00:00Z"
                    }
                  ],
                  "meta": { "page": 1, "per_page": 20, "total": 12 }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/pages/{pageSlug}": {
      "get": {
        "operationId": "getPage",
        "summary": "Get a single page by slug",
        "description": "Returns a single published CMS page. Returns 404 if the page does not exist or is not published. The single-item response includes the `blocks` field (structured block content) which is omitted from list responses.",
        "tags": ["Pages"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          {
            "name": "pageSlug",
            "in": "path",
            "required": true,
            "description": "The page's URL-safe slug (e.g. `about-us`).",
            "schema": {
              "type": "string",
              "example": "about-us"
            }
          },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "The requested published page.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PageItemResponse" },
                "example": {
                  "data": {
                    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "about-us",
                    "title": "About Us",
                    "body": "<p>We help businesses grow online.</p>",
                    "layout": "default",
                    "blocks": null,
                    "seo_title": "About Us | Acme Co",
                    "seo_description": "Learn more about Acme Co and our mission.",
                    "og_description": null,
                    "og_image_url": null,
                    "canonical_url": null,
                    "robots": "index, follow",
                    "status": "published",
                    "published_at": "2026-06-01T12:00:00Z",
                    "created_at": "2026-05-15T09:30:00Z",
                    "updated_at": "2026-06-23T14:00:00Z"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/posts": {
      "get": {
        "operationId": "listPosts",
        "summary": "List published blog posts",
        "description": "Returns all published blog posts for the tenant, ordered by `published_at` date (newest first). Drafts and scheduled posts are never included.",
        "tags": ["Posts"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          { "$ref": "#/components/parameters/pageParam" },
          { "$ref": "#/components/parameters/perPageParam" },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of published blog posts.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PostListResponse" },
                "example": {
                  "data": [
                    {
                      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
                      "slug": "how-to-grow-your-business",
                      "title": "How to Grow Your Business in 2026",
                      "body": "<p>Growing a business requires...</p>",
                      "excerpt": "A practical guide to scaling your business sustainably.",
                      "author_name": "Jane Smith",
                      "featured_image_url": "https://cdn.example.com/posts/grow-business.jpg",
                      "featured": true,
                      "seo_title": "How to Grow Your Business in 2026 | Acme Blog",
                      "seo_description": "Practical tips for growing your business sustainably in 2026.",
                      "canonical_url": null,
                      "robots": "index, follow",
                      "status": "published",
                      "scheduled_for": null,
                      "published_at": "2026-06-10T08:00:00Z",
                      "created_at": "2026-06-05T10:00:00Z",
                      "updated_at": "2026-06-23T14:00:00Z"
                    }
                  ],
                  "meta": { "page": 1, "per_page": 20, "total": 47 }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/posts/{postSlug}": {
      "get": {
        "operationId": "getPost",
        "summary": "Get a single blog post by slug",
        "description": "Returns a single published blog post. Returns 404 if the post does not exist or is not published. The single-item response includes the `meta` field (arbitrary metadata object) which is omitted from list responses.",
        "tags": ["Posts"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          {
            "name": "postSlug",
            "in": "path",
            "required": true,
            "description": "The post's URL-safe slug (e.g. `how-to-grow-your-business`).",
            "schema": {
              "type": "string",
              "example": "how-to-grow-your-business"
            }
          },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "The requested published blog post.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PostItemResponse" },
                "example": {
                  "data": {
                    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
                    "slug": "how-to-grow-your-business",
                    "title": "How to Grow Your Business in 2026",
                    "body": "<p>Growing a business requires...</p>",
                    "excerpt": "A practical guide to scaling your business sustainably.",
                    "author_name": "Jane Smith",
                    "featured_image_url": "https://cdn.example.com/posts/grow-business.jpg",
                    "featured": true,
                    "meta": null,
                    "seo_title": "How to Grow Your Business in 2026 | Acme Blog",
                    "seo_description": "Practical tips for growing your business sustainably in 2026.",
                    "canonical_url": null,
                    "robots": "index, follow",
                    "status": "published",
                    "scheduled_for": null,
                    "published_at": "2026-06-10T08:00:00Z",
                    "created_at": "2026-06-05T10:00:00Z",
                    "updated_at": "2026-06-23T14:00:00Z"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/assets": {
      "get": {
        "operationId": "listAssets",
        "summary": "List assets",
        "description": "Returns all media and file assets for the tenant, ordered by upload date (newest first).",
        "tags": ["Assets"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          { "$ref": "#/components/parameters/pageParam" },
          { "$ref": "#/components/parameters/perPageParam" },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of assets.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AssetListResponse" },
                "example": {
                  "data": [
                    {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "storage_path": "images/hero-banner.jpg",
                      "public_url": "https://cdn.1digital.ai/assets/my-tenant/images/hero-banner.jpg",
                      "mime_type": "image/jpeg",
                      "size_bytes": 204800,
                      "created_at": "2026-06-01T10:00:00Z"
                    }
                  ],
                  "meta": { "page": 1, "per_page": 20, "total": 83 }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/seo": {
      "get": {
        "operationId": "getSeoSettings",
        "summary": "Get SEO settings",
        "description": "Returns the global SEO settings object for the tenant. The response is a key-value map where keys are setting names (e.g. `default_title`, `default_description`, `google_analytics_id`). Returns an empty object if no SEO settings have been configured.",
        "tags": ["SEO"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" }
        ],
        "responses": {
          "200": {
            "description": "The tenant's SEO settings.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoItemResponse" },
                "example": {
                  "data": {
                    "default_title": "Acme Co — Digital Marketing Agency",
                    "default_description": "Acme Co helps businesses grow online.",
                    "default_og_image": "https://cdn.1digital.ai/assets/my-tenant/og-default.jpg",
                    "robots": "index, follow",
                    "google_analytics_id": "G-XXXXXXXXXX",
                    "google_tag_manager_id": "GTM-XXXXXXX"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/redirects": {
      "get": {
        "operationId": "listRedirects",
        "summary": "List URL redirects",
        "description": "Returns all URL redirect rules for the tenant, ordered by creation date (newest first). Use these to keep your headless frontend's redirect config in sync with the CMS.",
        "tags": ["Redirects"],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" },
          { "$ref": "#/components/parameters/pageParam" },
          { "$ref": "#/components/parameters/perPageParam" },
          { "$ref": "#/components/parameters/fieldsParam" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of redirect rules.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RedirectListResponse" },
                "example": {
                  "data": [
                    {
                      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
                      "from_path": "/old-page",
                      "to_path": "/new-page",
                      "status_code": 301,
                      "created_at": "2026-05-20T08:00:00Z"
                    }
                  ],
                  "meta": { "page": 1, "per_page": 20, "total": 15 }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/{slug}/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "Get this OpenAPI spec",
        "description": "Returns this OpenAPI 3.1 specification document. No authentication required — the spec is public documentation.",
        "tags": ["Meta"],
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/tenantSlug" }
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 specification for the WorkspaceCMS Tenant API.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "OpenAPI 3.1 document."
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Pages",
      "description": "Published CMS pages."
    },
    {
      "name": "Posts",
      "description": "Published blog posts."
    },
    {
      "name": "Assets",
      "description": "Media and file assets."
    },
    {
      "name": "SEO",
      "description": "Global SEO settings."
    },
    {
      "name": "Redirects",
      "description": "URL redirect rules."
    },
    {
      "name": "Meta",
      "description": "API documentation and metadata."
    }
  ]
}
