{
  "openapi": "3.0.0",
  "info": {
    "title": "API SERPmantics",
    "version": "1.0",
    "description": "Every authenticated endpoint may answer `503` with `{ \"success\": false, \"error\": \"Service temporarily unavailable\", \"code\": \"database_unavailable\" }` and a `Retry-After: 30` header when the database cannot be reached. This is a temporary outage on our side: your API key is not in question, do not regenerate or revoke it, retry after the delay (see the `DatabaseUnavailable` response)."
  },
  "paths": {
    "/api/v1/aissistant/tokens": {
      "get": {
        "summary": "Get the current user's available AI tokens",
        "description": "Returns the number of AI tokens available to the authenticated user.\n\nThese tokens fund EVERY AI feature in SERPmantics — meta, outline,\nintent, internal-links, EEAT, EEAT competitors, score, AND the\nAISSistant prompts. The endpoint lives under /aissistant for\nhistorical reasons but the balance is shared across all AI features.\n\nDo NOT confuse with guide-creation credits (see /api/v1/credits).\nFor a combined view (credits + tokens) prefer /api/v1/credits.\n",
        "tags": [
          "v1",
          "Usage"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successfully retrieved token balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "tokens": {
                      "type": "number",
                      "example": 100
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid or missing authentication",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Unauthorized"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Method not allowed"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Internal server error"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/credits": {
      "get": {
        "summary": "Get user balance (guide credits + AI tokens)",
        "description": "Returns the authenticated user's full balance.\n\nSERPmantics has TWO distinct currencies:\n\n- **credits** (`credits`): how many NEW GUIDES the user can still create.\n  Consumed once per guide creation. `\"unlimited\"` if the user's plan\n  grants unlimited guide creation (`hasUnlimitedCredits: true`).\n\n- **AI tokens** (`tokens`): pool consumed by every AI feature (meta,\n  outline, intent, internal-links, EEAT, EEAT competitors…).\n  Each feature has its own cost — call `/api/v1/tokens-usage` to get\n  the per-feature pricing.\n\nDo not confuse the two: running out of `credits` blocks new guides;\nrunning out of `tokens` blocks AI features inside existing guides.\n",
        "tags": [
          "v1",
          "Usage"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with user balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "credits": {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "type": "string",
                          "enum": [
                            "unlimited"
                          ]
                        }
                      ],
                      "description": "Remaining guide-creation credits, or \"unlimited\""
                    },
                    "hasUnlimitedCredits": {
                      "type": "boolean",
                      "description": "True if the user's plan grants unlimited guide creation"
                    },
                    "tokens": {
                      "type": "number",
                      "description": "Remaining AI tokens (used by meta, outline, intent, internal-links, EEAT)",
                      "example": 1234
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/api/v1/eeat-competitors": {
      "post": {
        "summary": "Start an E-E-A-T analysis on a guide's top SERP competitors",
        "description": "Starts an asynchronous E-E-A-T analysis on the top competitors of the guide's SERP.\nEach competitor is fetched and scored individually. Use GET /api/v1/eeat-competitors to\npoll for results until `pending` reaches `0`.\n\n**Token cost:** This endpoint charges `eeatCompetitorsTokensCostPerCompetitor`\n(see /api/v1/tokens-usage) per competitor analyzed, up to 10 competitors.\nExample: 10 competitors × 5 tokens = 50 tokens. 6 competitors × 5 tokens = 30 tokens.\nThe number of competitors corresponds to the deduplicated URLs in the guide's top-10 SERP\nresults (available via GET /api/v1/guide).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide whose competitors should be analyzed",
                    "example": "507f1f77bcf86cd799439011"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Competitor analysis successfully started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "description": "Initial state of the analysis (counts and pending competitors)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Insufficient tokens or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't own the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "409": {
            "description": "Conflict. Most often: no SERP corpus is available for this guide, so its\ncompetitors cannot be analyzed. A corpus is missing when its pages were never\ncollected, or when they have been purged (pages older than 180 days are purged\nnightly).\n\nThe tokens reserved for the call are refunded before this response is returned,\n**and `refunded` tells you whether that actually succeeded**. Trust that field,\nnot the sentence: when `refunded` is `false` the refund failed, it is logged\nserver-side for a manual replay, and `error` says so instead of claiming that\nnothing was charged.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "description": "Ends with `No tokens were charged.` only when `refunded` is `true`. When the\nrefund failed, it says so instead and asserts nothing about the balance.\n",
                      "example": "No SERP corpus available for this guide: its analyzable pages are gone or were never collected, which is notably the case for guides older than 180 days. No tokens were charged."
                    },
                    "errorCode": {
                      "type": "string",
                      "description": "Machine-readable reason, so integrations never have to match on the message.",
                      "enum": [
                        "no_serp_corpus",
                        "conflict"
                      ],
                      "example": "no_serp_corpus"
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Whether the tokens reserved for the call were actually given back.\n`true`: the credit is on the ledger, nothing was charged. `false`: the\nrefund itself failed (it is logged server-side so it can be replayed by\nhand), so do NOT tell the user that no tokens were charged. The reason\nfor the refusal is unaffected: `errorCode` still carries the real cause.\n",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is reserved nor consumed by this refusal, so there is nothing to refund and no `refunded` field: unlike the 409 below, which counts pages and can say nothing about a date, this refusal reads the creation date and nothing else."
          },
          "500": {
            "description": "Internal server error.\n\nWhen the failure comes from the backend call (unreachable, timeout, backend 500),\nthe tokens reserved for it are refunded before this response is returned and the\nbody carries `refunded`, with the same meaning as on the 409: `true` means the\ncredit is on the ledger, `false` means the refund failed and is logged for a\nmanual replay. The `error` sentence follows that field and never claims more.\n\nWhen the failure comes from writing the token ledger itself, the tokens are\nrefunded too and `refunded` is carried the same way. That case is worth telling\napart: the balance was already debited, only the ledger line failed, so the\nsentence never claims the debit did not happen.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "description": "Ends with `No tokens were charged.` only when `refunded` is `true`, as on\nthe 409. The ledger-write failure says instead that the debit could not\nbe recorded, because on that path the balance really did move.\n",
                      "example": "Failed to start competitors EEAT analysis. No tokens were charged."
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Same meaning as on the 409. Carried whether the failure comes from the\nbackend call or from writing the token ledger. Trust this field, never\nthe sentence.\n",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get E-E-A-T analysis results for a guide's competitors",
        "description": "Retrieves the E-E-A-T analysis results for the top competitors of a guide.\nPoll this endpoint until `pending` is `0` to know when the full analysis is complete.\nIndividual competitor results are available as soon as their `status` is `done`.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide whose competitors should be returned",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Competitor analysis successfully retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "description": "Total number of competitors being analyzed",
                          "example": 10
                        },
                        "completed": {
                          "type": "integer",
                          "description": "Number of competitors whose analysis is done",
                          "example": 7
                        },
                        "pending": {
                          "type": "integer",
                          "description": "Number of competitors still being analyzed",
                          "example": 3
                        },
                        "averages": {
                          "type": "object",
                          "description": "Average scores across completed competitors (only when at least one is done)",
                          "properties": {
                            "scores": {
                              "type": "object",
                              "properties": {
                                "experience": {
                                  "type": "number",
                                  "example": 6.8
                                },
                                "expertise": {
                                  "type": "number",
                                  "example": 7.2
                                },
                                "authoritativeness": {
                                  "type": "number",
                                  "example": 7.5
                                },
                                "trustworthiness": {
                                  "type": "number",
                                  "example": 7
                                }
                              }
                            },
                            "overall": {
                              "type": "number",
                              "example": 7.13
                            }
                          }
                        },
                        "competitors": {
                          "type": "array",
                          "description": "List of competitor analyses (one per SERP result)",
                          "items": {
                            "type": "object",
                            "properties": {
                              "competitor_position": {
                                "type": "integer",
                                "description": "Position of the competitor in the SERP. snake_case, like every field of these items: this handler serves the backend body verbatim (`data: response.data`), and each item is built by `to_dict()`, which emits the dataclass field names. Only the envelope around them (`total`, `completed`, `pending`, `averages`, `competitors`) is a Python literal, hence its plain names.",
                                "example": 1
                              },
                              "competitor_url": {
                                "type": "string",
                                "description": "URL of the competitor's page",
                                "example": "https://example.com/article"
                              },
                              "status": {
                                "type": "string",
                                "description": "Status of the competitor's analysis",
                                "enum": [
                                  "pending",
                                  "started",
                                  "analyzing",
                                  "done",
                                  "failed"
                                ],
                                "example": "done"
                              },
                              "article_context": {
                                "type": "object",
                                "description": "Detected context of the competitor's article (only when status is `done`). snake_case at BOTH levels, the object and its `primary_purpose`.",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "example": "How-to guide"
                                  },
                                  "niche": {
                                    "type": "string",
                                    "example": "SEO"
                                  },
                                  "primary_purpose": {
                                    "type": "string",
                                    "example": "Inform"
                                  }
                                }
                              },
                              "article": {
                                "type": "object",
                                "description": "Scores of the competitor's article (only when status is `done`)",
                                "properties": {
                                  "scores": {
                                    "type": "object",
                                    "properties": {
                                      "experience": {
                                        "type": "number",
                                        "example": 7
                                      },
                                      "expertise": {
                                        "type": "number",
                                        "example": 7.5
                                      },
                                      "authoritativeness": {
                                        "type": "number",
                                        "example": 8
                                      },
                                      "trustworthiness": {
                                        "type": "number",
                                        "example": 7
                                      }
                                    }
                                  },
                                  "overall": {
                                    "type": "number",
                                    "example": 7.38
                                  }
                                }
                              },
                              "analysis": {
                                "type": "object",
                                "description": "Detailed assessment per dimension (only when status is `done`)",
                                "properties": {
                                  "experience": {
                                    "type": "object",
                                    "properties": {
                                      "assessment": {
                                        "type": "string"
                                      }
                                    }
                                  },
                                  "expertise": {
                                    "type": "object",
                                    "properties": {
                                      "assessment": {
                                        "type": "string"
                                      }
                                    }
                                  },
                                  "authoritativeness": {
                                    "type": "object",
                                    "properties": {
                                      "assessment": {
                                        "type": "string"
                                      }
                                    }
                                  },
                                  "trustworthiness": {
                                    "type": "object",
                                    "properties": {
                                      "assessment": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "summary": {
                                "type": "array",
                                "description": "List of strengths (only when status is `done`)",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "title": {
                                      "type": "string"
                                    },
                                    "detail": {
                                      "type": "string"
                                    }
                                  }
                                }
                              },
                              "suggestions": {
                                "type": "array",
                                "description": "List of improvement suggestions (only when status is `done`)",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't have access to the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      }
    },
    "/api/v1/eeat": {
      "post": {
        "summary": "Start an E-E-A-T analysis on a guide content",
        "description": "Starts an asynchronous E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) analysis\non the HTML content provided for a given guide. The analysis runs in the background — use\nGET /api/v1/eeat to poll for results until `status` is `done` (or `failed`).\n\n**Note:** This endpoint uses tokens (check /api/v1/tokens-usage for the cost, might vary).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId",
                  "html_content"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide to analyze",
                    "example": "507f1f77bcf86cd799439011"
                  },
                  "html_content": {
                    "type": "string",
                    "description": "HTML of the article to analyze (without `<mark>` tags)",
                    "example": "<h1>Title</h1><p>This is my article HTML content...</p>"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Analysis successfully started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "eeatId": {
                          "type": "string",
                          "description": "ID of the started analysis, use it to poll for the result via GET /api/v1/eeat",
                          "example": "507f1f77bcf86cd799439013"
                        },
                        "status": {
                          "type": "string",
                          "description": "Initial status of the analysis (typically `pending` or `started`)",
                          "example": "pending"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Missing html_content, insufficient tokens, or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't own the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get E-E-A-T analysis results",
        "description": "Retrieves the result of an E-E-A-T analysis.\n\n`guideId` is **always required**: access is granted on the guide, so a request without it\nis answered with `404 Guide not found`. `eeatId` is an optional filter on top of it: pass it\nto fetch one specific analysis of that guide, omit it to fetch the latest one.\nAn `eeatId` that does not belong to `guideId` is answered with `404`, never with the\nother account's data.\n\nPoll this endpoint until `status` is `done` (results available) or `failed`.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide the analysis belongs to. Required, and access is checked on it.",
            "example": "507f1f77bcf86cd799439011"
          },
          {
            "in": "query",
            "name": "eeatId",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional filter, ID of one specific analysis of that guide (returned by POST /api/v1/eeat). Omit it to get the latest analysis of `guideId`.",
            "example": "507f1f77bcf86cd799439013"
          }
        ],
        "responses": {
          "200": {
            "description": "Analysis successfully retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "description": "Status of the analysis",
                          "enum": [
                            "none",
                            "pending",
                            "started",
                            "analyzing",
                            "done",
                            "failed"
                          ],
                          "example": "done"
                        },
                        "article_context": {
                          "type": "object",
                          "description": "Detected context of the analyzed article (only when status is `done`). Note the snake_case, here and in `primary_purpose`: this handler serves the backend body verbatim (`data: served`), and the backend builds it with `to_dict()`, which emits the dataclass field names. It is documented as it is actually served, not as it reads best.",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "How-to guide"
                            },
                            "niche": {
                              "type": "string",
                              "example": "SEO"
                            },
                            "primary_purpose": {
                              "type": "string",
                              "example": "Inform"
                            }
                          }
                        },
                        "article": {
                          "type": "object",
                          "description": "Scores of the analyzed article (only when status is `done`)",
                          "properties": {
                            "scores": {
                              "type": "object",
                              "description": "Individual E-E-A-T scores (each between 0 and 10)",
                              "properties": {
                                "experience": {
                                  "type": "number",
                                  "example": 7.5
                                },
                                "expertise": {
                                  "type": "number",
                                  "example": 8
                                },
                                "authoritativeness": {
                                  "type": "number",
                                  "example": 6.5
                                },
                                "trustworthiness": {
                                  "type": "number",
                                  "example": 7
                                }
                              }
                            },
                            "overall": {
                              "type": "number",
                              "description": "Overall E-E-A-T score (between 0 and 10)",
                              "example": 7.25
                            }
                          }
                        },
                        "analysis": {
                          "type": "object",
                          "description": "Detailed assessment per dimension (only when status is `done`)",
                          "properties": {
                            "experience": {
                              "type": "object",
                              "properties": {
                                "assessment": {
                                  "type": "string",
                                  "example": "The article shows some personal experience..."
                                }
                              }
                            },
                            "expertise": {
                              "type": "object",
                              "properties": {
                                "assessment": {
                                  "type": "string"
                                }
                              }
                            },
                            "authoritativeness": {
                              "type": "object",
                              "properties": {
                                "assessment": {
                                  "type": "string"
                                }
                              }
                            },
                            "trustworthiness": {
                              "type": "object",
                              "properties": {
                                "assessment": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "summary": {
                          "type": "array",
                          "description": "List of strengths identified in the article (only when status is `done`)",
                          "items": {
                            "type": "object",
                            "properties": {
                              "title": {
                                "type": "string",
                                "example": "Clear author bio"
                              },
                              "detail": {
                                "type": "string",
                                "example": "The author bio clearly establishes credentials..."
                              }
                            }
                          }
                        },
                        "suggestions": {
                          "type": "array",
                          "description": "List of suggestions to improve the article (only when status is `done`)",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Add references to peer-reviewed studies",
                            "Include the author's professional background"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't have access to the guide"
          },
          "404": {
            "description": "Guide not found (including when `guideId` is missing or malformed), or the requested `eeatId` is unknown, or malformed, or it does not belong to the guide given in `guideId`. The three `eeatId` cases are deliberately indistinguishable, same status and same body `{ success: false, error: \"EEAT audit not found\" }`: telling them apart would confirm the existence of someone else's analysis."
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      }
    },
    "/api/v1/information-gain": {
      "post": {
        "operationId": "createInformationGain",
        "x-serpmantics-access": "information-gain",
        "summary": "Launch an information gain analysis on a guide",
        "description": "Réservé : en test, accès sur invitation. Reserved feature, in test, access by invitation. Accounts without access receive 403 FEATURE_NOT_AVAILABLE.\n\nAnswers the question \"what is missing from my text to bring more than the top results, and where?\". The analysis reads the competing pages of the guide's Google results, extracts the facts they state (each with a verbatim excerpt), groups them, checks which ones the guide text states (covered, partial or absent), and lists information gain leads found beyond the top results plus first-hand questions only the author can answer.\n\nWhen to call it. Once per guide, or again after the competing pages changed. To check an edited text against an existing analysis, call recheck_information_gain_coverage instead (about 10 seconds, no competitor is read again).\n\nThe analysis is asynchronous and takes 30 to 60 seconds. Wait about 30 seconds, then call get_information_gain (view=summary, the default) every 10 seconds until status is done or failed. Read view=summary first; ask for view=full only if the whole matrix is needed.\n\nhtmlContent is optional. When given, the guide column is checked against this HTML (send the text as displayed, without mark tags); when omitted, the text saved in the guide is used. htmlContent is limited to 2,000,000 bytes (UTF-8) through the API, and to about 1 MB through the MCP server (the size of the whole MCP request).\n\nCosts 60 tokens, charged when the analysis is launched (202, the body carries tokensCharged). Nothing is charged when the call is refused before the launch is attempted (invalid input, 403, 404, or 409 ANALYSIS_RUNNING for an analysis already running). When the launch itself fails after the charge, whatever the status, the tokens are given back before the response and the body carries refunded (true when the credit is on the ledger, false when the refund failed and is logged for a manual replay). When an accepted analysis ends failed, its tokens are given back at the first read that sees it (get_information_gain), whose response then carries refunded and refundedTokens.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide to analyze (24 hexadecimal characters).",
                    "example": "507f1f77bcf86cd799439011"
                  },
                  "htmlContent": {
                    "type": "string",
                    "description": "Optional HTML of the article to check, without mark tags. Omit it to check the text saved in the guide. At most 2,000,000 bytes (UTF-8) through the API, about 1 MB through the MCP server.",
                    "example": "<h1>Title</h1><p>My article...</p>"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Analysis launched. Poll pollUrl (or call get_information_gain) until status is done or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "analysisId": {
                          "type": "string",
                          "example": "6ad076f5848df4b18fb3d71e"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "pollUrl": {
                          "type": "string",
                          "example": "/api/v1/information-gain?guideId=507f1f77bcf86cd799439011&view=summary"
                        },
                        "tokensCharged": {
                          "type": "integer",
                          "description": "Tokens charged for this launch.",
                          "example": 60
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, or not enough tokens. code is INVALID_GUIDE_ID (missing or malformed guideId) or INVALID_HTML_CONTENT (htmlContent given but not a non-empty string, or not valid Unicode, for instance a lone UTF-16 surrogate), fix the input and do not retry as is. code INSUFFICIENT_TOKENS means the balance is under the 60 tokens of an analysis, nothing was charged nor launched, and the body carries inviteToUpgrade true, upgradeType tokens and tokensRequired. code TOKENS_NOT_CONSUMED means the debit could not be confirmed, nothing was launched.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_GUIDE_ID",
                        "INVALID_HTML_CONTENT",
                        "INSUFFICIENT_TOKENS",
                        "TOKENS_NOT_CONSUMED"
                      ]
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    },
                    "tokensRequired": {
                      "type": "integer",
                      "example": 60
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Present when the tokens had been charged and the backend then refused the text. true when they were given back, false when the refund failed and is logged for a manual replay."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized, invalid or missing API key."
          },
          "403": {
            "description": "code FEATURE_NOT_AVAILABLE. The feature is in test and reserved to invited accounts. Nothing was read nor launched. Do not retry."
          },
          "404": {
            "description": "code GUIDE_NOT_FOUND. The guide does not exist, was deleted, or is not accessible to this account (owner, or guide shared for edition). The cases are deliberately indistinguishable."
          },
          "405": {
            "description": "Method not allowed."
          },
          "409": {
            "description": "code ANALYSIS_RUNNING. An analysis (or a recheck) is already running for this guide. The body carries analysisId (the running one, null if it could not be read) and pollUrl, so follow it instead of launching again. Nothing is charged when the running analysis is seen before charging. When it is only reported by the analysis service (concurrent launch), the tokens are given back and the body carries refunded."
          },
          "413": {
            "description": "code CONTENT_TOO_LARGE. htmlContent is over 2,000,000 bytes (UTF-8). Send a shorter text, do not retry as is. A JSON body over 4 MB is refused before being read, with a plain text 413."
          },
          "500": {
            "description": "Internal error. When the token debit could not be recorded, the tokens are given back and the body carries refunded."
          },
          "502": {
            "description": "code BACKEND_ERROR. The analysis service did not answer or answered unexpectedly. Retry later, unless the message says retrying will not help. The tokens charged for the call are given back and the body carries refunded."
          },
          "503": {
            "description": "Temporary outage (database or analysis service). Retry after a short delay."
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "operationId": "getInformationGain",
        "x-serpmantics-access": "information-gain",
        "summary": "Get the latest information gain analysis of a guide",
        "description": "Réservé : en test, accès sur invitation. Reserved feature, in test, access by invitation. Accounts without access receive 403 FEATURE_NOT_AVAILABLE.\n\nReturns the latest analysis of the guide, whatever its status. While status is pending or running, call again every 10 seconds (an analysis takes 30 to 60 seconds, a recheck about 10 seconds).\n\nview=summary (default, read it first) is built for an agent. score is the coverage of the consensus, from 0 to 100, or null when nothing is measurable (never 0 by default). The consensus is the facts stated by at least 2 distinct sites among the top pages (a site is a registrable domain, so two pages of the same domain or of its subdomains count as one site), and each consensus fact weighs its number of sites in the score. missing lists the consensus facts absent from the text, partial the ones stated only in part (with what is missing), both sorted by number of sites, then number of pages, then best position. single lists the facts stated by one site only and not covered (outside the score). leads are facts found beyond the top results, questions are first-hand questions only the author can answer. covered lists the consensus facts the text already states, and unknown the consensus facts with no verdict on the text yet (outside the score). sites lists the distinct sites among the top pages read, one per site with its best ranked page (url, position) and its number of pages read, sorted by position. Each source carries the key of its site, the same as in sites, so a fact can be laid out site by site. counts has one scope, the consensus (consensus, covered, partial, missing, unknown), plus single, the length of the single list. Only the pages read from the top results count as pages or sites of a fact. Every fact carries pages and pagesTotal (pages that state it, pages read), sites and sitesTotal (distinct sites that state it, distinct sites among the pages read), its sources, one per page (url, position, verbatim excerpt), and the verdict on the guide text (status, excerpt, missing). Every fact also carries scoreGain, the points the score would gain if the text fully stated it, all else being equal, max(1, round(100 x sites x (1 - c) / total)), c being 0 for absent and 0.5 for partial, total the sum of sites of the consensus facts with a known verdict (the denominator of score). scoreGain is null for any other fact (covered, unknown, outside the consensus), never 0, and since every positive gain counts at least 1, the gains may add up to more than 100 minus score, so never sum them. textSource is request as soon as SERPmantics sent the text to check (htmlContent, or the saved text read when the call was made), saved when the analysis service read the saved text itself (older analyses), and null while status is pending or running, or when a failed analysis did not record it (unknown source). textHash is the fingerprint of the text actually read. sitesRead is the number of distinct sites among the top pages read (same rule as sitesTotal), null when no top page was read. limits flags partial measures, and limits.issues lists each partial failure as a stable code (leads_untriaged, source_check_partial, cross_check_partial, questions_failed, coverage_partial, coverage_no_answer, excerpts_rejected) with failed and total when known. excerpts_rejected also carries undecided, the items the check left undecided for any reason; when it equals failed, checking again would not settle them.\n\nview=full returns the whole normalized document (full matrix of topics, facts and sources, pages read and skipped with their reason, statistics).\n\nReading is free. When the analysis ended failed after being launched (or the last recheck ended without a new verdict, the analysis being done again with only its error set), the tokens charged for it are given back once, at the first read that sees it. The response then carries refunded (true when the credit is on the ledger, false when the refund failed and will be tried again at a later read) and refundedTokens, for the account that was charged. Both fields are absent when nothing was given back, or when the reader is not the charged account (the tokens still go back to it).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide (24 hexadecimal characters).",
            "example": "507f1f77bcf86cd799439011"
          },
          {
            "in": "query",
            "name": "view",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "summary",
                "full"
              ]
            },
            "description": "summary (default) for the scored, sorted lists; full for the whole matrix."
          }
        ],
        "responses": {
          "200": {
            "description": "The analysis, as a summary (view=summary) or as the full normalized document (view=full).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Present only when the analysis (or its last recheck) failed after being launched and charged. true when its tokens were given back, false when the refund failed (tried again at the next read)."
                    },
                    "refundedTokens": {
                      "type": "integer",
                      "description": "Tokens given back (or to give back), present with refunded.",
                      "example": 60
                    },
                    "data": {
                      "type": "object",
                      "description": "With view=summary, the fields below. Each fact is an object with id, statement, topic, pages, pagesTotal, sites, sitesTotal, sources (one per page, url, position, excerpt, site), guide (status covered, partial, absent or unknown, excerpt, missing) and scoreGain (integer or null, the points the score would gain if the text fully stated an absent or partial consensus fact, null otherwise). The sites field of a fact is a number, per fact, counting the distinct registrable domains among its sources, and decides the consensus (at least 2 sites). Do not confuse it with the top level sites field, the list of the top sites read.",
                      "properties": {
                        "analysisId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "running",
                            "done",
                            "failed"
                          ]
                        },
                        "stage": {
                          "type": "string",
                          "nullable": true
                        },
                        "mode": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "analysis",
                            "guide_coverage"
                          ],
                          "description": "Nature of the current or last run. analysis reads the top pages and builds the lists, guide_coverage only re-checks the text of the guide against the facts already extracted. null when the document does not say. Read this field, not stage, to tell a re-check apart from a full analysis passing through the guide_coverage stage."
                        },
                        "progress": {
                          "type": "object",
                          "nullable": true,
                          "description": "Counter written by the current stage while the analysis runs, null when the stage writes none. done and total are integers, unit is pages (pages read during the extraction) or facts (facts processed during the grouping and the check of the text). Never an estimate.",
                          "properties": {
                            "done": {
                              "type": "integer",
                              "example": 3
                            },
                            "total": {
                              "type": "integer",
                              "example": 7
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "pages",
                                "facts"
                              ]
                            }
                          }
                        },
                        "runStartedAt": {
                          "type": "string",
                          "nullable": true,
                          "description": "Start of the current run (ISO 8601), the moment the analysis went running, else its launch. null when the document does not say."
                        },
                        "lastActivityAt": {
                          "type": "string",
                          "nullable": true,
                          "description": "Last write of the analysis service (ISO 8601), updated at each stage. null when the document does not say."
                        },
                        "analyzedAt": {
                          "type": "string",
                          "nullable": true
                        },
                        "checkedAt": {
                          "type": "string",
                          "nullable": true
                        },
                        "textSource": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "request",
                            "saved"
                          ],
                          "description": "request when SERPmantics sent the text to check (htmlContent, or the saved text read at call time), saved when the analysis service read the saved text itself, null while status is pending or running, or when a failed analysis did not record it."
                        },
                        "textHash": {
                          "type": "string",
                          "nullable": true,
                          "description": "Fingerprint of the text actually read by the guide column."
                        },
                        "score": {
                          "type": "integer",
                          "nullable": true,
                          "example": 75
                        },
                        "counts": {
                          "type": "object",
                          "description": "One scope, the consensus (facts stated by at least 2 distinct sites among the pages read from the top results), so consensus equals covered plus partial plus missing plus unknown. single is the length of the single list, outside the score.",
                          "properties": {
                            "consensus": {
                              "type": "integer",
                              "example": 8
                            },
                            "covered": {
                              "type": "integer",
                              "example": 5
                            },
                            "partial": {
                              "type": "integer",
                              "example": 2
                            },
                            "missing": {
                              "type": "integer",
                              "example": 1
                            },
                            "unknown": {
                              "type": "integer",
                              "example": 0
                            },
                            "single": {
                              "type": "integer",
                              "example": 40
                            },
                            "leads": {
                              "type": "integer"
                            },
                            "questions": {
                              "type": "integer"
                            }
                          }
                        },
                        "missing": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "scoreGain": {
                                "type": "integer",
                                "nullable": true,
                                "example": 10
                              }
                            }
                          }
                        },
                        "partial": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "scoreGain": {
                                "type": "integer",
                                "nullable": true,
                                "example": 5
                              }
                            }
                          }
                        },
                        "single": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "leads": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "questions": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "question": {
                                "type": "string"
                              },
                              "why": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "covered": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "unknown": {
                          "type": "array",
                          "description": "Consensus facts with no verdict on the guide text yet, sorted like missing. Its length equals counts.unknown. Outside the score.",
                          "items": {
                            "type": "object"
                          }
                        },
                        "sites": {
                          "type": "array",
                          "description": "The list of the top sites read (not the number of sites of a fact), one entry per distinct site (registrable domain) among the top pages read, sorted by position of its best ranked page, unknown positions last. Its length equals sitesRead, and it is empty when sitesRead is null. The site key is the one carried by each source of a fact.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "site": {
                                "type": "string",
                                "description": "Key of the site, the same as the site of a source."
                              },
                              "domain": {
                                "type": "string",
                                "description": "Host name of the best ranked page of the site, for display."
                              },
                              "url": {
                                "type": "string",
                                "description": "URL of the best ranked page of the site."
                              },
                              "position": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Position of the best ranked page of the site."
                              },
                              "pages": {
                                "type": "integer",
                                "description": "Number of top pages read from this site."
                              }
                            }
                          }
                        },
                        "sitesRead": {
                          "type": "integer",
                          "nullable": true,
                          "example": 7,
                          "description": "Distinct sites (registrable domains) among the top pages read, null when no top page was read. Never 0 by default."
                        },
                        "limits": {
                          "type": "object",
                          "properties": {
                            "issues": {
                              "type": "array",
                              "description": "Partial failures of the analysis, one entry per problem, as stable codes. A failure of the check of the guide text appears once, as coverage_partial or coverage_no_answer.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "code": {
                                    "type": "string",
                                    "enum": [
                                      "leads_untriaged",
                                      "source_check_partial",
                                      "cross_check_partial",
                                      "questions_failed",
                                      "coverage_partial",
                                      "coverage_no_answer",
                                      "excerpts_rejected"
                                    ]
                                  },
                                  "failed": {
                                    "type": "integer"
                                  },
                                  "total": {
                                    "type": "integer"
                                  },
                                  "undecided": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "error": {
                          "type": "object",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input. code is INVALID_GUIDE_ID or INVALID_VIEW (view other than summary or full)."
          },
          "401": {
            "description": "Unauthorized, invalid or missing API key."
          },
          "403": {
            "description": "code FEATURE_NOT_AVAILABLE. The feature is in test and reserved to invited accounts. Nothing was read. Do not retry."
          },
          "404": {
            "description": "code GUIDE_NOT_FOUND (guide unknown, deleted or not accessible, deliberately indistinguishable) or NO_ANALYSIS (the guide has no analysis yet, launch one with create_information_gain)."
          },
          "405": {
            "description": "Method not allowed."
          },
          "500": {
            "description": "Internal error."
          },
          "502": {
            "description": "code BACKEND_ERROR. The analysis service did not answer or answered unexpectedly. Retry later."
          },
          "503": {
            "description": "Temporary outage (database or analysis service). Retry after a short delay."
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      }
    },
    "/api/v1/information-gain/coverage": {
      "post": {
        "operationId": "recheckInformationGainCoverage",
        "x-serpmantics-access": "information-gain",
        "summary": "Recheck the guide text against the latest information gain analysis",
        "description": "Réservé : en test, accès sur invitation. Reserved feature, in test, access by invitation. Accounts without access receive 403 FEATURE_NOT_AVAILABLE.\n\nRecomputes only the guide column of the latest analysis (which facts the text states, in full or in part) without reading any competing page again. Call it after editing the text, to see the score and the missing facts move. It needs a done analysis; launch one with create_information_gain first.\n\nAsynchronous, about 10 seconds. Wait about 10 seconds, then call get_information_gain (view=summary) until status is done or failed.\n\nhtmlContent is optional. When given, this HTML is checked (send the text as displayed, without mark tags); when omitted, the text saved in the guide is used. htmlContent is limited to 2,000,000 bytes (UTF-8) through the API, and to about 1 MB through the MCP server (the size of the whole MCP request).\n\nCosts 5 tokens, charged when the recheck is launched (202, the body carries tokensCharged). Nothing is charged when the call is refused before the launch is attempted (invalid input, 403, 404, or 409 ANALYSIS_RUNNING and ANALYSIS_NOT_DONE read before charging). When the launch itself fails after the charge, whatever the status, the tokens are given back before the response and the body carries refunded (true when the credit is on the ledger, false when the refund failed and is logged for a manual replay). When an accepted recheck ends without a new verdict, its tokens are given back at the first read that sees it (get_information_gain), whose response then carries refunded and refundedTokens.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide (24 hexadecimal characters).",
                    "example": "507f1f77bcf86cd799439011"
                  },
                  "htmlContent": {
                    "type": "string",
                    "description": "Optional HTML of the article to check, without mark tags. Omit it to check the text saved in the guide. At most 2,000,000 bytes (UTF-8) through the API, about 1 MB through the MCP server."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Recheck launched. Poll pollUrl (or call get_information_gain) until status is done or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "analysisId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "pollUrl": {
                          "type": "string"
                        },
                        "tokensCharged": {
                          "type": "integer",
                          "description": "Tokens charged for this recheck.",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, or not enough tokens. code is INVALID_GUIDE_ID or INVALID_HTML_CONTENT (htmlContent given but not a non-empty string, or not valid Unicode, for instance a lone UTF-16 surrogate), fix the input and do not retry as is. code INSUFFICIENT_TOKENS means the balance is under the 5 tokens of a recheck, nothing was charged nor launched, and the body carries inviteToUpgrade true, upgradeType tokens and tokensRequired. code TOKENS_NOT_CONSUMED means the debit could not be confirmed, nothing was launched.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_GUIDE_ID",
                        "INVALID_HTML_CONTENT",
                        "INSUFFICIENT_TOKENS",
                        "TOKENS_NOT_CONSUMED"
                      ]
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    },
                    "tokensRequired": {
                      "type": "integer",
                      "example": 5
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Present when the tokens had been charged and the backend then refused the text. true when they were given back, false when the refund failed and is logged for a manual replay."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized, invalid or missing API key."
          },
          "403": {
            "description": "code FEATURE_NOT_AVAILABLE. The feature is in test and reserved to invited accounts. Nothing was read nor launched. Do not retry."
          },
          "404": {
            "description": "code GUIDE_NOT_FOUND (guide unknown, deleted or not accessible, deliberately indistinguishable) or NO_ANALYSIS (no analysis to recheck, launch one with create_information_gain)."
          },
          "405": {
            "description": "Method not allowed."
          },
          "409": {
            "description": "code ANALYSIS_RUNNING (an analysis or a recheck is already running, the body carries its analysisId and pollUrl, follow it) or ANALYSIS_NOT_DONE (the latest analysis failed, launch a new one). Nothing is charged when the conflict is read before charging. When it is only reported by the analysis service (concurrent launch), the tokens are given back and the body carries refunded."
          },
          "413": {
            "description": "code CONTENT_TOO_LARGE. htmlContent is over 2,000,000 bytes (UTF-8). Send a shorter text, do not retry as is. A JSON body over 4 MB is refused before being read, with a plain text 413."
          },
          "500": {
            "description": "Internal error. When the token debit could not be recorded, the tokens are given back and the body carries refunded."
          },
          "502": {
            "description": "code BACKEND_ERROR. The analysis service did not answer or answered unexpectedly. Retry later, unless the message says retrying will not help. The tokens charged for the call are given back and the body carries refunded."
          },
          "503": {
            "description": "Temporary outage (database or analysis service). Retry after a short delay."
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      }
    },
    "/api/v1/information-gain/insert": {
      "post": {
        "operationId": "insertInformationGainSuggestion",
        "x-serpmantics-access": "information-gain",
        "x-mcp-exclude": true,
        "summary": "Write one or two sentences that add a fact of the analysis to the guide text",
        "description": "Réservé : en test, accès sur invitation. Reserved feature, in test, access by invitation. Accounts without access receive 403 FEATURE_NOT_AVAILABLE.\n\nWrites one or two sentences, in the tone and language of the text, that bring a fact (kind fact, a missing or partial fact of the consensus) or a lead (kind lead, an information given by a single site) of the latest analysis into the guide text. Nothing is written into the guide, the caller inserts the returned paragraph itself, after the block whose text is anchor (at the end of the text when anchor is null). Competitor brands, site names and prices are generalized. An information specific to one site, with nothing general to draw from it, is refused (422 NOT_TRANSFERABLE, tokens given back).\n\nstatement must be the exact statement of a fact or lead of the latest analysis, as returned by get_information_gain. htmlContent is optional. When given, the anchor is chosen in this HTML (send the text as displayed, without mark tags); when omitted, the text saved in the guide is used. Synchronous, a few seconds.\n\nCosts 3 tokens, charged before the sentences are written (200, the body carries tokensCharged). Nothing is charged when the call is refused before (invalid input, 403, 404, or 409 read before charging). When the writing fails after the charge, whatever the status, the tokens are given back before the response and the body carries refunded (true when the credit is on the ledger, false when the refund failed and is logged for a manual replay).\n\nSend an Idempotency-Key header to retry safely after a timeout or a lost connection. With the same key and the same guideId, kind and statement, a call that succeeded (200) in the last 24 hours is answered again as is, with the header Idempotent-Replayed true, without charging nor writing again (tokensCharged keeps the value of the original charge). A call still in progress gives 409 REQUEST_IN_PROGRESS, another guideId, kind or statement gives 422 IDEMPOTENCY_KEY_REUSED, and a call that failed frees the key (nothing was charged, the retry is processed as a new call). Keys are scoped to the account.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "description": "Optional. 1 to 255 printable ASCII characters, without spaces (a UUID is a good choice). Use a new key for each new suggestion, and the same key to retry the same one. Invalid value gives 400 INVALID_IDEMPOTENCY_KEY, nothing charged.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "example": "5f0c6a7e-2b1d-4f7a-9c1e-3d2b8a6f4e10"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId",
                  "kind",
                  "statement"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide (24 hexadecimal characters).",
                    "example": "507f1f77bcf86cd799439011"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "fact",
                      "lead"
                    ],
                    "description": "fact for a missing or partial fact of the consensus, lead for an information given by a single site."
                  },
                  "statement": {
                    "type": "string",
                    "description": "Exact statement of the fact or lead, as returned by get_information_gain. At most 300 characters (Unicode code points)."
                  },
                  "htmlContent": {
                    "type": "string",
                    "description": "Optional HTML of the article, without mark tags. Omit it to use the text saved in the guide. At most 2,000,000 bytes (UTF-8)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sentences written. Insert html after the block whose text is anchor.",
            "headers": {
              "Idempotent-Replayed": {
                "description": "true when this response replays a previous successful call made with the same Idempotency-Key (nothing charged nor written again). Absent otherwise.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "html": {
                          "type": "string",
                          "description": "The sentences, HTML-escaped, in one p element."
                        },
                        "text": {
                          "type": "string",
                          "description": "The same sentences, as plain text."
                        },
                        "mode": {
                          "type": "string",
                          "enum": [
                            "after"
                          ],
                          "description": "Always after, the paragraph is added and never replaces text."
                        },
                        "anchor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Text of the block after which to insert (one whole line of the text, one line per heading, paragraph, list item or cell), null to insert at the end of the text."
                        },
                        "block": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rank of that line among the non-empty lines of the text, from 1, to tell apart two blocks with the same text. null when anchor is null.",
                          "example": 4
                        },
                        "tokensCharged": {
                          "type": "integer",
                          "description": "Tokens charged for this call.",
                          "example": 3
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, or not enough tokens. code is INVALID_GUIDE_ID, INVALID_KIND, INVALID_STATEMENT, INVALID_HTML_CONTENT or INVALID_IDEMPOTENCY_KEY, fix the input and do not retry as is. code INSUFFICIENT_TOKENS means the balance is under the 3 tokens of a suggestion, nothing was charged, and the body carries inviteToUpgrade true, upgradeType tokens and tokensRequired. code TOKENS_NOT_CONSUMED means the debit could not be confirmed, nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_GUIDE_ID",
                        "INVALID_KIND",
                        "INVALID_STATEMENT",
                        "INVALID_HTML_CONTENT",
                        "INVALID_IDEMPOTENCY_KEY",
                        "INSUFFICIENT_TOKENS",
                        "TOKENS_NOT_CONSUMED"
                      ]
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    },
                    "tokensRequired": {
                      "type": "integer",
                      "example": 3
                    },
                    "refunded": {
                      "type": "boolean",
                      "description": "Present when the tokens had been charged and the analysis service then refused the text. true when they were given back, false when the refund failed and is logged for a manual replay."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized, invalid or missing API key."
          },
          "403": {
            "description": "code FEATURE_NOT_AVAILABLE. The feature is in test and reserved to invited accounts. Nothing was read nor charged. Do not retry."
          },
          "404": {
            "description": "code GUIDE_NOT_FOUND (guide unknown, deleted or not accessible, deliberately indistinguishable), NO_ANALYSIS (launch an analysis first) or FACT_NOT_FOUND (the statement is not in the latest analysis, read it again)."
          },
          "405": {
            "description": "Method not allowed."
          },
          "409": {
            "description": "code ANALYSIS_RUNNING (an analysis or a recheck is running, wait until it is done) or ANALYSIS_NOT_DONE (the latest analysis failed, launch a new one). Nothing is charged when the conflict is read before charging; otherwise the tokens are given back and the body carries refunded. code REQUEST_IN_PROGRESS means a call with the same Idempotency-Key is still running, nothing more was charged, retry in a moment with the same key."
          },
          "413": {
            "description": "code CONTENT_TOO_LARGE. htmlContent (or the saved text) is over 2,000,000 bytes (UTF-8). A JSON body over 4 MB is refused before being read, with a plain text 413."
          },
          "422": {
            "description": "code NOT_TRANSFERABLE. The information is specific to one competitor site (its own price, catalogue, address), there is nothing general to add from it. The tokens charged for the call are given back and the body carries refunded. Do not retry with the same statement. code IDEMPOTENCY_KEY_REUSED means the Idempotency-Key was already used for another guideId, kind or statement in the last 24 hours, nothing was charged, send a new key."
          },
          "500": {
            "description": "Internal error. When the token debit could not be recorded, the tokens are given back and the body carries refunded."
          },
          "502": {
            "description": "code BACKEND_ERROR. The analysis service did not answer, or no model could write a valid suggestion. Retry later. The tokens charged for the call are given back and the body carries refunded."
          },
          "503": {
            "description": "Temporary outage (database or analysis service). Retry after a short delay. code BACKEND_SLOW means the reads before writing took too long to leave time for the writing: nothing was charged."
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      }
    },
    "/api/v1/intent": {
      "post": {
        "summary": "Generate search intent analysis for a guide",
        "description": "Analyzes search intent for a guide and optionally analyzes provided content against that intent. \n**Note:** This endpoint uses tokens (check /api/v1/tokens-usage for the cost, might vary).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide",
                    "example": "507f1f77bcf86cd799439011"
                  },
                  "content": {
                    "type": "string",
                    "description": "Optional content to analyze against the intent (only after intent analysis was created)",
                    "example": "<h1>Title</h1><p>This is my article HTML content to analyze for search intent...</p>"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Intent analysis initiated or completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Invalid guideId, insufficient tokens, or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't own the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get search intent analysis for a guide",
        "description": "Retrieves existing intent analysis results for a guide",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Intent analysis retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "isReady": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "intents": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "title": {
                                "type": "string",
                                "description": "Title of the intent group",
                                "example": "Titre de l'intention #1"
                              },
                              "intents": {
                                "type": "array",
                                "description": "List of user intent questions in this group",
                                "items": {
                                  "type": "string"
                                },
                                "example": [
                                  "Question #1 descriptive de l'intention #1",
                                  "Question #2 descriptive de l'intention #1",
                                  "..."
                                ]
                              },
                              "answering_results": {
                                "type": "array",
                                "description": "Indices of answer results relevant to this intent group",
                                "items": {
                                  "type": "integer"
                                },
                                "example": [
                                  1,
                                  3,
                                  4,
                                  6,
                                  8,
                                  9,
                                  10
                                ]
                              }
                            }
                          }
                        },
                        "status": {
                          "type": "string",
                          "description": "Status of the intent analysis",
                          "example": "done"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key or user doesn't have access to the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "delete": {
        "summary": "Delete generated intent analysis for a guide",
        "description": "Removes previously generated intent analysis for the given guide.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide"
          }
        ],
        "responses": {
          "200": {
            "description": "Intent successfully deleted"
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden - User does not own the guide"
          },
          "404": {
            "description": "Guide not found or nothing to delete"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. The guide itself can still be deleted with DELETE /api/v1/guide."
          }
        },
        "tags": [
          "v1",
          "Intent"
        ]
      }
    },
    "/api/v1/internal-links": {
      "post": {
        "summary": "Generate internal linking suggestions for a guide",
        "description": "Analyzes a guide and generates internal linking suggestions based on content analysis. \n**Note:** This endpoint uses tokens (check /api/v1/tokens-usage for the cost, might vary).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide",
                    "example": "507f1f77bcf86cd799439011"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Internal links generation initiated or completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Missing guideId, insufficient tokens or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "403": {
            "description": "Not authorized to access this guide (no token is consumed)"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get internal linking suggestions for a guide",
        "description": "Retrieves existing internal linking suggestions for a guide",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Internal linking suggestions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "internalLinks": {
                          "type": "object",
                          "properties": {
                            "status": {
                              "type": "string",
                              "example": "done"
                            },
                            "serp": {
                              "type": "object",
                              "properties": {
                                "status": {
                                  "type": "string",
                                  "example": "done"
                                },
                                "incomingLinks": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "url": {
                                        "type": "string",
                                        "example": "https://www.domaine.com/url1"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "group": {
                              "type": "object",
                              "properties": {
                                "status": {
                                  "type": "string",
                                  "example": "done"
                                },
                                "incomingLinks": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "from": {
                                        "type": "object",
                                        "properties": {
                                          "guideId": {
                                            "type": "string",
                                            "example": "68d4ec2a85ca486f278c4941"
                                          },
                                          "query": {
                                            "type": "string",
                                            "example": "Requête du guide #1"
                                          }
                                        }
                                      }
                                    }
                                  }
                                },
                                "outgoingLinks": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "to": {
                                        "type": "object",
                                        "properties": {
                                          "guideId": {
                                            "type": "string",
                                            "example": "68d4ec2a85ca486f278c4941"
                                          },
                                          "query": {
                                            "type": "string",
                                            "example": "Requête du guide #1"
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "403": {
            "description": "Not authorized to access this guide"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "delete": {
        "summary": "Delete generated internal-links suggestions for a guide",
        "description": "Removes previously generated internal-links suggestions for the given guide.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide"
          }
        ],
        "responses": {
          "200": {
            "description": "Internal-links suggestions successfully deleted"
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden - User does not own the guide"
          },
          "404": {
            "description": "Guide not found or nothing to delete"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. The guide itself can still be deleted with DELETE /api/v1/guide."
          }
        },
        "tags": [
          "v1",
          "Internal Links"
        ]
      }
    },
    "/api/v1/meta": {
      "post": {
        "summary": "Generate SEO meta titles and descriptions for a guide",
        "description": "Generates optimized meta titles and descriptions based on guide content analysis.\n**Note:** This endpoint uses tokens (check /api/v1/tokens-usage for the cost, might vary).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Meta generation initiated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Missing guideId, insufficient tokens, or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "403": {
            "description": "Forbidden - User doesn't own the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "429": {
            "description": "Rate limit reached, try again later"
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get generated meta titles and descriptions for a guide",
        "description": "Retrieves generated SEO meta titles and descriptions for a guide",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Meta titles and descriptions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "isReady": {
                      "type": "boolean",
                      "description": "Whether the generation is complete",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "titles": {
                          "type": "array",
                          "description": "List of generated meta titles",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Titre #1",
                            "Titre #2",
                            "..."
                          ]
                        },
                        "descriptions": {
                          "type": "array",
                          "description": "List of generated meta descriptions",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Description #1",
                            "Description #2",
                            "..."
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "403": {
            "description": "Forbidden - User doesn't own the guide"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "delete": {
        "summary": "Delete generated meta for a guide",
        "description": "Removes previously generated meta titles/descriptions for the given guide.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide"
          }
        ],
        "responses": {
          "200": {
            "description": "Meta successfully deleted"
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden - User does not own the guide"
          },
          "404": {
            "description": "Guide not found or nothing to delete"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. The guide itself can still be deleted with DELETE /api/v1/guide."
          }
        },
        "tags": [
          "v1",
          "Meta"
        ]
      }
    },
    "/api/v1/outline": {
      "post": {
        "summary": "Generate content outline for a guide",
        "description": "Generates a content outline based on SERP analysis for a guide.\n**Note:** This endpoint uses tokens (check /api/v1/tokens-usage for the cost, might vary).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Outline generation initiated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Invalid guideId, insufficient tokens, or failed to consume tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Insufficient tokens"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. No token is consumed by this refusal."
          },
          "429": {
            "description": "Rate limit reached or free plan limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Free plan limit reached (1), please upgrade your plan"
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "plan"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "get": {
        "summary": "Get generated page structure for a guide",
        "description": "Retrieves the generated page structure (flat heading list) for a guide.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide",
            "example": "507f1f77bcf86cd799439011"
          }
        ],
        "responses": {
          "200": {
            "description": "Page structure retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "isReady": {
                      "type": "boolean",
                      "description": "Whether the generation is complete",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "pageStructure": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "hn": {
                                "type": "integer",
                                "description": "Heading level (1-6)",
                                "example": 2
                              },
                              "text": {
                                "type": "string",
                                "description": "Heading text",
                                "example": "Introduction au SEO"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - Invalid API key"
          },
          "404": {
            "description": "Page structure not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Page structure not found"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword."
          },
          "500": {
            "description": "Internal server error"
          }
        },
        "tags": [
          "v1",
          "In-guide features"
        ]
      },
      "delete": {
        "summary": "Delete generated outline for a guide",
        "description": "Removes previously generated outline data for the given guide.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "guideId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID of the guide"
          }
        ],
        "responses": {
          "200": {
            "description": "Outline successfully deleted"
          },
          "400": {
            "description": "Missing guideId"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden - User does not own the guide"
          },
          "404": {
            "description": "Guide not found or nothing to delete"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. The guide itself can still be deleted with DELETE /api/v1/guide."
          }
        },
        "tags": [
          "v1",
          "Outline"
        ]
      }
    },
    "/api/v1/tokens-usage": {
      "get": {
        "summary": "Get API token usage costs for different endpoints",
        "description": "Returns the number of tokens required for each API endpoint operation.\n\nNote: `eeatCompetitorsTokensCostPerCompetitor` is a **per-competitor** cost.\nThe total cost of POST /api/v1/eeat-competitors equals this value × the number of\ncompetitors analyzed (deduplicated URLs in the guide's top-10 SERP, capped at 10).\n",
        "tags": [
          "v1",
          "Usage"
        ],
        "responses": {
          "200": {
            "description": "Token usage details for API endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "apiTokensUsage": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "number"
                      },
                      "example": {
                        "internalLinksGenerationTokensCost": 99,
                        "outlineGenerationTokensCost": 99,
                        "intentGenerationTokensCost": 99,
                        "metaGenerationTokensCost": 99
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Method not allowed"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Internal server error"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/citation-check": {
      "post": {
        "operationId": "createCitationCheck",
        "summary": "Test whether ChatGPT, Perplexity and Google AI Overview cite a website for one question (free tool)",
        "description": "Asks one question to three AI answer engines (ChatGPT with web search, Perplexity, Google AI\nOverview) in the chosen market, and reports for each engine whether your domain is cited as a\nsource (`cited`, a citation on your registrable domain or one of its subdomains), whether your\nbrand is named in the answer (`mentioned`, which is NOT a citation), which of your pages are cited,\nan excerpt of the answer, its sources, and the domains cited instead of yours.\n\n**One answer, on one day.** This is a snapshot, not a lasting measure: AI answers change from one week\nto the next, and between two calls. Run the test again to compare.\n\n**Asynchronous, poll the result.** This call only starts the test and answers HTTP 202 with a\n`checkId` and a `pollUrl`. Call `GET /api/v1/tools/citation-check?checkId=...` every 3 seconds\nuntil `status` is `done` or `failed`; a test usually takes less than 2 minutes. MCP clients must\ncall the `get_citation_check` tool again until the status is terminal.\n\n**Identical questions are reused for 24 hours.** The same question (case and spaces ignored) in\nthe same market, asked less than 24 hours ago, returns the engine answers already collected,\nrecomputed for your domain: HTTP 200 with `reused` true and `status` already `done`. Nothing is\npaid for again, and your daily quota is given back.\n\n**Domain.** Send a domain name or any URL of the site: it is reduced to its registrable domain\n(`https://www.example.co.uk/blog/` becomes `example.co.uk`). IP addresses, local names and unknown\nsuffixes return 400 INVALID_DOMAIN.\n\n**Tiers, per UTC day.**\n- Without an API key, free anonymous tier: 1 test per day per IP address.\n- Signed-in account, or API key of an account whose plan includes API access: 10 tests per day.\n\nThe API key of an account whose plan does NOT include API access does not authenticate, and the\ncall returns 401. Remove the `Authorization` header to use the free anonymous tier instead.\n\nQuota headers: `X-RateLimit-Limit`, `X-RateLimit-Used`, `X-RateLimit-Remaining`,\n`X-RateLimit-Reset` (Unix time of the next UTC midnight). `Retry-After` is set on 429 and 503.\nA 502 or 503 answer gives the consumed quota back, and so does a 429 FREE_CAPACITY_REACHED\nand a 200 reused answer; SERVICE_UNAVAILABLE consumes none.\n\n**Request format.** Send the body as JSON with `Content-Type: application/json`: any other\ncontent type returns 415. Without an `Authorization` header, a request sent by a web page of\nanother site (an `Origin` other than the SERPmantics app, or `Sec-Fetch-Site: cross-site`)\nreturns 403. Server-to-server calls send no `Origin` header and are not affected.\n",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain",
                  "question",
                  "market"
                ],
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "Domain to look for in the answers, or any URL of the site. Reduced to the registrable domain.",
                    "example": "serpmantics.com"
                  },
                  "question": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 200,
                    "description": "The question a prospect would ask an AI assistant, 10 to 200 characters once spaces are collapsed.",
                    "example": "Quel est le meilleur outil d'optimisation sémantique pour le SEO ?"
                  },
                  "market": {
                    "type": "string",
                    "enum": [
                      "fr-FR",
                      "en-US",
                      "es-ES",
                      "de-DE",
                      "it-IT"
                    ],
                    "description": "Language and country in which the engines are asked.",
                    "example": "fr-FR"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The same question in the same market was answered less than 24 hours ago: the answers are\nreused and recomputed for your domain. Nothing was paid for again, the daily quota is given\nback, and the test is already done: read it at pollUrl, no polling loop needed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "checkId": {
                          "type": "string",
                          "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "done"
                          ]
                        },
                        "reused": {
                          "type": "boolean",
                          "example": true
                        },
                        "pollUrl": {
                          "type": "string",
                          "example": "https://app.serpmantics.com/api/v1/tools/citation-check?checkId=Q3v9Zk2LmN8pR5tW1xY7aB"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Test accepted. Poll pollUrl until status is done or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "checkId": {
                          "type": "string",
                          "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "pollUrl": {
                          "type": "string",
                          "example": "https://app.serpmantics.com/api/v1/tools/citation-check?checkId=Q3v9Zk2LmN8pR5tW1xY7aB"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, codes INVALID_BODY, INVALID_DOMAIN, INVALID_QUESTION, INVALID_MARKET, INVALID_INPUT.\nNo quota is consumed. details.field names the faulty field; INVALID_QUESTION gives details.reason\n(TOO_SHORT, TOO_LONG, EMPTY, CONTROL_CHARACTERS) with details.min and details.max. INVALID_INPUT means\nthe test service itself rejected the request, and the quota is given back.\n"
          },
          "401": {
            "description": "UNAUTHORIZED, the Authorization header does not authenticate (unknown key, or key of a plan without API access). Remove the header to use the free anonymous tier."
          },
          "403": {
            "description": "CROSS_SITE_FORBIDDEN, request without an Authorization header sent by a web page of another site (Origin other than the SERPmantics app, or Sec-Fetch-Site cross-site). Refused before any quota is consumed. Server-to-server calls send no Origin header and are not affected."
          },
          "405": {
            "description": "METHOD_NOT_ALLOWED"
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE, the body is not sent with Content-Type application/json (a charset parameter is accepted). Refused before authentication and before any quota is consumed."
          },
          "429": {
            "description": "RATE_LIMITED (daily limit of your tier, see details.reset_at and details.signup_url or details.pricing_url) or FREE_CAPACITY_REACHED (shared capacity of the day used up, see details.tier and details.reset_at). There is one shared daily capacity for all anonymous visitors together and another for all accounts together, so signing in or upgrading does not lift it for an account; details.signup_url is only sent to the anonymous tier. Retry-After is set. FREE_CAPACITY_REACHED gives your quota back. RATE_LIMITED means your own quota is already spent, and the refusal itself consumes nothing more."
          },
          "502": {
            "description": "UPSTREAM_ERROR, the test service did not answer. The quota is given back."
          },
          "503": {
            "description": "SERVICE_BUSY, too many tests in progress: retry after Retry-After seconds, the quota is given back. SERVICE_UNAVAILABLE, the daily counter itself is unavailable: the request is refused before anything is created, no quota is consumed, and Retry-After says when to try again. Reading a test is not affected.\n"
          }
        },
        "tags": [
          "v1",
          "Free tools"
        ]
      },
      "get": {
        "operationId": "getCitationCheck",
        "summary": "Read an AI citation test, poll until it is done",
        "description": "Returns the test started by `POST /api/v1/tools/citation-check`. While `status` is `pending` or\n`running`, call again every 3 seconds: each engine fills in as soon as it has answered. When\n`status` is `done`, at least one engine answered; `summary.citedBy` engines cite your domain out\nof `summary.enginesAnswered` engines that answered. When `status` is `failed`, `failureCode` says\nwhy (NO_ENGINE_ANSWERED, TIMEOUT, INTERNAL).\n\nPer engine: `cited` is true when a source of the answer is on your registrable domain or one of\nits subdomains, and `citedUrls` lists those pages; `mentioned` is true when your brand name appears\nin the answer text, which is NOT a citation. `status` no_answer means the engine showed no answer\nfor this question (Google often shows no AI Overview), which is not a failure.\n\nNo quota is consumed. The `checkId` is random: whoever holds it can read the test, which is what\nmakes share links work. Tests expire after 30 days. Reads that are not attributed to an account are\ncapped at 1000 per IP address and per UTC day, far above any legitimate polling: crossing it returns\n429 READ_RATE_LIMITED with `Retry-After`.\n",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "checkId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The checkId returned by the creation call (22 characters).",
            "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
          }
        ],
        "responses": {
          "200": {
            "description": "The test, in progress or finished.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "checkId",
                        "status",
                        "domain",
                        "question",
                        "market",
                        "createdAt",
                        "updatedAt",
                        "engines",
                        "summary"
                      ],
                      "properties": {
                        "checkId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "running",
                            "done",
                            "failed"
                          ]
                        },
                        "failureCode": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "NO_ENGINE_ANSWERED",
                            "TIMEOUT",
                            "INTERNAL",
                            null
                          ],
                          "description": "Why the test failed, absent or null unless status is failed."
                        },
                        "domain": {
                          "type": "string",
                          "description": "The registrable domain that was tested."
                        },
                        "question": {
                          "type": "string"
                        },
                        "market": {
                          "type": "string",
                          "enum": [
                            "fr-FR",
                            "en-US",
                            "es-ES",
                            "de-DE",
                            "it-IT"
                          ]
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "engines": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "engine": {
                                "type": "string",
                                "enum": [
                                  "chatgpt",
                                  "perplexity",
                                  "google_ai_overview"
                                ]
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "done",
                                  "no_answer",
                                  "failed"
                                ]
                              },
                              "answerExcerpt": {
                                "type": "string",
                                "nullable": true,
                                "maxLength": 1200
                              },
                              "citations": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "position": {
                                      "type": "integer"
                                    },
                                    "url": {
                                      "type": "string"
                                    },
                                    "domain": {
                                      "type": "string"
                                    },
                                    "title": {
                                      "type": "string",
                                      "nullable": true
                                    }
                                  }
                                }
                              },
                              "cited": {
                                "type": "boolean",
                                "description": "A source of the answer is on the tested domain or one of its subdomains."
                              },
                              "citedUrls": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "mentioned": {
                                "type": "boolean",
                                "description": "The brand name appears in the answer text. Being named is not being cited as a source."
                              }
                            }
                          }
                        },
                        "summary": {
                          "type": "object",
                          "properties": {
                            "citedBy": {
                              "type": "integer"
                            },
                            "enginesAnswered": {
                              "type": "integer"
                            },
                            "competitors": {
                              "type": "array",
                              "maxItems": 10,
                              "description": "Domains cited instead of yours, across engines.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "domain": {
                                    "type": "string"
                                  },
                                  "engines": {
                                    "type": "array",
                                    "items": {
                                      "type": "string",
                                      "enum": [
                                        "chatgpt",
                                        "perplexity",
                                        "google_ai_overview"
                                      ]
                                    }
                                  },
                                  "count": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_CHECK_ID, checkId is missing or malformed."
          },
          "404": {
            "description": "NOT_FOUND, no test matches this checkId."
          },
          "429": {
            "description": "READ_RATE_LIMITED, anti-abuse ceiling on reads that are not attributed to an account, 1000 per IP address and per UTC day. It is not a usage quota. `details.limit`, `Retry-After` and `details.reset_at` describe it. Reads authenticated with an API key are not subject to it."
          },
          "502": {
            "description": "UPSTREAM_ERROR, the test service did not answer."
          }
        },
        "tags": [
          "v1",
          "Free tools"
        ]
      }
    },
    "/api/v1/tools/persona-brief/discover": {
      "post": {
        "operationId": "discoverPersonaBriefPages",
        "summary": "Find the representative pages of a website from its sitemaps, before generating a persona brief (free tool)",
        "description": "Reads the sitemaps of a website (declared in robots.txt, otherwise /sitemap.xml,\n/sitemap_index.xml and /wp-sitemap.xml) and returns up to 200 URLs of the same host, editorial\npages first. The first URLs, as many as the URL limit of your tier, are flagged `suggested`:\nsend them in `urls` to `POST /api/v1/tools/persona-brief`.\n\nSame tiers as the creation call, with their own daily counter per UTC day: 20 discoveries per\nday per IP address without an API key, 50 for a signed-in account, 200 with the API key of an\naccount whose plan includes API access. A key whose plan does not include API access returns\n401: remove the `Authorization` header to use the free anonymous tier.\n\nQuota headers: `X-RateLimit-Limit`, `X-RateLimit-Used`, `X-RateLimit-Remaining`,\n`X-RateLimit-Reset`. `Retry-After` is set on 429 and 503. A 400, 502 or 503 answer gives the\nconsumed quota back; SERVICE_UNAVAILABLE consumes none.\n\n**Request format.** Send the body as JSON with `Content-Type: application/json`: any other\ncontent type returns 415. Without an `Authorization` header, a request sent by a web page of\nanother site (an `Origin` other than the SERPmantics app, or `Sec-Fetch-Site: cross-site`)\nreturns 403. Server-to-server calls send no `Origin` header and are not affected.\n",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site"
                ],
                "properties": {
                  "site": {
                    "type": "string",
                    "description": "URL or domain of the website. A bare domain is completed with https.",
                    "example": "https://serpmantics.com/"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pages found in the sitemaps.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "site": {
                          "type": "string"
                        },
                        "sitemapsRead": {
                          "type": "integer"
                        },
                        "totalFound": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "urls": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string"
                              },
                              "lastmod": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "Last modification date read in the sitemap, ISO 8601 UTC, null when absent or unreadable.",
                                "example": "2026-09-14T00:00:00Z"
                              },
                              "suggested": {
                                "type": "boolean"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_INPUT or INVALID_URL, the site is missing or is not an http(s) URL (details.reason NOT_HTTP_URL or\nTOO_LONG), refused before any quota is consumed. INVALID_URL with details.reason UNRESOLVABLE_HOST when the\ndomain does not resolve, and BLOCKED_ADDRESS when the address is private, local or blocked: in both cases\nnothing was read and the quota is given back.\n"
          },
          "401": {
            "description": "UNAUTHORIZED, the Authorization header does not authenticate. Remove it to use the free anonymous tier."
          },
          "403": {
            "description": "CROSS_SITE_FORBIDDEN, request without an Authorization header sent by a web page of another site (Origin other than the SERPmantics app, or Sec-Fetch-Site cross-site). Refused before any quota is consumed. Server-to-server calls send no Origin header and are not affected."
          },
          "405": {
            "description": "METHOD_NOT_ALLOWED"
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE, the body is not sent with Content-Type application/json (a charset parameter is accepted). Refused before authentication and before any quota is consumed."
          },
          "422": {
            "description": "NO_SITEMAP, no sitemap was found. The site was reached and its robots.txt read, so the discovery stays counted. Send the URLs of a few articles directly to the creation call."
          },
          "429": {
            "description": "RATE_LIMITED, daily discovery limit of your tier reached. See details.reset_at. Retry-After is set."
          },
          "502": {
            "description": "UPSTREAM_ERROR, the service did not answer. The quota is given back."
          },
          "503": {
            "description": "SERVICE_BUSY, too many site discoveries are running: retry after Retry-After seconds, the quota is given back. SERVICE_UNAVAILABLE, the daily counter itself is unavailable: the request is refused before anything is read, no quota is consumed, and Retry-After says when to try again.\n"
          }
        },
        "tags": [
          "v1",
          "Free tools"
        ]
      }
    },
    "/api/v1/tools/persona-brief": {
      "post": {
        "operationId": "createPersonaBrief",
        "summary": "Generate an AI persona and brand voice brief from the pages of a website (free tool)",
        "description": "Reads public pages of a website and produces an editorial persona and writing style brief:\nwho writes, for whom, tone on 6 axes, vocabulary, structure, do and don't lists, verified\nquotes from the pages, style metrics measured on the text, and a ready-to-paste writing\nprompt for any AI writing tool (`brief.writingPrompt`, plus `brief.writingPromptCompact`\nof at most 1500 characters).\n\n**Asynchronous, poll the result.** This call only starts the job and answers HTTP 202 with a\n`briefId` and a `pollUrl`. Call `GET /api/v1/tools/persona-brief?briefId=...` every 3 seconds\nuntil `status` is `done` or `failed`. A brief takes about 80 seconds plus 5 seconds per page,\nrounded up to the half minute: about 2 min for 3 pages, 2 min 30 s for 10 pages.\nMCP clients must call the `get_persona_brief` tool again until the status is terminal.\n\n**Identical requests are reused for 72 hours.** The same set of URLs, in any order, with the\nsame `outputLanguage` and the same `brandName`, returns the brief already computed instead of\ncomputing it again: HTTP 200 with `reused` true, `status` already `done`, the original\n`briefId` and `reusedCreatedAt`, the date of the brief it reuses. Nothing is created, and your\ndaily quota is given back, so a reused answer costs you nothing. The pages of a reused brief\nare listed in the order of the original request, not in the order you just sent. Send\n`force` true to skip the lookup and compute a new brief, which answers 202 and consumes quota.\n\n**Addresses.** Duplicate URLs, compared once normalized, are read once. A private, local or\nblocked address is not refused by this call: the brief is created, and that page ends with\n`status` failed and `failureReason` BLOCKED_ADDRESS (the brief fails with NO_USABLE_PAGE when\nno other page could be read).\n\n**Tiers, per UTC day.** Every tier is capped at 10 URLs per brief since 21 September 2026.\n- Without an API key, free anonymous tier: 3 briefs per day per IP address, 10 URLs per brief.\n- Signed-in account on the web interface, plan without API access: 10 briefs per day, 10 URLs per brief.\n- API key of an account whose plan includes API access, pro tier: 50 briefs per day, 10 URLs per brief.\n\nThe API key of an account whose plan does NOT include API access does not authenticate, and the\ncall returns 401. Remove the `Authorization` header to use the free anonymous tier instead.\n\nQuota headers: `X-RateLimit-Limit`, `X-RateLimit-Used`, `X-RateLimit-Remaining`,\n`X-RateLimit-Reset` (Unix time of the next UTC midnight). `Retry-After` is set on 429 and 503.\nA 502 or 503 answer gives the consumed quota back, and so does a 429 FREE_CAPACITY_REACHED\nand a 200 reused answer; SERVICE_UNAVAILABLE consumes none.\n\n**Request format.** Send the body as JSON with `Content-Type: application/json`: any other\ncontent type returns 415. Without an `Authorization` header, a request sent by a web page of\nanother site (an `Origin` other than the SERPmantics app, or `Sec-Fetch-Site: cross-site`)\nreturns 403. Server-to-server calls send no `Origin` header and are not affected.\n",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "urls"
                ],
                "properties": {
                  "urls": {
                    "type": "array",
                    "description": "Public http(s) URLs of representative pages of one website, 5 to 10 articles give the best brief. At most 10 URLs, whatever the tier, each of at most 2048 characters.",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "https://serpmantics.com/blog/first-article/",
                      "https://serpmantics.com/blog/second-article/"
                    ]
                  },
                  "outputLanguage": {
                    "type": "string",
                    "enum": [
                      "fr",
                      "en",
                      "es",
                      "de",
                      "it"
                    ],
                    "description": "Language in which the brief is written. When omitted, the service chooses it."
                  },
                  "brandName": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Optional brand name, on a single line, used in the brief."
                  },
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "True to compute a new brief even when an identical one exists, which answers 202 and consumes quota. Any value that is not a boolean returns 400 INVALID_BODY."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An identical brief computed less than 72 hours ago was reused. Nothing was created, the daily\nquota is given back, and the brief is already done: read it at pollUrl, no polling loop needed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "briefId": {
                          "type": "string",
                          "description": "Identifier of the EXISTING brief, not a new one.",
                          "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "done"
                          ],
                          "example": "done"
                        },
                        "reused": {
                          "type": "boolean",
                          "example": true
                        },
                        "reusedCreatedAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "ISO 8601, UTC, creation date of the brief being reused.",
                          "example": "2026-09-19T17:19:44.000Z"
                        },
                        "pollUrl": {
                          "type": "string",
                          "example": "https://app.serpmantics.com/api/v1/tools/persona-brief?briefId=Q3v9Zk2LmN8pR5tW1xY7aB"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Brief accepted. Poll pollUrl until status is done or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "briefId": {
                          "type": "string",
                          "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "pollUrl": {
                          "type": "string",
                          "example": "https://app.serpmantics.com/api/v1/tools/persona-brief?briefId=Q3v9Zk2LmN8pR5tW1xY7aB"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, codes INVALID_INPUT, TOO_MANY_URLS, INVALID_URL, INVALID_BRAND_NAME, INVALID_OUTPUT_LANGUAGE, INVALID_BODY.\nNo quota is consumed. INVALID_BODY names the faulty field in details.field, and is what a\nnon-boolean `force` returns. TOO_MANY_URLS counts the array as sent, duplicates included (details.max,\ndetails.received). INVALID_URL names the faulty URL in details.index and details.url, and gives\ndetails.reason, NOT_HTTP_URL or TOO_LONG (more than 2048 characters once normalized, each\nnon-ASCII character being percent-encoded into several).\n"
          },
          "401": {
            "description": "UNAUTHORIZED, the Authorization header does not authenticate (unknown key, or key of a plan without API access). Remove the header to use the free anonymous tier."
          },
          "403": {
            "description": "CROSS_SITE_FORBIDDEN, request without an Authorization header sent by a web page of another site (Origin other than the SERPmantics app, or Sec-Fetch-Site cross-site). Refused before any quota is consumed. Server-to-server calls send no Origin header and are not affected."
          },
          "405": {
            "description": "METHOD_NOT_ALLOWED"
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE, the body is not sent with Content-Type application/json (a charset parameter is accepted). Refused before authentication and before any quota is consumed."
          },
          "429": {
            "description": "RATE_LIMITED (daily limit of your tier, see details.reset_at and details.signup_url or details.pricing_url) or FREE_CAPACITY_REACHED (free anonymous capacity of the day used up, see details.reset_at and details.signup_url). Retry-After is set. FREE_CAPACITY_REACHED gives your quota back, the shared free capacity is what ran out and not your own limit, so your daily counter is not decremented. RATE_LIMITED means your own quota is already spent, and the refusal itself consumes nothing more."
          },
          "502": {
            "description": "UPSTREAM_ERROR, the generation service did not answer. The quota is given back."
          },
          "503": {
            "description": "SERVICE_BUSY, too many briefs in progress: retry after Retry-After seconds, the quota is given back. SERVICE_UNAVAILABLE, the daily counter itself is unavailable: the request is refused before anything is created, no quota is consumed, and Retry-After says when to try again. Reading a brief is not affected.\n"
          }
        },
        "tags": [
          "v1",
          "Free tools"
        ]
      },
      "get": {
        "operationId": "getPersonaBrief",
        "summary": "Read an AI persona and brand voice brief, poll until it is done",
        "description": "Returns the brief started by `POST /api/v1/tools/persona-brief`. While `status` is `pending`,\n`fetching`, `analyzing` or `synthesizing`, call again every 3 seconds: `progress` and `pages`\nshow what has been read so far. When `status` is `done`, `brief` holds the structured brief,\n`brief.writingPrompt` the ready-to-paste prompt and `markdown` the full document. When `status`\nis `failed`, `failureReason` says why (NO_USABLE_PAGE, LLM_UNAVAILABLE, TIMEOUT, INTERNAL).\n\nA page with a `failureReason` did not contribute to the brief. ANALYSIS_FAILED is the only one\non a page that was read: it keeps its `title` and `wordCount`, stays counted in\n`progress.pagesFetched` and never enters `progress.pagesAnalyzed` nor `metrics`.\n\nNo quota is consumed. The `briefId` is random: whoever holds it can read the brief, which is\nwhat makes share links work. Briefs expire after 90 days. Reads that are not attributed to an\naccount are subject to three anti-abuse counters, all per IP address and per UTC day:\nthe total number of requests, the reads of a single brief, and the number of different\nbriefs read. All three are far above any legitimate polling, including the busiest paid\naccount: crossing any of them returns 429 READ_RATE_LIMITED with `Retry-After`. Once the\ncost ceiling is reached, further requests keep being refused until it resets, whether or\nnot they carry a valid briefId.\n",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "briefId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The briefId returned by the creation call (22 characters).",
            "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
          }
        ],
        "responses": {
          "200": {
            "description": "The brief, in progress or finished.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "briefId",
                        "status",
                        "failureReason",
                        "createdAt",
                        "updatedAt",
                        "outputLanguage",
                        "brandName",
                        "progress",
                        "pages",
                        "warnings",
                        "metrics",
                        "brief",
                        "markdown",
                        "quality",
                        "models"
                      ],
                      "properties": {
                        "briefId": {
                          "type": "string",
                          "example": "Q3v9Zk2LmN8pR5tW1xY7aB"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "fetching",
                            "analyzing",
                            "synthesizing",
                            "done",
                            "failed"
                          ]
                        },
                        "failureReason": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "NO_USABLE_PAGE",
                            "LLM_UNAVAILABLE",
                            "TIMEOUT",
                            "INTERNAL",
                            null
                          ],
                          "description": "Why the brief failed, null unless status is failed."
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "ISO 8601, UTC.",
                          "example": "2026-09-15T10:00:00.000Z"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "ISO 8601, UTC. Refreshed at every page and every step."
                        },
                        "outputLanguage": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "fr",
                            "en",
                            "es",
                            "de",
                            "it",
                            null
                          ],
                          "description": "Language requested for the brief, null when the service chooses it."
                        },
                        "brandName": {
                          "type": "string",
                          "nullable": true
                        },
                        "progress": {
                          "type": "object",
                          "required": [
                            "pagesTotal",
                            "pagesFetched",
                            "pagesFailed",
                            "pagesAnalyzed"
                          ],
                          "properties": {
                            "pagesTotal": {
                              "type": "integer"
                            },
                            "pagesFetched": {
                              "type": "integer",
                              "description": "Pages read, including pages whose analysis then failed."
                            },
                            "pagesFailed": {
                              "type": "integer",
                              "description": "Pages that could not be read. Read-stage only, so pagesFetched + pagesFailed = pagesTotal once reading is over. A page read but not analysed is NOT counted here, it carries status failed in pages[]."
                            },
                            "pagesAnalyzed": {
                              "type": "integer",
                              "description": "Pages actually analysed by the model, and the only ones the brief is built from. Count the failures in pages[], never as pagesFetched minus pagesAnalyzed."
                            }
                          }
                        },
                        "pages": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "url",
                              "finalUrl",
                              "status",
                              "failureReason",
                              "httpStatus",
                              "title",
                              "wordCount",
                              "language"
                            ],
                            "properties": {
                              "url": {
                                "type": "string"
                              },
                              "finalUrl": {
                                "type": "string",
                                "nullable": true,
                                "description": "URL after redirects."
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "ok",
                                  "failed"
                                ]
                              },
                              "failureReason": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                  "BLOCKED_ADDRESS",
                                  "INVALID_URL",
                                  "FETCH_FAILED",
                                  "BOT_PROTECTION",
                                  "NOT_HTML",
                                  "TOO_SHORT",
                                  "TOO_LARGE",
                                  "TOO_COMPLEX",
                                  "TIMEOUT",
                                  "HTTP_ERROR",
                                  "ANALYSIS_FAILED",
                                  null
                                ],
                                "description": "Why the page did not contribute. TOO_COMPLEX, the page markup exceeds the element cap and was refused whole, never analysed in part. ANALYSIS_FAILED, the page was read but its analysis failed."
                              },
                              "httpStatus": {
                                "type": "integer",
                                "nullable": true
                              },
                              "title": {
                                "type": "string",
                                "nullable": true
                              },
                              "wordCount": {
                                "type": "integer",
                                "nullable": true
                              },
                              "language": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "warnings": {
                          "type": "array",
                          "description": "Empty until the brief is done.",
                          "items": {
                            "type": "string",
                            "enum": [
                              "SINGLE_PAGE",
                              "FEW_VERIFIED_EXAMPLES",
                              "MIXED_LANGUAGES",
                              "HETEROGENEOUS_STYLE",
                              "PAGES_TRUNCATED"
                            ]
                          }
                        },
                        "metrics": {
                          "type": "object",
                          "nullable": true,
                          "description": "Style metrics computed in code on the analyzed pages, never by the model. Null until computed; a failed brief may carry them when its pages were measured before the failure. A metric that cannot be computed is null, never 0.",
                          "properties": {
                            "pagesAnalyzed": {
                              "type": "integer"
                            },
                            "wordsAnalyzed": {
                              "type": "integer"
                            },
                            "dominantLanguage": {
                              "type": "string",
                              "nullable": true
                            },
                            "sentenceLength": {
                              "type": "object",
                              "description": "Words per sentence, mean, median and p90."
                            },
                            "longSentenceRatio": {
                              "type": "number"
                            },
                            "paragraphLengthMean": {
                              "type": "number"
                            },
                            "questionsPer1000Words": {
                              "type": "number"
                            },
                            "exclamationsPer1000Words": {
                              "type": "number"
                            },
                            "pronouns": {
                              "type": "object",
                              "nullable": true,
                              "description": "Per 1000 words, French and English only."
                            },
                            "readability": {
                              "type": "object",
                              "nullable": true
                            },
                            "headingsPer1000Words": {
                              "type": "number"
                            },
                            "listItemsPer1000Words": {
                              "type": "number"
                            },
                            "boldPer1000Words": {
                              "type": "number"
                            },
                            "punctuationPer1000Words": {
                              "type": "object"
                            },
                            "emojiPer1000Words": {
                              "type": "number"
                            },
                            "topExpressions": {
                              "type": "array",
                              "maxItems": 20,
                              "items": {
                                "type": "object",
                                "properties": {
                                  "expression": {
                                    "type": "string"
                                  },
                                  "count": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "brief": {
                          "type": "object",
                          "nullable": true,
                          "description": "The structured brief, set when status is done.",
                          "properties": {
                            "schemaVersion": {
                              "type": "integer"
                            },
                            "language": {
                              "type": "string",
                              "enum": [
                                "fr",
                                "en",
                                "es",
                                "de",
                                "it"
                              ]
                            },
                            "sourceLanguage": {
                              "type": "string",
                              "nullable": true
                            },
                            "summary": {
                              "type": "string"
                            },
                            "consistency": {
                              "type": "object"
                            },
                            "authorPersona": {
                              "type": "object"
                            },
                            "readerPersona": {
                              "type": "object"
                            },
                            "tone": {
                              "type": "object",
                              "description": "Six axes scored 1 to 5, with evidence."
                            },
                            "writingStyle": {
                              "type": "object"
                            },
                            "vocabulary": {
                              "type": "object"
                            },
                            "structure": {
                              "type": "object"
                            },
                            "doList": {
                              "type": "array",
                              "maxItems": 15,
                              "items": {
                                "type": "string"
                              }
                            },
                            "dontList": {
                              "type": "array",
                              "maxItems": 15,
                              "items": {
                                "type": "string"
                              }
                            },
                            "examples": {
                              "type": "array",
                              "maxItems": 12,
                              "description": "Quotes verified in the pages.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "quote": {
                                    "type": "string"
                                  },
                                  "sourceUrl": {
                                    "type": "string"
                                  },
                                  "illustrates": {
                                    "type": "string"
                                  }
                                }
                              }
                            },
                            "writingPrompt": {
                              "type": "string",
                              "description": "Complete system prompt, ready to paste into any AI writing tool."
                            },
                            "writingPromptCompact": {
                              "type": "string",
                              "maxLength": 1500
                            },
                            "sampleText": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "topic": {
                                  "type": "string"
                                },
                                "text": {
                                  "type": "string"
                                }
                              }
                            },
                            "reviewChecklist": {
                              "type": "array",
                              "minItems": 0,
                              "maxItems": 10,
                              "description": "Review questions, possibly empty.",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "markdown": {
                          "type": "string",
                          "nullable": true,
                          "description": "The full brief as a Markdown document, set when status is done."
                        },
                        "quality": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "examplesProposed": {
                              "type": "integer"
                            },
                            "examplesVerified": {
                              "type": "integer"
                            },
                            "expressionsProposed": {
                              "type": "integer"
                            },
                            "expressionsVerified": {
                              "type": "integer"
                            }
                          }
                        },
                        "models": {
                          "type": "object",
                          "nullable": true,
                          "description": "Models actually used for the page analysis and the synthesis.",
                          "properties": {
                            "map": {
                              "type": "string",
                              "nullable": true
                            },
                            "synthesis": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BRIEF_ID, briefId is missing or malformed."
          },
          "404": {
            "description": "NOT_FOUND, no brief matches this briefId."
          },
          "429": {
            "description": "READ_RATE_LIMITED, anti-abuse ceiling on reads that are not attributed to an account. Three counters, all per IP address and per UTC day: the total number of requests (the cost ceiling), the reads of a single brief (hammering one resource), and the number of different briefs read (scanning for identifiers). All three are far above any legitimate polling, and none of them is a usage quota: reading a brief consumes no quota at all. The cost ceiling counts every request, refused ones included, because a refused request still costs server work; the two others only count requests they let through. `details.limit`, `Retry-After` and `details.reset_at` describe the counter that was crossed. Reads authenticated with an API key are not subject to them.\n"
          },
          "502": {
            "description": "UPSTREAM_ERROR, the generation service did not answer."
          }
        },
        "tags": [
          "v1",
          "Free tools"
        ]
      }
    },
    "/api/v1/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Get current API usage and quota status",
        "description": "Returns the current period's API guide usage (guides created through the API or MCP), quota limit, remaining count and the renewal date. Aligned on the Stripe subscription billing cycle, or on the calendar month (UTC) for a Conqueror account without a subscription. Does not consume credits. `enforced` tells whether the quota is enforced on the current window: when `true`, a guide creation through the API or MCP that would exceed the limit is refused with HTTP 402 (see `POST /api/v1/guides`). When `false` (any plan other than Conqueror, a billing cycle that started before the refusal went live, or a Conqueror subscription in payment recovery, counted over the last 30 days with no reset date), the figures are informational only and nothing is refused on quota grounds. `upgrade_url` is the absolute link to the \"Priority and API volume\" add-on (the same as `details.upgrade_url` of the 402), absent when the add-on cannot raise the quota (already at its highest volume, or account without a subscription to add it to).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current quota status"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/admin/credits/ledger": {
      "get": {
        "operationId": "getCreditLedger",
        "summary": "Grand-livre crédits d'un compte (admin)",
        "description": "Timeline complète et immuable des mouvements de crédits d'un utilisateur (octrois, consommations, refunds, resets, ajustements admin), avec libellés FR en clair, delta signé, solde après, source, auteur et référence. Inclut le solde reconstruit à une date arbitraire (paramètre `at`) et un contrôle de cohérence (solde == dernier balanceAfter == somme des deltas). Réservé aux administrateurs. Lecture seule.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "query",
            "required": true,
            "description": "Identifiant Mongo de l'utilisateur.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "Date ISO pour reconstruire le solde à cet instant (point-in-time).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Nombre maximum de lignes (défaut 200, max 1000).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timeline + en-tête + cohérence."
          },
          "400": {
            "description": "userId manquant."
          },
          "403": {
            "description": "Accès réservé aux administrateurs."
          }
        },
        "tags": [
          "Admin",
          "Credits"
        ]
      }
    },
    "/api/v1/guide": {
      "get": {
        "summary": "Get data for a specific guide",
        "description": "Retrieves details of a specific guide. While the guide is being created, the endpoint returns 202 (poll again later). If the creation failed permanently (explicit terminal flag, or a failure older than 15 minutes with no retry left, see progressStatus.terminalInferred), it returns 200 with success=false, status=failed, a human-readable French error message, creationFailed=true, refunded (whether the consumed credits were automatically given back) and guide.progressStatus (raw reason, message, terminal, kind, and failureFacts when the figures are known, which is only possible for sources whose web pages we read, that is a search engine or a `_CITATIONS` source, never a source in answer mode). Stop polling in that case.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ID of the guide to retrieve"
          }
        ],
        "responses": {
          "200": {
            "description": "Guide successfully retrieved, or guide creation failed permanently (success=false, status=failed)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Guide ready",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "example": true
                        },
                        "guide": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "query": {
                              "type": "string"
                            },
                            "lang": {
                              "type": "string"
                            },
                            "source": {
                              "type": "string",
                              "description": "Engine and analysis mode the guide targets, returned as the UPPERCASE database form of the source (e.g. `GOOGLE`, `CHATGPT`, `CHATGPT_CITATIONS`, `GOOGLE_AI_OVERVIEW`, `GOOGLE_AI_OVERVIEW_CITATIONS`). Either the Google SERP (`GOOGLE`), Google's AI answers as `GOOGLE_AI_OVERVIEW` (answer text analysis) or `GOOGLE_AI_OVERVIEW_CITATIONS` (analysis of the cited sources), or an AI engine as `<ENGINE>` (AI answer analysis) or `<ENGINE>_CITATIONS` (analysis of the sources the AI cites)"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "shareState": {
                              "type": "string",
                              "enum": [
                                "private",
                                "shared-read",
                                "shared-edit"
                              ],
                              "description": "Sharing mode of the guide. Always returned, and `private` when the guide has never been shared. Change it with PUT /api/v1/guide.",
                              "example": "private"
                            },
                            "shareUrl": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Public URL of the guide, readable without an account, returned when `shareState` starts with `shared-`. `null` when the guide is private (or when the server has no public URL configured).",
                              "example": "https://app.serpmantics.com/6a8e660408106ac39f4f6261/edit"
                            },
                            "guide": {
                              "type": "object",
                              "properties": {
                                "structure": {
                                  "type": "object",
                                  "properties": {
                                    "length": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "headings": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "paragraphs": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "images": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "videos": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "linksInternal": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "linksExternal": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "tables": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "lists": {
                                      "type": "object",
                                      "properties": {
                                        "from": {
                                          "type": "number"
                                        },
                                        "to": {
                                          "type": "number"
                                        }
                                      }
                                    }
                                  }
                                },
                                "add": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "expression": {
                                        "type": "string"
                                      },
                                      "from": {
                                        "type": "number"
                                      },
                                      "to": {
                                        "type": "number"
                                      },
                                      "isStatisticallyDifferent": {
                                        "type": "boolean"
                                      },
                                      "serpResultsPresence": {
                                        "type": "object",
                                        "additionalProperties": {
                                          "type": "number"
                                        }
                                      }
                                    }
                                  }
                                },
                                "avoid": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "expression": {
                                        "type": "string"
                                      },
                                      "serpResultsPresence": {
                                        "type": "object",
                                        "additionalProperties": {
                                          "type": "number"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "scoreGuidelines": {
                              "type": "object",
                              "properties": {
                                "expressions": {
                                  "type": "object",
                                  "properties": {
                                    "bad": {
                                      "type": "number"
                                    },
                                    "poor": {
                                      "type": "number"
                                    },
                                    "good": {
                                      "type": "number"
                                    },
                                    "supreme": {
                                      "type": "number"
                                    }
                                  }
                                },
                                "stats": {
                                  "type": "object",
                                  "properties": {
                                    "expressionsAddOptimisation": {
                                      "type": "object",
                                      "properties": {
                                        "isStatisticallyDifferent": {
                                          "type": "boolean"
                                        },
                                        "lowerWhisker": {
                                          "type": "number"
                                        },
                                        "q25": {
                                          "type": "number"
                                        },
                                        "median": {
                                          "type": "number"
                                        },
                                        "q75": {
                                          "type": "number"
                                        },
                                        "upperWhisker": {
                                          "type": "number"
                                        }
                                      }
                                    },
                                    "structureOptimisation": {
                                      "type": "object",
                                      "properties": {
                                        "isStatisticallyDifferent": {
                                          "type": "boolean"
                                        },
                                        "lowerWhisker": {
                                          "type": "number"
                                        },
                                        "q25": {
                                          "type": "number"
                                        },
                                        "median": {
                                          "type": "number"
                                        },
                                        "q75": {
                                          "type": "number"
                                        },
                                        "upperWhisker": {
                                          "type": "number"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "topSERPResultsDetails": {
                              "type": "array"
                            },
                            "isReady": {
                              "type": "boolean"
                            },
                            "corpusTopWindow": {
                              "$ref": "#/components/schemas/GuideCorpusTopWindow"
                            },
                            "serpQuestions": {
                              "type": "array",
                              "nullable": true,
                              "description": "People also ask questions that Google shows below the first results for the query, in SERP order. Always present. `null` for a guide created before 2026-10-19 (the field did not exist yet), `[]` when the SERP showed no such block. Only the questions are returned, never Google's answers.",
                              "items": {
                                "type": "object",
                                "required": [
                                  "position",
                                  "question",
                                  "seedQuestion"
                                ],
                                "properties": {
                                  "position": {
                                    "type": "integer",
                                    "nullable": true,
                                    "description": "Absolute rank of the questions block among the SERP items (the same for every question of one block), or `null` when unknown."
                                  },
                                  "question": {
                                    "type": "string",
                                    "description": "The question as Google displays it, plain text."
                                  },
                                  "seedQuestion": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Question this one was expanded from, or `null` for a top-level question."
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Guide creation failed permanently (stop polling). `status` is `failed`, `error` holds the human-readable French message and `guide.progressStatus` exposes the raw `reason`, the `message`, `terminal`, `kind` (`legitimate` or `technical`) and, when the figures are known, `failureFacts` (only for sources whose web pages we read, that is a search engine or a `_CITATIONS` source, never a source in answer mode).",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "example": false
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "failed"
                          ]
                        },
                        "error": {
                          "type": "string",
                          "description": "Human-readable French message explaining the failure (depends on the reason and the source)",
                          "example": "Pas assez de sources exploitables : ChatGPT a cité trop peu de pages accessibles pour cette requête. Essayez une requête plus générale, ou la source Google. Vos crédits sont recrédités automatiquement."
                        },
                        "creationFailed": {
                          "type": "boolean",
                          "example": true
                        },
                        "refunded": {
                          "type": "boolean",
                          "description": "Whether the consumed credits were automatically given back (refund journaled in the credits ledger)"
                        },
                        "guide": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "query": {
                              "type": "string"
                            },
                            "lang": {
                              "type": "string"
                            },
                            "source": {
                              "type": "string"
                            },
                            "progressStatus": {
                              "$ref": "#/components/schemas/GuideProgressStatus"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Guide is still being created (poll again later). Never returned for a permanently failed guide (see the 200 failed shape). The body is `{ success: false, error: \"Guide is not ready yet\" }`. When the guide is queued (the account is beyond the priority guides of its plan this month, so further guides start at a rate of one per hour), the same 202 also carries `queued: true`, `estimatedStartAt`, `startsSoon`, `message`, `upgradeUrl` (only when the \"Priority and API volume\" add-on can raise this account's volume) and `progressStatus` (at the root, step `queued`). There is never a `guide` key in a 202. A queued guide may wait several hours: keep polling only while `startsSoon` is true, otherwise poll less often, or come back after `estimatedStartAt`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "Guide is not ready yet"
                      ],
                      "description": "Always this exact value, queued or not."
                    },
                    "queued": {
                      "type": "boolean",
                      "description": "Only when the guide is queued, always true then."
                    },
                    "estimatedStartAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Only when queued. Estimated start of the generation (ISO 8601, UTC). An estimate, not a promise."
                    },
                    "startsSoon": {
                      "type": "boolean",
                      "description": "Only when queued. Computed by the server at response time: true when `estimatedStartAt` is at most 5 minutes ahead, or at most 10 minutes past; false otherwise. Use it instead of comparing `estimatedStartAt` with your own clock."
                    },
                    "message": {
                      "type": "string",
                      "description": "Only when queued. Human-readable explanation, in English. Never states a past time: once `estimatedStartAt` has passed it says the guide starts shortly, or as soon as a slot is free."
                    },
                    "upgradeUrl": {
                      "type": "string",
                      "description": "Only when queued, and only when the \"Priority and API volume\" add-on can raise this account's monthly volume. Absolute URL where to buy it."
                    },
                    "progressStatus": {
                      "description": "Only when queued. Creation progress, step `queued`.",
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/GuideProgressStatus"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing guide ID"
          },
          "401": {
            "description": "Unauthorized - User doesn't own the guide or invalid token"
          },
          "404": {
            "description": "Guide not found"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. The guide is NOT deleted, it still appears in GET /api/v1/guides with `expired: true`, and DELETE /api/v1/guide still removes it."
          },
          "500": {
            "description": "Server error - Failed to get guide"
          }
        },
        "tags": [
          "v1",
          "Guide"
        ]
      },
      "delete": {
        "summary": "Delete a guide",
        "description": "Deletes a specific guide. Deliberately NOT blocked by the 180-day expiry: an expired guide can no longer be read, but it can always be deleted, so that you can still clean up your account.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ID of the guide to delete"
          }
        ],
        "responses": {
          "200": {
            "description": "Guide successfully deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing guide ID"
          },
          "401": {
            "description": "Unauthorized - User doesn't own the guide or invalid token"
          },
          "500": {
            "description": "Server error - Failed to delete guide"
          }
        },
        "tags": [
          "v1",
          "Guide"
        ]
      },
      "put": {
        "summary": "Update a guide",
        "description": "Updates editable fields of a specific guide (group, status, share state, linked URL, meta, hidden expressions). Only owners may update.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ID of the guide to update"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "group": {
                    "type": "string",
                    "description": "Group/folder the guide belongs to (triggers cannibalisation analysis on previous and new group)"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "",
                      "draft",
                      "active",
                      "review",
                      "published",
                      "archived"
                    ],
                    "description": "Editorial status of the guide. Empty string clears the status."
                  },
                  "shareState": {
                    "type": "string",
                    "enum": [
                      "private",
                      "shared-read",
                      "shared-edit"
                    ],
                    "description": "Sharing mode. Any value outside the enum is rejected with 400 (the check is case-sensitive). Plan must allow shared-read or shared-edit, otherwise 403 is returned."
                  },
                  "linkedUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public URL where the guide content is published (validated as URL)"
                  },
                  "meta": {
                    "type": "object",
                    "description": "SEO meta override for the guide",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      }
                    }
                  },
                  "hiddenExpressions": {
                    "type": "array",
                    "description": "Expressions to hide from the optimisation suggestions",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Guide successfully updated. When shareState was part of the update, data.shareState echoes the state that was requested, validated and persisted by this call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "description": "Only present when shareState was part of the update",
                      "properties": {
                        "shareState": {
                          "type": "string",
                          "enum": [
                            "private",
                            "shared-read",
                            "shared-edit"
                          ],
                          "description": "Share state requested, validated against your plan, and persisted by this call. Read it back with GET /api/v1/guide to observe the stored state."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing guide ID, invalid status, shareState outside the enum, invalid URL, invalid meta or invalid hiddenExpressions format"
          },
          "401": {
            "description": "Unauthorized - User doesn't own the guide or invalid token"
          },
          "403": {
            "description": "Plan does not allow the requested shareState (response includes inviteToUpgrade flag)"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. DELETE /api/v1/guide still removes an expired guide. Housekeeping is still allowed on an expired guide: a request whose body contains ONLY `group`, `status` and/or `shareState` (and, for `shareState`, only the value `private`) is accepted and applied. Anything else is refused in full, including a body that mixes an allowed field with a forbidden one: in that case nothing at all is written."
          },
          "500": {
            "description": "Server error - Failed to update guide"
          }
        },
        "tags": [
          "v1",
          "Guide"
        ]
      }
    },
    "/api/v1/guides": {
      "get": {
        "summary": "List user's guides",
        "description": "Returns a list of guides for the authenticated user.\n\nUse `query` to check whether a guide already exists before creating one\n(creating a duplicate costs credits). Beware of two behaviours of this endpoint:\n- `query` and `group` are case-insensitive REGULAR EXPRESSIONS, not exact matches:\n  special characters in the value are interpreted as regex syntax, and an invalid\n  regex returns no result at all. (`status` is different: a known status is matched\n  exactly, any other value falls back to a regex.) Always compare the returned\n  `query` and `source` yourself, and fall back to an unfiltered paginated scan\n  before concluding that nothing exists;\n- `totalCount` does NOT take `query` nor `group` into account (known limitation:\n  both are applied as application-level regexes that the count query does not\n  know, so it returns 0 as soon as either is used, alone or combined with\n  `status`). Filtering on `status` alone does give a correct `totalCount`, since\n  it is a real stored field. For an existence check, trust the `guides` actually\n  returned; `totalCount` is only reliable for paginating a listing filtered by\n  `status` alone, or not filtered at all.\n\nAn empty result is returned as a 404 with `success: false` and\n`error: \"No guides returned\"`, not as an empty list.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "required": false,
            "type": "integer"
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Number of guides per page",
            "required": false,
            "type": "integer"
          },
          {
            "name": "query",
            "in": "query",
            "description": "Filter on the guide query. Case-insensitive partial match, interpreted as a regular expression (see the endpoint description).",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "assurance emprunteur"
          },
          {
            "name": "group",
            "in": "query",
            "description": "Filter on the guide group. Case-insensitive partial match, interpreted as a regular expression.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter on the guide status. Exact match when the value is one of the known statuses, otherwise a case-insensitive regular expression.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "review",
                "published",
                "archived"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with guides",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "guides": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "query": {
                            "type": "string"
                          },
                          "lang": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "description": "Engine and analysis mode the guide targets, returned as the UPPERCASE database form of the source (e.g. `GOOGLE`, `CHATGPT`, `CHATGPT_CITATIONS`, `GOOGLE_AI_OVERVIEW`, `GOOGLE_AI_OVERVIEW_CITATIONS`). Either the Google SERP (`GOOGLE`), Google's AI answers as `GOOGLE_AI_OVERVIEW` (answer text analysis) or `GOOGLE_AI_OVERVIEW_CITATIONS` (analysis of the cited sources), or an AI engine as `<ENGINE>` (AI answer analysis) or `<ENGINE>_CITATIONS` (analysis of the sources the AI cites)"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "shareState": {
                            "type": "string",
                            "enum": [
                              "private",
                              "shared-read",
                              "shared-edit"
                            ],
                            "description": "Sharing mode of the guide. Absent from the item when the guide has never been shared, which is equivalent to `private` (GET /api/v1/guide always returns the effective value, plus the public `shareUrl`). Change it with PUT /api/v1/guide.",
                            "example": "shared-read"
                          },
                          "expired": {
                            "type": "boolean",
                            "description": "Present and `true` ONLY when the guide is expired (more than 180 days after its creation). Absent otherwise. This listing is the only endpoint that still returns expired guides: reading them (GET /api/v1/guide, score, eeat, outline, meta...) answers 410, but DELETE /api/v1/guide still removes them so you can clean up. Their analysis payload is stripped from the item.",
                            "example": true
                          },
                          "expiresAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Date at which the guide became unreadable (creation + 180 days). Present only alongside `expired: true`."
                          },
                          "progressStatus": {
                            "$ref": "#/components/schemas/GuideProgressStatus"
                          },
                          "corpusTopWindow": {
                            "$ref": "#/components/schemas/GuideCorpusTopWindow"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "No guide matches. The body is `{ success: false, error: \"No guides returned\" }`. This is the normal answer for an empty result, not an error to retry."
          },
          "500": {
            "description": "Server error"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseUnavailable"
          }
        },
        "tags": [
          "v1",
          "Guide"
        ]
      },
      "post": {
        "summary": "Create new guides",
        "description": "Create one or more new guides based on provided queries.\nEach guide targets exactly ONE engine and ONE analysis mode, chosen with the optional `source` field (default `google`).\n\nHow to request each guide type:\n1. Google SERP guide (1 credit per guide): omit `source`, or pass `source: \"google\"`.\n   Example payload: {\"queries\": [\"best crm\"], \"lang\": \"en-us\"}\n1bis. Google AI Overview guide (1 credit per guide). Two modes, like AI engines:\n   `source: \"google_ai_overview\"` builds the guide from the TEXT of Google's AI answers\n   (AI Overview, completed with AI Mode answers) ; `source: \"google_ai_overview_citations\"`\n   builds it from the content of the web SOURCES those answers cite (recommended for GEO).\n   Same language/country parameters as a Google SERP guide, 1 credit per guide in both modes.\n   Example payload: {\"queries\": [\"best crm\"], \"lang\": \"en-us\", \"source\": \"google_ai_overview_citations\"}\n2. LLM ANSWER guide (4 credits per guide): pass the engine name alone, e.g. `source: \"chatgpt\"`.\n   The guide is built from the answer text the AI generates for the query.\n   Example payload: {\"queries\": [\"best crm\"], \"lang\": \"en-us\", \"source\": \"chatgpt\"}\n3. LLM CITATIONS guide (4 credits per guide) [RECOMMENDED AI mode]: pass the engine name with the `_citations` suffix, e.g. `source: \"chatgpt_citations\"`.\n   The guide is built from the content of the web pages the AI cites in its answer.\n   Example payload: {\"queries\": [\"best crm\"], \"lang\": \"en-us\", \"source\": \"chatgpt_citations\"}\n\nWhich AI mode to pick? For GEO (getting a page visible in AI answers), prefer `<engine>_citations`:\nAI engines send traffic by CITING pages as sources, so the winning move is to look like the pages they cite.\nThe answer-text mode (`<engine>` alone) is mostly useful to analyze how the AI phrases its own answer.\nWhen in doubt, pick `<engine>_citations`.\n\nThe same two modes exist for every AI engine (chatgpt, perplexity, claude, gemini, grok, mistral, deepseek).\nTo optimize the same page for several engines or modes (e.g. Google AND ChatGPT answers AND ChatGPT sources), create one guide per source value on the same query.\n\nIMPORTANT, HOW TO READ THE RESPONSE OF THIS ENDPOINT, WHICH SPENDS CREDITS.\nQueries listed in `guidesFailed` are PROVEN not to have produced a guide and their credit was\ngiven back (unless the account has unlimited credits, where nothing was reserved): re-sending\nthem is free and correct. Queries listed in `guidesUnknown` have an UNDECIDABLE outcome and\ntheir credit is deliberately KEPT, because the guide was most likely written: DO NOT re-send\nthem, you would pay for the same guide twice. Look them up in `GET /api/v1/guides` after a few\nminutes instead, and contact support if nothing shows up.\nFinally, a `200` is NOT a promise that every query produced a guide: compare `guides.length`\nwith the number of queries you sent, never read `success` alone, and never re-send a query\njust because it is missing from `guides`.",
        "tags": [
          "v1",
          "Guide"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "examples": {
                "googleSerpGuide": {
                  "summary": "Google SERP guide (1 credit)",
                  "value": {
                    "queries": [
                      "best crm"
                    ],
                    "lang": "en-us"
                  }
                },
                "googleAiOverviewGuide": {
                  "summary": "Google AI Overview answer guide, built from the text of Google's AI answers (1 credit)",
                  "value": {
                    "queries": [
                      "best crm"
                    ],
                    "lang": "en-us",
                    "source": "google_ai_overview"
                  }
                },
                "googleAiOverviewCitationsGuide": {
                  "summary": "Google AI Overview citations guide, built from the sources Google's AI answers cite (1 credit, recommended AI mode for GEO)",
                  "value": {
                    "queries": [
                      "best crm"
                    ],
                    "lang": "en-us",
                    "source": "google_ai_overview_citations"
                  }
                },
                "llmAnswerGuide": {
                  "summary": "LLM answer guide, built from the AI's generated answer (4 credits)",
                  "value": {
                    "queries": [
                      "best crm"
                    ],
                    "lang": "en-us",
                    "source": "chatgpt"
                  }
                },
                "llmCitationsGuide": {
                  "summary": "LLM citations guide, built from the sources the AI cites (4 credits, recommended AI mode for GEO)",
                  "value": {
                    "queries": [
                      "best crm"
                    ],
                    "lang": "en-us",
                    "source": "chatgpt_citations"
                  }
                }
              },
              "schema": {
                "type": "object",
                "required": [
                  "queries",
                  "lang"
                ],
                "properties": {
                  "queries": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of queries to create guides for"
                  },
                  "lang": {
                    "type": "string",
                    "description": "Language code for the guides"
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "google",
                      "google_ai_overview",
                      "google_ai_overview_citations",
                      "chatgpt",
                      "chatgpt_citations",
                      "perplexity",
                      "perplexity_citations",
                      "claude",
                      "claude_citations",
                      "gemini",
                      "gemini_citations",
                      "grok",
                      "grok_citations",
                      "mistral",
                      "mistral_citations",
                      "deepseek",
                      "deepseek_citations"
                    ],
                    "default": "google",
                    "description": "Target engine AND analysis mode the guide optimizes for. One guide = one source. `google` analyzes the Google SERP (1 credit per guide). Google's AI answers have the same two modes as AI engines : `google_ai_overview` analyzes the TEXT of the AI answers (AI Overview, completed with AI Mode) ; `google_ai_overview_citations` analyzes the content of the web SOURCES those answers cite, the recommended mode for GEO. 1 credit per guide in both modes, same language/country parameters as `google`. For AI engines (chatgpt, perplexity, claude, gemini, grok, mistral, deepseek), pick the mode: `<engine>` analyzes the AI's generated ANSWER for the query; `<engine>_citations` analyzes the content of the web SOURCES the AI cites, which is the recommended mode for GEO (become one of the cited sources). Both AI modes cost 4 credits per guide. Omit for the default `google`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "No requested query landed in `guidesFailed` nor in `guidesUnknown`. Returned with `206` instead as soon as one requested query landed in either list. WARNING, A `200` IS NOT A PROMISE THAT EVERY QUERY PRODUCED A GUIDE, AND THE CREDIT IS STILL SPENT. When the backend answers success WITHOUT a usable guide id, the query enters NONE of the three lists and its credit is KEPT on purpose (the guide was most likely written, so retrying would pay for it twice). A batch made only of such queries therefore answers `200 { success: true, guides: [], guidesFailed: [], guidesUnknown: [] }` with the credits debited. Always compare `guides.length` with the number of queries you sent, never read `success` alone; on a difference, look the guides up in `GET /api/v1/guides` instead of re-sending the query.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "`true` only when BOTH `guidesFailed` and `guidesUnknown` are empty. An undecidable outcome is not a success: its credit was kept, so reporting it as a complete success would hide a charge with nothing to show yet. WARNING, `true` still does not mean that every query produced a guide: a query answered as a success WITHOUT a usable guide id enters neither list, so `success` stays `true` while `guides` can be shorter than the queries you sent, credit spent. `guides.length` is the only reliable count of what was created."
                    },
                    "guides": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "query": {
                            "type": "string"
                          },
                          "lang": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "description": "Engine and analysis mode the guide targets, returned as the UPPERCASE database form of the source (e.g. `GOOGLE`, `CHATGPT`, `CHATGPT_CITATIONS`, `GOOGLE_AI_OVERVIEW`, `GOOGLE_AI_OVERVIEW_CITATIONS`). Either the Google SERP (`GOOGLE`), Google's AI answers as `GOOGLE_AI_OVERVIEW` (answer text analysis) or `GOOGLE_AI_OVERVIEW_CITATIONS` (analysis of the cited sources), or an AI engine as `<ENGINE>` (AI answer analysis) or `<ENGINE>_CITATIONS` (analysis of the sources the AI cites)"
                          },
                          "userId": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "guidesFailed": {
                      "type": "array",
                      "description": "Queries whose guide is PROVEN not to exist (validation refusal, or a 5xx that proves nothing was saved). Their credit was refunded, so requesting them again is free and correct. On an account with unlimited credits no credit was reserved in the first place, so there is nothing to refund and re-requesting is free all the same.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "query": {
                            "type": "string"
                          },
                          "lang": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "guidesUnknown": {
                      "type": "array",
                      "description": "Queries whose outcome is UNDECIDABLE: the backend saves the guide BEFORE starting its task, so a non-proving 5xx, a timeout that reached the server or a dropped connection all leave a document the worker will pick up. Their credit is deliberately KEPT (refunding would make a produced guide free), except on an account with unlimited credits, where no credit was reserved and therefore none is kept. Do NOT re-submit these queries: the guide will most likely show up in `GET /api/v1/guides` within a few minutes, and re-submitting charges the same guide twice. Contact support if nothing appears.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "query": {
                            "type": "string"
                          },
                          "lang": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "credits": {
                      "type": "number",
                      "nullable": true,
                      "description": "Spendable balance (plan credits plus non-expired One-Shot) re-read from the source AFTER every movement. `null` when that read failed: the guides were still created and charged, the balance is simply unknown. Never a locally estimated figure, which would silently open or close the creation path."
                    },
                    "hasUnlimitedCredits": {
                      "type": "boolean",
                      "description": "Whether the account spends from an unlimited plan. Reported on the same response as `credits` because it decides what a client may TRUTHFULLY say about money: credits are only reserved when this is `false`, so a message claiming a charge is wrong on an unlimited account, and omitting it hides a real charge on every other one. Read it from here rather than from a value cached when the page was loaded."
                    },
                    "guidesCountBefore": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Number of guides the user owned BEFORE this call (0 for a first guide). `null` when the count could not be read."
                    }
                  }
                }
              }
            }
          },
          "206": {
            "description": "Partial success, and the ONLY status under which `guidesUnknown` can be non-empty. At least one requested query landed in `guidesFailed` (absence PROVEN, credit refunded, retrying is free and correct) or in `guidesUnknown` (outcome UNDECIDABLE, credit KEPT on purpose because the guide was most likely written, so DO NOT retry: you would pay for it twice). `success` is `false`. The body has exactly the same shape as the `200` above, `guides`, `guidesFailed`, `guidesUnknown`, `credits`, `hasUnlimitedCredits` and `guidesCountBefore` included; read the three arrays to know which query is in which state. The schema is deliberately NOT repeated here: two copies would drift apart, and this one describes money."
          },
          "400": {
            "description": "Bad request. `queries` is missing, is not an array of non-empty strings, or `lang` is invalid."
          },
          "402": {
            "description": "Monthly API guide quota exceeded. Applies only to the Conqueror plan, to guides created through the API or MCP, and only on a billing cycle that started on or after the date the refusal went live, or on the calendar month (UTC) for a Conqueror account without a subscription: `GET /api/v1/usage` returns `enforced: true` when it applies to the current window. The WHOLE request is refused when the guides already created in the window plus the requested ones exceed the limit: no guide is created and nothing is charged. `details.remaining` tells how many can still be created, `details.upgrade_url` where to raise the quota (absent when the add-on cannot raise it, at its highest volume or without a subscription). Guides created in the web interface are never refused. Other plans have no API quota: their credits are the only bound.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "API_QUOTA_EXCEEDED"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "limit": {
                              "type": "integer"
                            },
                            "used": {
                              "type": "integer"
                            },
                            "remaining": {
                              "type": "integer"
                            },
                            "requested": {
                              "type": "integer"
                            },
                            "period_end": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "upgrade_url": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. Either not enough credits for the requested guides, or the requested `source` is in beta and currently restricted to administrators.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Not enough credits. You need 4 credits (4 credits per guide for chatgpt guides), but you only have 1."
                    },
                    "inviteToUpgrade": {
                      "type": "boolean",
                      "example": true
                    },
                    "upgradeType": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "credits",
                        "tokens"
                      ],
                      "example": "credits"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many guides already waiting or in progress for this account (20 by default). The whole request is refused before any credit is reserved: no guide was created and nothing was charged. When the account is beyond the priority guides of its plan this month (generation slowed down to one guide per hour), the body carries `code: \"PRIORITY_QUEUE_FULL\"`, an explicit `error` message, the `Retry-After: 3600` header and, when the \"Priority and API volume\" add-on can raise the volume, `upgrade_url`. Otherwise the body is `{ success: false, error: \"Too many guides in progress\" }` without `code`: retry once some of the guides in progress are ready.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Only with `code` `PRIORITY_QUEUE_FULL` (3600).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "PRIORITY_QUEUE_FULL"
                      ],
                      "description": "Present only when the account is slowed down (see above)."
                    },
                    "error": {
                      "type": "string",
                      "example": "20 guides are already waiting or in progress (limit 20). Your account is beyond the priority guides of its plan this month (230/200), so generation is slowed down to one guide per hour. No guide was created and nothing was charged. Retry when some of the guides in progress finish: they may be generating (a few minutes) or waiting for validation, and a held guide starts about once an hour."
                    },
                    "upgrade_url": {
                      "type": "string",
                      "description": "Only with `code` `PRIORITY_QUEUE_FULL`, when the add-on can raise the volume. Absolute URL."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseUnavailable"
          }
        }
      },
      "delete": {
        "summary": "Delete multiple guides",
        "description": "Deletes multiple guides at once",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "guideIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of guide IDs to delete",
                    "example": [
                      "507f1f77bcf86cd799439011",
                      "507f1f77bcf86cd799439012"
                    ]
                  }
                },
                "required": [
                  "guideIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Guides successfully deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "deletedCount": {
                      "type": "number",
                      "example": 2
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - Missing or invalid guide IDs"
          },
          "401": {
            "description": "Unauthorized - Invalid token or user doesn't own guides"
          },
          "500": {
            "description": "Server error - Failed to delete guides"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseUnavailable"
          }
        },
        "tags": [
          "v1",
          "Guide"
        ]
      }
    },
    "/api/v1/score": {
      "post": {
        "summary": "Analyze content optimization score",
        "description": "Analyzes the optimization score of the provided content for a specific guide",
        "tags": [
          "v1",
          "Guide"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "guideId",
                  "content"
                ],
                "properties": {
                  "guideId": {
                    "type": "string",
                    "description": "ID of the guide to analyze"
                  },
                  "content": {
                    "type": "string",
                    "description": "Content to analyze"
                  },
                  "pageUrl": {
                    "type": "string",
                    "description": "Optional. Read only for a visitor of a guide shared in read-only mode: the URL of one of the guide's SERP pages, whose stored content is then analysed instead of the guide's text. Ignored for the owner and for a guide shared in edit mode. Unknown URL: the guide's text is analysed.\n"
                  },
                  "saveToGuide": {
                    "type": "boolean",
                    "description": "Whether to save the content and the analysis score back to the guide (default: true)\n"
                  },
                  "scoreProgressive": {
                    "type": "boolean",
                    "default": true,
                    "description": "Whether to compute the progressive score build-up curve (default: true). When false, progressiveScores is returned empty. Null is treated as \"not provided\", so the default applies.\n"
                  },
                  "includeEmbedding": {
                    "type": "boolean",
                    "default": true,
                    "description": "Whether to compute the semantic embedding analysis, i.e. proximity and PCA (default: true). When false, the embeddingAnalysis key is omitted from the response. Null is treated as \"not provided\", so the default applies.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response with content analysis results. A call that saves nothing (`saveToGuide: false`, or a guide shared in read-only mode) and that repeats an identical request made less than 10 minutes ago is served from a cache, with the same body and without a new analysis.",
            "headers": {
              "X-Score-Cache": {
                "description": "`hit` when the analysis was served from the cache of identical requests, `miss` when it was computed.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "hit",
                    "miss"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "guide": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "query": {
                          "type": "string"
                        },
                        "lang": {
                          "type": "string"
                        },
                        "userId": {
                          "type": "string"
                        }
                      }
                    },
                    "contentAnalysis": {
                      "type": "object",
                      "properties": {
                        "structure": {
                          "type": "object",
                          "properties": {
                            "length": {
                              "type": "number"
                            },
                            "headings": {
                              "type": "number"
                            },
                            "paragraphs": {
                              "type": "number"
                            },
                            "images": {
                              "type": "number"
                            },
                            "videos": {
                              "type": "number"
                            },
                            "links": {
                              "type": "number"
                            },
                            "tables": {
                              "type": "number"
                            },
                            "lists": {
                              "type": "number"
                            }
                          }
                        },
                        "expressions": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "number"
                          }
                        },
                        "score": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request (missing guide ID or content)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "410": {
            "description": "Guide expired. Guides are locked 180 days after their creation, because the Google results they are built on are no longer representative. The body is `{ success: false, error: \"Guide expired\", expiresAt: \"<ISO 8601 expiry date>\" }`. Definitive: do not retry, create a new guide on the same keyword. Nothing is analysed, nothing is saved and nothing is charged."
          },
          "429": {
            "description": "Too many score requests: at most 60 calls per UTC minute and per account, calls served from the cache included (without an account, per guide). Nothing is analysed and nothing is saved. Wait the number of seconds given by `Retry-After` (also `retryAfter` in the body) before calling again: earlier calls are refused the same way. Repeating an unchanged request is not needed, an identical result stays available from the cache for 10 minutes.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the end of the current minute, between 1 and 60.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "example": "Too many score requests"
                    },
                    "code": {
                      "type": "string",
                      "example": "rate_limited"
                    },
                    "limit": {
                      "type": "integer",
                      "example": 60
                    },
                    "retryAfter": {
                      "type": "integer",
                      "example": 42
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "DatabaseUnavailable": {
        "description": "Service temporarily unavailable: the database could not be reached while authenticating the request. Possible on EVERY authenticated endpoint. Your API key is NOT in question: do not regenerate or revoke it. Retry after the number of seconds given by the `Retry-After` header. Never returned as 401, 403, 404 or 410.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying (30).",
            "schema": {
              "type": "integer",
              "example": 30
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "error": {
                  "type": "string",
                  "example": "Service temporarily unavailable"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "database_unavailable"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key"
      }
    },
    "schemas": {
      "GuideCorpusTopWindow": {
        "type": "object",
        "description": "Present ONLY on a guide built from pages ranked beyond the top 10 (shared by /guide and /guides). None of the first 10 results of the search engine could be kept for the analysis, so the guide relies on the next ranked pages instead of failing. Absent on every other guide, which is the normal case: treat a missing field as \"built on the top 10\". Positions are the real ranks, never renumbered.",
        "properties": {
          "fallback": {
            "type": "boolean",
            "description": "Always `true` when the object is present.",
            "example": true
          },
          "maxPosition": {
            "type": "integer",
            "description": "Rank of the last page the guide relies on (30 at most).",
            "example": 22
          },
          "documents": {
            "type": "integer",
            "description": "Number of pages the guide relies on.",
            "example": 3
          }
        }
      },
      "GuideProgressStatus": {
        "type": "object",
        "description": "Creation progress of a guide (shared by /guide and /guides). When `step` is `failed`, `generationFailed`, `terminal`, `reason`, `message` and `kind` are set, and `failureFacts` may be (only for sources whose web pages we read).",
        "properties": {
          "isReady": {
            "type": "boolean",
            "description": "True once the guide is ready to be read"
          },
          "pct": {
            "type": "number",
            "description": "Creation progress in percent (0 to 100)"
          },
          "step": {
            "type": "string",
            "enum": [
              "notStarted",
              "queued",
              "created",
              "serpFetched",
              "pagesCrawled",
              "ready",
              "failed"
            ],
            "description": "Current creation step. `queued` (returned by GET /api/v1/guides, and by the 202 of GET /api/v1/guide): the guide has not started and is held because the account is beyond the priority guides of its plan for the current month; held guides start at a rate of one per hour, in creation order. See `estimatedStartAt`."
          },
          "estimatedStartAt": {
            "type": "string",
            "format": "date-time",
            "description": "Only when step is `queued`. Estimated start of the generation (UTC). An estimate, not a promise: the guide then moves to `created` and the usual steps."
          },
          "upgradeUrl": {
            "type": "string",
            "description": "Only when step is `queued`, and only when the \"Priority and API volume\" add-on can raise this account's monthly volume. Absolute URL where to buy it."
          },
          "requiresValidation": {
            "type": "boolean",
            "description": "Present and true when the guide waits for the user to validate the results to analyse"
          },
          "generationFailed": {
            "type": "boolean",
            "description": "Present and true when the creation failed (step `failed`)"
          },
          "terminal": {
            "type": "boolean",
            "description": "Only when step is `failed`. True when the failure is definitive (explicit backend flag, or inferred when the failure is older than 15 minutes, see `terminalInferred`). False while the backend may still retry (failure younger than 15 minutes without the explicit flag)."
          },
          "terminalInferred": {
            "type": "boolean",
            "description": "Only when step is `failed` and `terminal` is true without the explicit backend flag (failure older than 15 minutes, no retry will happen). Present and true in that case only. The refund status is then given by the `refunded` field of /guide, never promised in `message`."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Only when step is `failed`. Raw, stable failure reason slug",
            "enum": [
              "exception-creation",
              "no-serp-results",
              "not-enough-serp-results",
              "not-enough-fetchable-pages",
              "no-top10-fetchable-pages",
              "failed-to-save-serp-results",
              "exception-fetch-serp",
              "exception-fetch-results",
              "exception-fetch-llm-citations",
              "no-citations",
              "unsupported-locale",
              "exception-fetch-llm-content",
              "no-ai-overview-available",
              "exception-fetch-ai-overview",
              "exception-process-corpus",
              "exception-wrap-up",
              "failed-task",
              "relaunch_count_exceeded"
            ]
          },
          "message": {
            "type": "string",
            "description": "Only when step is `failed`. Human-readable French message, depending on the reason and the source of the guide",
            "example": "Pas assez de sources exploitables : ChatGPT a cité trop peu de pages accessibles pour cette requête. Essayez une requête plus générale, ou la source Google. Vos crédits sont recrédités automatiquement."
          },
          "kind": {
            "type": "string",
            "enum": [
              "legitimate",
              "technical"
            ],
            "description": "Only when step is `failed`. `legitimate`: the query or the source does not allow a guide to be built (no results, too few readable pages, no AI Overview, unsupported language); nothing is broken, change the query or the source. `technical`: an error on our side, including any unknown reason."
          },
          "failureFacts": {
            "type": "object",
            "description": "Only when step is `failed`, only for the reasons `not-enough-fetchable-pages` and `no-top10-fetchable-pages`, only for sources whose web pages we read (a search engine such as `GOOGLE` or `BING`, or a `_CITATIONS` source), and only when the figures could be read and cross-checked. Never set for a source in answer mode (`GOOGLE_AI_OVERVIEW`, `CHATGPT`, `PERPLEXITY`, ... without `_CITATIONS`): those guides are built from AI answers, no web page is read. Absent otherwise: never assume 0. Always `attempted = readable + notAnalyzable + invalidContent + unreachable`.",
            "required": [
              "attempted",
              "readable",
              "unreadable"
            ],
            "properties": {
              "attempted": {
                "type": "integer",
                "description": "Number of results whose page we tried to read.",
                "example": 22
              },
              "readable": {
                "type": "integer",
                "description": "Number of pages read and usable for the analysis.",
                "example": 1
              },
              "unreadable": {
                "type": "object",
                "description": "Pages that could not be read, by cause.",
                "required": [
                  "notAnalyzable",
                  "invalidContent",
                  "unreachable"
                ],
                "properties": {
                  "notAnalyzable": {
                    "type": "integer",
                    "description": "Pages on sites we do not analyze (social networks, video platforms).",
                    "example": 14
                  },
                  "invalidContent": {
                    "type": "integer",
                    "description": "Pages whose content is too short or unreadable.",
                    "example": 5
                  },
                  "unreachable": {
                    "type": "integer",
                    "description": "Pages that are unreachable or protected.",
                    "example": 2
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": []
}