{
  "openapi": "3.0.3",
  "info": {
    "title": "PageRankCafe API",
    "description": "API for managing ads, credits, and referrals on PageRankCafe. Authenticate with a Bearer token obtained from your account's API Keys page.",
    "version": "1.0.0",
    "contact": {
      "url": "https://pagerankcafe.com/contact-us"
    }
  },
  "servers": [
    {
      "url": "https://pagerankcafe.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key obtained from your account's API Keys page at /apiKeys"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "views": {
            "type": "string"
          },
          "link_post_count": {
            "type": "string"
          }
        }
      },
      "Banner": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Banner image URL (HTTPS, must be a valid image with supported dimensions)"
          },
          "url_target": {
            "type": "string",
            "description": "Click-through destination URL (HTTPS)"
          },
          "impressions": {
            "type": "string"
          },
          "clicks": {
            "type": "string"
          },
          "ctr": {
            "type": "string"
          }
        }
      },
      "YouTubeAd": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "video_url": {
            "type": "string"
          },
          "views": {
            "type": "string"
          },
          "youtube_ad_post_count": {
            "type": "string"
          }
        }
      },
      "PressRelease": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "views": {
            "type": "string"
          },
          "post_count": {
            "type": "string"
          }
        }
      },
      "Credits": {
        "type": "object",
        "properties": {
          "available_credits": {
            "type": "integer"
          },
          "available_posts": {
            "type": "integer"
          },
          "links_viewed": {
            "type": "integer"
          },
          "links_remaining": {
            "type": "integer"
          }
        }
      },
      "Referrals": {
        "type": "object",
        "properties": {
          "today": {
            "type": "integer"
          },
          "last_7_days": {
            "type": "integer"
          },
          "last_30_days": {
            "type": "integer"
          },
          "all_time": {
            "type": "integer"
          }
        }
      },
      "LinkPost": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "link_id": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "bold": {
            "type": "string"
          },
          "view_time": {
            "type": "string"
          },
          "view_credits": {
            "type": "string"
          },
          "date_posted": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Post": {
        "type": "object",
        "description": "One row per ad placement. Field set depends on `type`: link (views, view_credits, ad_days, icon, bold, view_time, date_posted), banner (impressions, clicks, ctr, start_date, end_date, days), youtube (impressions, start_date, end_date, days), press_release (views, start_date, end_date, days).",
        "properties": {
          "id": {
            "type": "string"
          },
          "link_id": {
            "type": "string",
            "description": "type=link only"
          },
          "banner_id": {
            "type": "string",
            "description": "type=banner only"
          },
          "youtube_ad_id": {
            "type": "string",
            "description": "type=youtube only"
          },
          "press_release_id": {
            "type": "string",
            "description": "type=press_release only"
          },
          "views": {
            "type": "string",
            "description": "type=link and type=press_release"
          },
          "view_credits": {
            "type": "string",
            "description": "type=link only"
          },
          "ad_days": {
            "type": "string",
            "description": "type=link only"
          },
          "icon": {
            "type": "string",
            "description": "type=link only"
          },
          "bold": {
            "type": "string",
            "description": "type=link only"
          },
          "view_time": {
            "type": "string",
            "description": "type=link only"
          },
          "date_posted": {
            "type": "string",
            "format": "date-time",
            "description": "type=link only"
          },
          "impressions": {
            "type": "string",
            "description": "type=banner and type=youtube"
          },
          "clicks": {
            "type": "string",
            "description": "type=banner only"
          },
          "ctr": {
            "type": "string",
            "description": "type=banner only"
          },
          "start_date": {
            "type": "string",
            "format": "date-time",
            "description": "type=banner, youtube, press_release"
          },
          "end_date": {
            "type": "string",
            "format": "date-time",
            "description": "type=banner, youtube, press_release"
          },
          "days": {
            "type": "string",
            "description": "type=banner, youtube, press_release"
          }
        }
      },
      "PlatformInsights": {
        "type": "object",
        "properties": {
          "period_days": {
            "type": "integer",
            "example": 30
          },
          "banners": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "location": {
                  "type": "string",
                  "description": "Banner size/location, e.g. TOP_BOTTOM, HALF_SIDE"
                },
                "active_placements": {
                  "type": "integer"
                },
                "avg_impressions_per_placement_per_day": {
                  "type": "number"
                },
                "avg_ctr": {
                  "type": "number"
                }
              }
            }
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "icon": {
                  "type": "boolean"
                },
                "bold": {
                  "type": "boolean"
                },
                "view_time": {
                  "type": "integer"
                },
                "view_credits": {
                  "type": "integer"
                },
                "active_placements": {
                  "type": "integer"
                },
                "avg_views_per_placement_per_day": {
                  "type": "number"
                }
              }
            }
          },
          "youtube": {
            "type": "object",
            "properties": {
              "active_placements": {
                "type": "integer"
              },
              "avg_impressions_per_placement_per_day": {
                "type": "number"
              }
            }
          },
          "press_releases": {
            "type": "object",
            "properties": {
              "active_placements": {
                "type": "integer"
              },
              "avg_views_per_article_per_day": {
                "type": "number"
              }
            }
          },
          "platform": {
            "type": "object",
            "properties": {
              "daily_views": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "views": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "prices": {
            "type": "object",
            "properties": {
              "post_ratio": {
                "type": "integer",
                "description": "Credits per posted point"
              },
              "banner_price": {
                "type": "number"
              },
              "sticky_price": {
                "type": "number"
              },
              "press_release_price": {
                "type": "number"
              },
              "youtube_ad_price": {
                "type": "number"
              },
              "post_point_price": {
                "type": "number"
              },
              "tiers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "price": {
                      "type": "number"
                    },
                    "discount": {
                      "type": "number"
                    },
                    "multiplier": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/health": {
      "get": {
        "summary": "Health check",
        "description": "Returns API status and authenticated user info.",
        "tags": [
          "General"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "user": {
                      "type": "string",
                      "example": "johndoe"
                    },
                    "membership": {
                      "type": "string",
                      "enum": [
                        "free",
                        "upgraded"
                      ],
                      "example": "upgraded"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/links": {
      "get": {
        "summary": "List your links",
        "description": "Returns all active, non-reported links for the authenticated user.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Link"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/banners": {
      "get": {
        "summary": "List your banners",
        "description": "Returns all active banners for the authenticated user.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "banners": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Banner"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/youtube-ads": {
      "get": {
        "summary": "List your YouTube ads",
        "description": "Returns all active, non-reported YouTube ads for the authenticated user.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "youtube_ads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/YouTubeAd"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/press-releases": {
      "get": {
        "summary": "List your press releases",
        "description": "Returns all active, non-reported press releases for the authenticated user.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "press_releases": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PressRelease"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/credits": {
      "get": {
        "summary": "Get credit balance",
        "description": "Returns available credits, posts, links viewed, and links remaining for the authenticated user.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": {
                      "$ref": "#/components/schemas/Credits"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/referral-stats": {
      "get": {
        "summary": "Get referral statistics",
        "description": "Returns referral counts for today, last 7 days, last 30 days, and all time.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "referrals": {
                      "$ref": "#/components/schemas/Referrals"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/images/upload": {
      "post": {
        "summary": "Upload an image",
        "description": "Uploads an image and returns a hosted HTTPS URL for use elsewhere in the API (e.g. as a banner image URL or an inline press release image). Requires paid membership. Sent as multipart/form-data. Max 2MB; allowed formats JPG, PNG, GIF, WEBP. When type is 'banner' the image must match a supported banner dimension.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image",
                  "type"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "The image file (max 2MB; JPG, PNG, GIF, WEBP)."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "banner",
                      "article"
                    ],
                    "description": "Image category. 'banner' images must match a supported banner dimension: 728x90, 300x250, 120x600, 160x600, 468x60, 336x280, 250x250, 851x315, 650x400, 800x82, 234x60, 125x125, 300x200, 120x240, 240x400, 150x60"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Image uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "uploaded"
                    },
                    "type": {
                      "type": "string",
                      "example": "banner"
                    },
                    "url": {
                      "type": "string",
                      "example": "https://pagerankcafe-assets.s3.us-west-2.amazonaws.com/banners/123-uuid.png"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/banners/create": {
      "post": {
        "summary": "Create a banner",
        "description": "Creates a new banner ad. Requires paid membership. The banner image URL must be HTTPS and point to an image with supported dimensions. The location is automatically detected from the image size.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "url",
                  "url_target"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Banner title"
                  },
                  "url": {
                    "type": "string",
                    "description": "HTTPS URL to the banner image. Supported sizes: 728x90, 300x250, 120x600, 160x600, 468x60, 336x280, 250x250, 851x315, 650x400, 800x82, 234x60, 125x125, 300x200, 120x240, 240x400, 150x60"
                  },
                  "url_target": {
                    "type": "string",
                    "description": "HTTPS click-through destination URL"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Banner created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "banner": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "url_target": {
                          "type": "string"
                        },
                        "location": {
                          "type": "string",
                          "enum": [
                            "TOP_BOTTOM",
                            "HALF",
                            "FULL_SIDE",
                            "HALF_SIDE",
                            "BILLBOARD"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/banners/post": {
      "post": {
        "summary": "Post a banner with credits",
        "description": "Posts an existing banner into rotation using credits. Paid members can post 1-2 day banners (10 credits per day).",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "banner_id"
                ],
                "properties": {
                  "banner_id": {
                    "type": "integer",
                    "description": "ID of the banner to post"
                  },
                  "days": {
                    "type": "integer",
                    "description": "Duration in days. Paid members: 1-2.",
                    "default": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Banner posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "posted"
                    },
                    "banner_post": {
                      "type": "object",
                      "properties": {
                        "banner_id": {
                          "type": "string"
                        },
                        "days": {
                          "type": "string"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error or insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/youtube-ads/create": {
      "post": {
        "summary": "Create a YouTube ad",
        "description": "Creates a new YouTube video ad. Requires paid membership. The video ID is extracted automatically from the youtube_link. Creating with status 1 (the default) emails your followers about the video \u2014 followers are only notified once per video, ever. Pass status 0 to create it silently as Disabled.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "youtube_link"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Video ad title"
                  },
                  "youtube_link": {
                    "type": "string",
                    "description": "YouTube video URL (youtube.com/watch, youtu.be, embed, or /v/ formats)"
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional description"
                  },
                  "status": {
                    "type": "integer",
                    "description": "1 = Active (default; notifies your followers), 0 = Disabled (silent)",
                    "enum": [
                      0,
                      1
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "YouTube ad created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "youtube_ad": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "youtube_video_id": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error"
          },
          "403": {
            "description": "Paid membership required"
          }
        }
      }
    },
    "/api/youtube-ads/update": {
      "patch": {
        "summary": "Update a YouTube ad",
        "description": "Updates an existing YouTube video ad. Only the fields supplied are changed; omitted fields keep their current value. You can only update your own ads. Requires paid membership. Uses PATCH: the request is a partial update, not a full replacement.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "youtube_ad_id"
                ],
                "properties": {
                  "youtube_ad_id": {
                    "type": "integer",
                    "description": "ID of the video ad to update (must belong to you)"
                  },
                  "title": {
                    "type": "string",
                    "description": "Ad title"
                  },
                  "description": {
                    "type": "string",
                    "description": "Ad description"
                  },
                  "youtube_link": {
                    "type": "string",
                    "description": "YouTube video URL; the video ID is re-derived from it"
                  },
                  "status": {
                    "type": "integer",
                    "description": "1 = Active, 0 = Disabled. Setting an ad Active emails your followers (once per ad, ever)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "YouTube ad updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "updated"
                    },
                    "youtube_ad": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "youtube_video_id": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "YouTube ad not found or not owned by you",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/press-releases/create": {
      "post": {
        "summary": "Create a press release",
        "description": "Creates a new press release / blog article. Requires paid membership.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "url",
                  "intro",
                  "body",
                  "slug"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Article title"
                  },
                  "url": {
                    "type": "string",
                    "description": "HTTPS source URL"
                  },
                  "intro": {
                    "type": "string",
                    "description": "Short introduction text"
                  },
                  "body": {
                    "type": "string",
                    "description": "Full article HTML content"
                  },
                  "slug": {
                    "type": "string",
                    "description": "URL slug (must be unique)"
                  },
                  "meta_description": {
                    "type": "string",
                    "description": "SEO meta description (optional)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Press release created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "press_release": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/press-releases/update": {
      "patch": {
        "summary": "Update a press release",
        "description": "Updates an existing press release / blog article. Only the fields supplied are changed; omitted fields keep their current value. You can only update your own articles. Requires paid membership. Uses PATCH: the request is a partial update, not a full replacement.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "press_release_id"
                ],
                "properties": {
                  "press_release_id": {
                    "type": "integer",
                    "description": "ID of the article to update (must belong to you)"
                  },
                  "title": {
                    "type": "string",
                    "description": "Article title"
                  },
                  "url": {
                    "type": "string",
                    "description": "HTTPS source URL"
                  },
                  "intro": {
                    "type": "string",
                    "description": "Short introduction text"
                  },
                  "body": {
                    "type": "string",
                    "description": "Full article HTML content"
                  },
                  "slug": {
                    "type": "string",
                    "description": "URL slug (must be unique)"
                  },
                  "meta_description": {
                    "type": "string",
                    "description": "SEO meta description"
                  },
                  "status": {
                    "type": "integer",
                    "description": "1 = Active, 0 = Disabled. Setting an article Active emails your followers (once per article, ever)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Press release updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "updated"
                    },
                    "press_release": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Press release not found or not owned by you",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/press-releases/post": {
      "post": {
        "summary": "Post a press release with credits",
        "description": "Posts an existing press release to the press release widget on the home page using credits. Paid members can post 7-day press releases (70 credits / 7 post points).",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "press_release_id"
                ],
                "properties": {
                  "press_release_id": {
                    "type": "integer",
                    "description": "ID of the press release to post"
                  },
                  "days": {
                    "type": "integer",
                    "description": "Duration in days. Paid members: 7.",
                    "default": 7
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Press release posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "posted"
                    },
                    "press_release_post": {
                      "type": "object",
                      "properties": {
                        "press_release_id": {
                          "type": "string"
                        },
                        "days": {
                          "type": "string"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error or insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/links/create": {
      "post": {
        "summary": "Create a link ad",
        "description": "Creates a new link ad, mirroring LinksController::add's validation. Requires a paid membership. Returns the new link's id.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "title"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "HTTPS destination URL for the link ad"
                  },
                  "title": {
                    "type": "string",
                    "description": "Link ad title"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "link": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/links/post": {
      "post": {
        "summary": "Post a link ad with credits",
        "description": "Posts an existing link ad into rotation using credits, mirroring the web free-post flow (LinkPost::postLink/savePost). Sponsored (paid via PayPal/Stripe) posts are not exposed here and stay on the website; ad_days must be 0. view_time and view_credits are derived server-side from icon/bold/double_time (the same schedule the web free-post form uses) and are not accepted as input, since view_credits is the credit reward funded by other members' views. If the posting user needs approval (LinkPost::user_needs_approval, e.g. the user has been reported), the post is created with status -2 and the response reports status \"pending\" with a message that it awaits approval before going live; otherwise the post is active immediately and the response reports status \"active\". Requires a paid membership. Returns the post id, the derived view_time/view_credits, and the caller's remaining credit balance.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "link_id",
                  "ad_days"
                ],
                "properties": {
                  "link_id": {
                    "type": "integer",
                    "description": "ID of the link to post (must belong to the caller)"
                  },
                  "ad_days": {
                    "type": "integer",
                    "description": "Must be 0. Sponsored/paid link placements (ad_days > 0) require PayPal/Stripe checkout and are not available via the API.",
                    "example": 0
                  },
                  "icon": {
                    "type": "boolean",
                    "description": "Show an icon next to the link (adds credit cost)",
                    "default": false
                  },
                  "bold": {
                    "type": "boolean",
                    "description": "Bold the link text (adds credit cost)",
                    "default": false
                  },
                  "double_time": {
                    "type": "boolean",
                    "description": "Double the ad's view time (view_time 6s -> 12s, +8 credits), matching the web free-post form's \"double view time\" option.",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "active",
                      "description": "\"active\" if the post is live immediately, \"pending\" if it requires approval first (LinkPost.status -2)."
                    },
                    "link_post": {
                      "$ref": "#/components/schemas/LinkPost"
                    },
                    "remaining_credits": {
                      "type": "integer"
                    },
                    "message": {
                      "type": "string",
                      "description": "Present only when status is \"pending\": explains that the post awaits approval before it goes live."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error, insufficient credits, or ad_days != 0",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Link not found or does not belong to the caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/posts": {
      "get": {
        "summary": "List ad placement performance",
        "description": "Returns one row per ad placement (post) belonging to the caller, with the performance fields that exist for that ad type.",
        "tags": [
          "Read"
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "link",
                "banner",
                "youtube",
                "press_release"
              ]
            },
            "description": "Ad type to list placements for"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "expired"
              ]
            },
            "description": "Filter placements by status. Omit to return all placements regardless of status."
          }
        ],
        "responses": {
          "200": {
            "description": "Placements for the requested type",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Post"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid type/status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/insights/platform": {
      "get": {
        "summary": "Platform-wide advertising insights",
        "description": "Trailing 30-day aggregates by ad type and option: active placement counts, average impressions/views per placement per day, average CTR, a 30-day daily platform views series, and current prices. Aggregates only \u2014 never per-member or per-item data belonging to other users. Cached for 1 hour. Requires a paid membership.",
        "tags": [
          "Read"
        ],
        "responses": {
          "200": {
            "description": "Platform insights",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "insights": {
                      "$ref": "#/components/schemas/PlatformInsights"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Paid membership required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "General",
      "description": "Health and status endpoints"
    },
    {
      "name": "Read",
      "description": "Read-only endpoints available to all members"
    },
    {
      "name": "Write",
      "description": "Write endpoints requiring paid membership"
    }
  ]
}
