{
  "openapi": "3.1.1",
  "info": {
    "title": "Coral Public API",
    "version": "v1.48.03",
    "summary": "Coral's public, unauthenticated read-only endpoints.",
    "description": "Coral helps Amazon brands run affiliate programs for TikTok and Instagram creators.\n\nThis document describes only the endpoints that are public by design: no API key, no session cookie, no rate-limit registration. Everything documented here is read-only and safe to cache.\n\nEndpoints NOT described here — the authenticated dashboard API, webhook receivers and internal proxies — are not part of any contract and may change without notice.\n\nErrors use the envelope in `ErrorResponse`: a top-level `message` for humans plus a structured `error` object with a stable `code`. The one exception is `/api/blog/*`, which predates the envelope and returns `LegacyBlogError` instead.",
    "contact": {
      "name": "Coral Support",
      "email": "support@coral.ax",
      "url": "https://coral.ax/docs"
    }
  },
  "servers": [
    {
      "url": "https://coral.ax",
      "description": "The origin this document was fetched from."
    }
  ],
  "tags": [
    {
      "name": "System",
      "description": "Liveness and build information."
    },
    {
      "name": "Blog",
      "description": "Published blog content. Uses the legacy error shape."
    },
    {
      "name": "Catalog",
      "description": "Products shown on public affiliate program pages."
    },
    {
      "name": "Plans",
      "description": "Publicly marketed subscription pricing."
    },
    {
      "name": "Brands",
      "description": "Public brand profiles."
    },
    {
      "name": "Affiliate Programs",
      "description": "Affiliate programs and their public invite links."
    },
    {
      "name": "Landing Pages",
      "description": "Brand-authored landing pages served to shoppers."
    },
    {
      "name": "Referrals",
      "description": "Creator referral codes."
    },
    {
      "name": "Machine-readable",
      "description": "Machine-readable descriptions of the site itself."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Service health",
        "description": "Reports that the API process is alive, with its deployed version and uptime. Performs no database work, so it stays fast and cannot fail because of a database hiccup. Never cached.",
        "tags": [
          "System"
        ],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/api/version": {
      "get": {
        "operationId": "getVersion",
        "summary": "Deployed version",
        "description": "Returns the deployed application version string, read from version.txt at the repository root. Useful for confirming which build a deployment is currently serving.",
        "tags": [
          "System"
        ],
        "responses": {
          "200": {
            "description": "The current version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Version"
                }
              }
            }
          }
        }
      }
    },
    "/api/blog/posts": {
      "get": {
        "operationId": "listBlogPosts",
        "summary": "List published blog posts",
        "description": "Returns published posts newest first, with pagination metadata. Optionally filtered to one category (case-insensitive). Errors from this endpoint use the legacy `{ error: string }` shape rather than the standard envelope.",
        "tags": [
          "Blog"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Defaults to 1; unparseable values fall back to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Posts per page. Defaults to 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 10
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Restrict to one category, matched case-insensitively.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of posts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "posts",
                    "pagination"
                  ],
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BlogPostSummary"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "currentPage",
                        "totalPages",
                        "totalPosts",
                        "hasNext",
                        "hasPrev"
                      ],
                      "properties": {
                        "currentPage": {
                          "type": "integer",
                          "description": "The page number that was served."
                        },
                        "totalPages": {
                          "type": "integer",
                          "description": "Total number of pages at this page size."
                        },
                        "totalPosts": {
                          "type": "integer",
                          "description": "Total number of published posts matching the filter."
                        },
                        "hasNext": {
                          "type": "boolean",
                          "description": "Whether a further page exists."
                        },
                        "hasPrev": {
                          "type": "boolean",
                          "description": "Whether an earlier page exists."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal error, in the legacy blog error shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyBlogError"
                }
              }
            }
          }
        }
      }
    },
    "/api/blog/posts/{slug}": {
      "get": {
        "operationId": "getBlogPost",
        "summary": "Get one blog post",
        "description": "Returns a single published post by its slug, including the full HTML body. Unpublished posts are treated as missing. Errors use the legacy `{ error: string }` shape.",
        "tags": [
          "Blog"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "URL slug of the post, exactly as returned in a listing.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlogPost"
                }
              }
            }
          },
          "404": {
            "description": "No published post has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyBlogError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error, in the legacy blog error shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyBlogError"
                }
              }
            }
          }
        }
      }
    },
    "/api/blog/categories": {
      "get": {
        "operationId": "listBlogCategories",
        "summary": "List blog categories",
        "description": "Returns the distinct, non-null categories in use across published posts, as a flat array of strings. Each value can be passed back as the `category` filter on the posts listing.",
        "tags": [
          "Blog"
        ],
        "responses": {
          "200": {
            "description": "Distinct category names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Category"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal error, in the legacy blog error shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyBlogError"
                }
              }
            }
          }
        }
      }
    },
    "/public/api/products": {
      "get": {
        "operationId": "listPublicProducts",
        "summary": "Look up products by id",
        "description": "Returns the products with the given ids, for rendering a public affiliate program or landing page. At least one `id` is required — this is a lookup, not a browsable catalogue, so there is no way to enumerate a brand's products through it.",
        "tags": [
          "Catalog"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Product id. Repeat the parameter to fetch several (`?id=1&id=2`). Values that are not integers are ignored; if none remain the request is rejected.",
            "schema": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching products, in unspecified order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Product"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/public/api/plans": {
      "get": {
        "operationId": "listPublicPlans",
        "summary": "List public pricing plans",
        "description": "Returns the plans shown on Coral's pricing page: ordinary per-brand plans in `plans`, and multi-brand bundles (with computed list price and savings) in `bundles`. Inactive plans and the legacy Starter plan are excluded.",
        "tags": [
          "Plans"
        ],
        "responses": {
          "200": {
            "description": "Active plans and bundles.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plans",
                    "bundles"
                  ],
                  "properties": {
                    "plans": {
                      "type": "array",
                      "description": "Per-brand plans, cheapest first.",
                      "items": {
                        "$ref": "#/components/schemas/Plan"
                      }
                    },
                    "bundles": {
                      "type": "array",
                      "description": "Multi-brand bundles, smallest first, each carrying the bundle-only pricing fields.",
                      "items": {
                        "$ref": "#/components/schemas/Plan"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/public/api/brands/{id}": {
      "get": {
        "operationId": "getPublicBrand",
        "summary": "Get a brand's public profile",
        "description": "Returns the small, deliberately curated subset of a brand's record that its public affiliate program page needs: name, logo, website and product category. No contact, billing or Amazon account information is exposed.",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Brand id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The brand's public profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Brand"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/affiliate-programs/public": {
      "get": {
        "operationId": "getPublicAffiliateProgram",
        "summary": "Get an affiliate program by id",
        "description": "Resolves one affiliate program by its numeric id, including its commission tier ladder when the program is in tiered mode. Despite the path, this is a single-resource lookup: the `id` query parameter is required and exactly one program is returned.\n\nIds are sequential and therefore enumerable, so this endpoint only discloses programs their owner has made visible. A program that exists but is hidden returns 200 with `is_visible: false`, `id` and `brand_id` and no other field — enough for an application page to explain the situation, without publishing the program's title, commission rates or contract URL. An archived program returns 410, matching the page-id lookup.",
        "tags": [
          "Affiliate Programs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Affiliate program id. Must parse as an integer.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The affiliate program when it is visible; otherwise the hidden-program stub.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AffiliateProgramSummary"
                    },
                    {
                      "$ref": "#/components/schemas/HiddenAffiliateProgram"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "description": "The program exists but has been archived and no longer accepts applications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "archived"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Human-readable explanation."
                    },
                    "archived": {
                      "type": "boolean",
                      "description": "Always true; distinguishes a retired program from a wrong id."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/affiliate-programs/page/{pageId}": {
      "get": {
        "operationId": "getAffiliateProgramByPageId",
        "summary": "Get an affiliate program by page id",
        "description": "Resolves the affiliate program behind a public invite link (/a/{pageId}), matched case-insensitively. An archived program returns 410 with `archived: true` so the application page can explain that the invite link has been retired rather than showing a bare 404.",
        "tags": [
          "Affiliate Programs"
        ],
        "parameters": [
          {
            "name": "pageId",
            "in": "path",
            "required": true,
            "description": "Public page slug of the invite link, matched case-insensitively.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The affiliate program.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateProgramSummary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "description": "The invite link exists but has been archived and no longer accepts applications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "archived"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Human-readable explanation."
                    },
                    "archived": {
                      "type": "boolean",
                      "description": "Always true; distinguishes a retired link from a wrong one."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/landing-pages/public/{slug}": {
      "get": {
        "operationId": "getPublicLandingPage",
        "summary": "Get a published landing page",
        "description": "Returns a published landing page and its visible sections in render order, matched on slug case-insensitively. Responses carry a weak ETag derived from the page and its sections, so a conditional request with If-None-Match gets a 304.",
        "tags": [
          "Landing Pages"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Landing page slug, matched case-insensitively.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ref",
            "in": "query",
            "required": false,
            "description": "Affiliate link id that referred the visitor. Recorded for attribution; does not change the response body.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The landing page and its sections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LandingPage"
                }
              }
            }
          },
          "304": {
            "description": "The supplied If-None-Match ETag still matches; the body is unchanged."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/referrals/info/{referralCode}": {
      "get": {
        "operationId": "getReferralInfo",
        "summary": "Resolve a referral code",
        "description": "Returns the creator name, brand name and commission rate behind an active referral code, so an invite landing experience can be personalised. Inactive or unknown codes return 404.",
        "tags": [
          "Referrals"
        ],
        "parameters": [
          {
            "name": "referralCode",
            "in": "path",
            "required": true,
            "description": "Referral code from a /r/{code} invite link, matched case-insensitively.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Display information for the referral.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralInfo"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "This OpenAPI document (JSON)",
        "description": "Serves this document as JSON. The `servers` entry is derived from the request's own origin, so a spec fetched from a preview deployment describes that deployment. Cached for one hour. Also available at /api/openapi.json.",
        "tags": [
          "Machine-readable"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document."
                }
              }
            }
          }
        }
      }
    },
    "/api/openapi.yaml": {
      "get": {
        "operationId": "getOpenApiSpecYaml",
        "summary": "This OpenAPI document (YAML)",
        "description": "Serves the same document as YAML, for tools that prefer it. Byte-for-byte equivalent in content to /openapi.json. Cached for one hour.",
        "tags": [
          "Machine-readable"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document as YAML.",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "string",
                  "description": "YAML document."
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "summary": "Site index for AI agents",
        "description": "A plain-text navigation index following the llms.txt convention: the site's key pages, public affiliate programs and blog posts, each linked to a Markdown twin of the HTML page. Intended as the entry point for an agent exploring Coral.",
        "tags": [
          "Machine-readable"
        ],
        "responses": {
          "200": {
            "description": "The llms.txt index.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "Markdown-flavoured plain text."
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "summary": "XML sitemap",
        "description": "The standard XML sitemap listing Coral's public pages, published blog posts and visible affiliate program pages. Also served at /sitemap.",
        "tags": [
          "Machine-readable"
        ],
        "responses": {
          "200": {
            "description": "A urlset sitemap.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string",
                  "description": "XML sitemap (sitemaps.org 0.9 schema)."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "description": "Standard Coral error envelope. Returned by every documented endpoint except `/api/blog/*`, which predates it and returns `LegacyBlogError` instead.\n\nOnly `message` is REQUIRED. The structured `error` object is still being rolled out across handlers (see server/lib/api-error.ts), so a client must tolerate its absence and fall back to `message`. It is listed as required-once-present rather than required so that this document validates against what the server sends today.",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable error text. LEGACY and load-bearing: every existing client reads this top-level field, and it mirrors `error.message` verbatim."
          },
          "error": {
            "type": "object",
            "description": "Structured detail for machine consumers. Added alongside `message`, never replacing it.",
            "required": [
              "code",
              "message",
              "hint",
              "docs",
              "status"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable error code. Branch on this, not on the HTTP status alone.",
                "enum": [
                  "not_found",
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "not_acceptable",
                  "method_not_allowed",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Same text as the top-level `message`."
              },
              "hint": {
                "type": "string",
                "description": "What to change about the request to make it succeed. May be an empty string."
              },
              "docs": {
                "type": "string",
                "description": "Documentation URL explaining this class of error.",
                "format": "uri"
              },
              "status": {
                "type": "integer",
                "description": "HTTP status code, repeated in the body so it survives logging and proxies."
              }
            }
          }
        }
      },
      "LegacyBlogError": {
        "type": "object",
        "description": "Legacy error shape used ONLY by `/api/blog/*`. Here `error` is a plain string, not an object. These endpoints predate the standard envelope and their clients read `error` as text, so the shape is frozen. Do not expect `ErrorResponse` from the blog endpoints.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error text, e.g. \"Post not found\"."
          }
        }
      },
      "Health": {
        "type": "object",
        "description": "Liveness of the API process. Deliberately does NOT probe the database, so a slow or degraded database never turns this endpoint red.",
        "required": [
          "status",
          "version",
          "uptime",
          "timestamp"
        ],
        "properties": {
          "status": {
            "type": "string",
            "description": "Always \"ok\" when the process is answering at all.",
            "enum": [
              "ok"
            ]
          },
          "version": {
            "type": "string",
            "description": "Deployed application version, e.g. \"v1.48.02\"."
          },
          "uptime": {
            "type": "number",
            "description": "Seconds this process has been running."
          },
          "timestamp": {
            "type": "string",
            "description": "Server time when the response was generated.",
            "format": "date-time"
          }
        }
      },
      "Version": {
        "type": "object",
        "description": "Deployed application version, read from version.txt at the repository root.",
        "required": [
          "version"
        ],
        "properties": {
          "version": {
            "type": "string",
            "description": "Version string, e.g. \"v1.48.02\". \"unknown\" if version.txt is unreadable."
          }
        }
      },
      "BlogPost": {
        "type": "object",
        "description": "A published blog post, including its rendered body.",
        "required": [
          "id",
          "title",
          "slug",
          "content",
          "published_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Post id."
          },
          "title": {
            "type": "string",
            "description": "Post title."
          },
          "slug": {
            "type": "string",
            "description": "URL slug; unique. Used as the path segment of /blog/{slug}."
          },
          "content": {
            "type": "string",
            "description": "Post body as HTML."
          },
          "content_markdown": {
            "type": [
              "string",
              "null"
            ],
            "description": "Post body as Markdown, when the source provided one."
          },
          "excerpt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short summary used in listings and meta tags."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Category name, matched case-insensitively by the `category` filter."
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Free-form tags."
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hero image URL."
          },
          "meta_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "SEO title override."
          },
          "meta_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "SEO meta description."
          },
          "keywords": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "SEO keywords."
          },
          "published_at": {
            "type": "string",
            "description": "Publication timestamp.",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "description": "Last modification timestamp.",
            "format": "date-time"
          },
          "is_published": {
            "type": "boolean",
            "description": "Always true on this endpoint; unpublished posts are never returned."
          }
        }
      },
      "BlogPostSummary": {
        "type": "object",
        "description": "The fields of a post that listing consumers should rely on. NOTE: `GET /api/blog/posts` currently also echoes the full body fields of `BlogPost` in each list entry; treat those as incidental — only the properties described here are part of the contract.",
        "required": [
          "id",
          "title",
          "slug",
          "published_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Post id."
          },
          "title": {
            "type": "string",
            "description": "Post title."
          },
          "slug": {
            "type": "string",
            "description": "URL slug; fetch the full post with GET /api/blog/posts/{slug}."
          },
          "excerpt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short summary suitable for a card or list row."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Category name."
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Free-form tags."
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hero image URL."
          },
          "published_at": {
            "type": "string",
            "description": "Publication timestamp.",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "description": "Last modification timestamp.",
            "format": "date-time"
          }
        }
      },
      "Category": {
        "type": "string",
        "description": "A blog category name, exactly as stored on the posts. Pass it back as the `category` query parameter of GET /api/blog/posts (matching is case-insensitive)."
      },
      "Product": {
        "type": "object",
        "description": "A product listed on a public affiliate program page. Only the fields below are part of the public contract; any other property present in a response is an implementation detail that may disappear without notice.",
        "required": [
          "id",
          "asin",
          "title"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Coral product id. Pass these back as the `id` query parameter."
          },
          "asin": {
            "type": "string",
            "description": "Amazon Standard Identification Number."
          },
          "title": {
            "type": "string",
            "description": "Product title."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amazon product category."
          },
          "subcategory": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amazon product subcategory."
          },
          "group": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amazon product group."
          },
          "availability": {
            "type": [
              "string",
              "null"
            ],
            "description": "Amazon availability string, e.g. \"IN_STOCK\"."
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Thumbnail image URL."
          },
          "large_image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full-size image URL."
          },
          "price": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current price as a decimal string, e.g. \"24.99\"."
          },
          "price_currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 currency code for `price`, e.g. \"USD\"."
          },
          "variations_list": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Variation labels (size, colour, ...) when the product has any."
          },
          "commission_perc": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Default commission percentage offered on this product, as shown to creators on the public application page."
          }
        }
      },
      "Plan": {
        "type": "object",
        "description": "A publicly marketed subscription plan. Entries in `bundles` are plans with `bundle_size` > 1 and carry the four additional bundle-pricing properties described below.",
        "required": [
          "id",
          "name",
          "slug",
          "monthly_fixed_fee_cents",
          "commission_rate",
          "bundle_size"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Plan id."
          },
          "name": {
            "type": "string",
            "description": "Display name, e.g. \"Pro\"."
          },
          "slug": {
            "type": "string",
            "description": "Stable machine identifier, e.g. \"pro\"."
          },
          "monthly_fixed_fee_cents": {
            "type": "integer",
            "description": "Monthly subscription fee in cents."
          },
          "yearly_fixed_fee_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total yearly fee in cents, or null when no yearly option exists."
          },
          "commission_rate": {
            "type": "string",
            "description": "Coral's commission percentage as a decimal string, e.g. \"3.00\"."
          },
          "default_trial_days": {
            "type": "integer",
            "description": "Length of the default free trial in days; 0 when there is none."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Marketing description."
          },
          "is_active": {
            "type": "boolean",
            "description": "Always true on this endpoint; inactive plans are filtered out."
          },
          "bundle_size": {
            "type": "integer",
            "description": "Number of brands the plan covers. 1 for ordinary plans, >1 for bundles."
          },
          "base_plan_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For a bundle, the id of the per-brand plan it is built from."
          },
          "list_price_cents": {
            "type": "integer",
            "description": "BUNDLES ONLY. Undiscounted monthly price (bundle_size x the base plan's monthly fee), for rendering a strikethrough price."
          },
          "list_price_yearly_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "BUNDLES ONLY. Monthly-equivalent undiscounted price under yearly billing; null when the base plan has no yearly option."
          },
          "savings_pct": {
            "type": "integer",
            "description": "BUNDLES ONLY. Percentage saved versus `list_price_cents`."
          },
          "base_plan_name": {
            "type": "string",
            "description": "BUNDLES ONLY. Display name of the base plan."
          },
          "base_plan_slug": {
            "type": "string",
            "description": "BUNDLES ONLY. Slug of the base plan."
          }
        }
      },
      "Brand": {
        "type": "object",
        "description": "The public profile of a brand, as shown on its affiliate program page.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Brand id."
          },
          "name": {
            "type": "string",
            "description": "Brand name."
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Brand logo, falling back to the affiliate program's logo when unset."
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "description": "Brand website URL."
          },
          "product_category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Primary product category the brand sells in."
          }
        }
      },
      "HiddenAffiliateProgram": {
        "type": "object",
        "description": "What GET /api/affiliate-programs/public returns for a program that exists but is not visible. Deliberately minimal: enough to tell a creator which brand is not currently accepting applications, and nothing about the program's terms. Branch on `is_visible` being false to tell this apart from a full AffiliateProgramSummary.",
        "required": [
          "id",
          "brand_id",
          "is_visible"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Affiliate program id."
          },
          "brand_id": {
            "type": "integer",
            "description": "Owning brand id; resolve it with GET /public/api/brands/{id}."
          },
          "is_visible": {
            "type": "boolean",
            "description": "Always false in this variant."
          }
        }
      },
      "AffiliateProgramSummary": {
        "type": "object",
        "description": "A brand's affiliate program (\"invite link\") as presented on its public application page. GET /api/affiliate-programs/page/{pageId} returns this shape for hidden programs too, because a page id is an unguessable slug a brand shares directly with creators. GET /api/affiliate-programs/public, addressed by an enumerable integer id, returns it only for visible programs and answers with HiddenAffiliateProgram otherwise.",
        "required": [
          "id",
          "brand_id",
          "title",
          "description",
          "commission_perc",
          "commission_mode"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Affiliate program id."
          },
          "brand_id": {
            "type": "integer",
            "description": "Owning brand id; resolve it with GET /public/api/brands/{id}."
          },
          "title": {
            "type": "string",
            "description": "Program title."
          },
          "description": {
            "type": "string",
            "description": "Program description shown to creators."
          },
          "is_visible": {
            "type": "boolean",
            "description": "Whether the program is listed publicly. Unlisted programs still resolve by page_id."
          },
          "auto_accept_applications": {
            "type": "boolean",
            "description": "Whether creator applications are accepted automatically."
          },
          "deep_links_default_enabled": {
            "type": "boolean",
            "description": "Whether new partnerships get deep links enabled by default."
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Program logo URL."
          },
          "page_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public page slug; the path segment of /a/{page_id}."
          },
          "require_contract_acceptance": {
            "type": "boolean",
            "description": "Whether a creator must sign a contract before joining."
          },
          "custom_contract_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Brand-supplied contract URL, when one replaces the default."
          },
          "commission_perc": {
            "type": "integer",
            "description": "Flat commission percentage applied to all products, when commission_mode is \"flat\"."
          },
          "per_item_commissions_enabled": {
            "type": "boolean",
            "description": "Whether per-product commission overrides are in effect."
          },
          "commission_mode": {
            "type": "string",
            "description": "\"flat\" uses commission_perc; \"tiered\" uses the `tiers` ladder.",
            "enum": [
              "flat",
              "tiered"
            ]
          },
          "tiers_enabled_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When tiered mode was first enabled; the tier counter's start date.",
            "format": "date-time"
          },
          "archived_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Soft-delete timestamp; null for an active program.",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "description": "Creation timestamp.",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "description": "Last modification timestamp.",
            "format": "date-time"
          },
          "tiers": {
            "type": "array",
            "description": "Units-sold commission ladder. Empty unless commission_mode is \"tiered\".",
            "items": {
              "type": "object",
              "description": "One rung of the ladder.",
              "properties": {
                "tier_order": {
                  "type": "integer",
                  "description": "1-based position of this tier in the ladder."
                },
                "threshold_units": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Units that must be sold to reach this tier; null means unbounded."
                },
                "commission_perc": {
                  "type": "integer",
                  "description": "Commission percentage paid while in this tier."
                }
              }
            }
          }
        }
      },
      "LandingPage": {
        "type": "object",
        "description": "A published landing page together with its visible sections, ordered by position. This is the whole response body of GET /api/landing-pages/public/{slug}.",
        "required": [
          "landingPage",
          "sections"
        ],
        "properties": {
          "landingPage": {
            "type": "object",
            "description": "The page itself, joined with a few fields of the product it promotes.",
            "required": [
              "id",
              "slug",
              "title"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "description": "Landing page id."
              },
              "brand_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Owning brand id."
              },
              "product_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Promoted product id, when the page is tied to one product."
              },
              "title": {
                "type": "string",
                "description": "Page title."
              },
              "slug": {
                "type": "string",
                "description": "URL slug; matched case-insensitively."
              },
              "template_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Template the page was built from."
              },
              "is_published": {
                "type": "boolean",
                "description": "Always true on this endpoint; drafts return 404."
              },
              "is_active": {
                "type": "boolean",
                "description": "Whether the page is currently active."
              },
              "meta_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SEO title."
              },
              "meta_description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SEO meta description."
              },
              "global_custom_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Brand-authored markup injected into the rendered page."
              },
              "created_at": {
                "type": "string",
                "description": "Creation timestamp.",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "description": "Last modification timestamp; also drives the ETag.",
                "format": "date-time"
              },
              "product_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Title of the promoted product."
              },
              "product_image_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Thumbnail of the promoted product."
              },
              "product_large_image_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full-size image of the promoted product."
              },
              "product_sku": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SKU of the promoted product."
              },
              "product_asin": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ASIN of the promoted product."
              }
            }
          },
          "sections": {
            "type": "array",
            "description": "Visible sections in render order. Hidden sections are omitted entirely.",
            "items": {
              "type": "object",
              "description": "One section of the page.",
              "required": [
                "id",
                "section_type",
                "position",
                "content"
              ],
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Section id."
                },
                "landing_page_id": {
                  "type": "integer",
                  "description": "Id of the page this section belongs to."
                },
                "section_type": {
                  "type": "string",
                  "description": "Section kind, e.g. \"hero\" or \"email_capture\"."
                },
                "position": {
                  "type": "integer",
                  "description": "0-based render order."
                },
                "content": {
                  "type": "string",
                  "description": "Section payload as a JSON-encoded string."
                },
                "is_visible": {
                  "type": "boolean",
                  "description": "Always true on this endpoint."
                },
                "created_at": {
                  "type": "string",
                  "description": "Creation timestamp.",
                  "format": "date-time"
                },
                "updated_at": {
                  "type": "string",
                  "description": "Last modification timestamp.",
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "ReferralInfo": {
        "type": "object",
        "description": "Display information behind a creator's /r/{code} invite link, used to render a personalised landing experience before the visitor is redirected.",
        "required": [
          "creator_name",
          "referral_commission_rate",
          "brand_name"
        ],
        "properties": {
          "creator_name": {
            "type": "string",
            "description": "Name of the creator who owns the referral code."
          },
          "referral_commission_rate": {
            "type": "integer",
            "description": "Commission percentage attached to this referral."
          },
          "brand_name": {
            "type": "string",
            "description": "Name of the brand being referred."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed — a required parameter was missing or unparseable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "No resource matches the identifier supplied.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ServerError": {
        "description": "Something failed on Coral's side. Safe to retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Coral API documentation",
    "url": "https://coral.ax/docs"
  }
}