{
  "openapi": "3.0.0",
  "paths": {
    "/v1/capabilities": {
      "get": {
        "description": "The catalogue, as this credential sees it. **Not a fixed list**: it is computed per call from the plan, the permissions of the bearer, the allowlist of the consent that produced the token, and the project the token carries. Two credentials on the same project can see different catalogues, and the same credential can see a different one tomorrow.\n\nEach entry carries its `paramsSchema` — the JSON Schema of what `invoke` accepts. An empty `properties` means the capability takes no parameters, and nothing else.\n\n**Read `writesOnInvoke` before calling anything.** It marks the capabilities whose invocation already starts the work and consumes quota — there is no dry run for those. The others declared `propose` only compute a diff: calling them changes nothing until `applyProposal` follows.",
        "operationId": "listCapabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The catalogue as this credential sees it, with the JSON Schema of each capability's parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CapabilityListingDto"
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "List the capabilities this credential may use",
        "tags": [
          "Capabilities"
        ]
      }
    },
    "/v1/capabilities/{capabilityId}/invoke": {
      "post": {
        "description": "Runs one capability from the catalogue. **Three shapes of success**, and a client has to handle all three:\n\n- `value` — the result, and the interaction is over.\n- `input_required` — a questionnaire to fill and send back through `requestState`. The handle is single-use.\n- `proposal` — a computed diff. **Nothing has been written**; `applyProposal` decides.\n\nThe project is never a parameter. It comes from the credential, and a scope key sent in `params` at any depth is refused with `scope_in_params` rather than ignored — silently dropping it would let a caller believe they had switched project.\n\nA capability the catalogue marks `writesOnInvoke` has already done its work when this returns. Ask before calling one, not after.",
        "operationId": "invokeCapability",
        "parameters": [
          {
            "name": "capabilityId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$",
              "example": "audit.overview.get"
            },
            "description": "The capability to run, exactly as `GET /capabilities` returns it. This surface does not resolve an approximate name: an unknown identifier is refused with `capability_unknown`, and the response carries a `suggestions` array — the cause is almost always a typo."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvokeCapabilityDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One of three shapes, told apart by `resultType` — a client has to handle all of them. `value`: the result, in `data`. `input_required`: a questionnaire, answered through `requestState`. `proposal`: a computed diff — nothing has been written.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CapabilityValueResultDto"
                    },
                    {
                      "$ref": "#/components/schemas/CapabilityInputRequiredResultDto"
                    },
                    {
                      "$ref": "#/components/schemas/CapabilityProposalResultDto"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "resultType",
                    "mapping": {
                      "value": "#/components/schemas/CapabilityValueResultDto",
                      "input_required": "#/components/schemas/CapabilityInputRequiredResultDto",
                      "proposal": "#/components/schemas/CapabilityProposalResultDto"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is malformed, not forbidden. `capability_not_invokable` — a write capability is not called directly, it is reached through the proposal it belongs to. `scope_in_params` — an organisation or project identifier was sent in the body. `project_required` — the capability is project-scoped and the credential carries no project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "409": {
            "description": "`invalid_handle` — the `requestState` is forged, expired, already used, or belongs to another principal. Resubmit the request that produces a fresh one; do not retry with the same. `project_unavailable` — the project exists but is not in a state that allows this now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Run a capability — returns a value, a questionnaire, or a proposal",
        "tags": [
          "Capabilities"
        ]
      }
    },
    "/v1/proposals/{proposalId}/apply": {
      "post": {
        "description": "Applies a diff produced by a `propose` capability. **The diff is recomputed and compared against the stored one before anything is written** — this is the only write path, and it refuses rather than overwrites.\n\nA proposal is applied once. `diff_stale` means someone — a person or another agent — changed the target between the proposal and the apply, and applying now would silently discard their change. It is the most common refusal here, and it is a safety net, not a fault: recompute and review the fresh diff.\n\nPublishing to an external network is never part of this. No capability declares it; it stays a human gesture.",
        "operationId": "applyProposal",
        "parameters": [
          {
            "name": "proposalId",
            "required": true,
            "in": "path",
            "description": "The proposal to apply, as returned by the `propose` capability that computed it. A proposal belongs to the principal that created it and is applied once.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The same envelope `invokeCapability` returns. **Applying yields a `value`**: the change was written, and `data` describes what was written. A `proposal` here would mean the apply itself proposed something further, which it does not.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CapabilityValueResultDto"
                    },
                    {
                      "$ref": "#/components/schemas/CapabilityInputRequiredResultDto"
                    },
                    {
                      "$ref": "#/components/schemas/CapabilityProposalResultDto"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "resultType",
                    "mapping": {
                      "value": "#/components/schemas/CapabilityValueResultDto",
                      "input_required": "#/components/schemas/CapabilityInputRequiredResultDto",
                      "proposal": "#/components/schemas/CapabilityProposalResultDto"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "409": {
            "description": "The proposal cannot be applied as it stands. `diff_stale` — the content changed since it was computed. `expired` — proposals are short-lived on purpose. `not_pending` — already applied or discarded. `not_found` — no proposal by that identifier. `diff_forbidden` — the diff reaches a field this capability may not write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "422": {
            "description": "`diff_unsafe_content` — the output filter refused what the proposal carried. The call was well-formed and authorised, hence 422. The offending detail stays in our logs: echoing it back would make the refusal a way to read what it blocks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Apply a proposal — the diff is recomputed and compared before writing",
        "tags": [
          "Capabilities"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/overview": {
      "get": {
        "description": "Aggregated scores for every audit module — local visibility, online reputation, online presence, content, analytics, AI discoverability — plus a comparison radar against the competitor benchmark.\n\n**Read the confidence fields before the numbers.** `competitorAvg` is `null` whenever no retained competitor has been measured on that pillar, and `competitorAvgReason` then says why (`cohort-below-minimum` or `no-competitor-measured-this-pillar`). `competitorPosture` is a verdict on the **cohort** — its coverage and freshness over a 30-day window — never on an individual axis: a `High Confidence` cohort can still carry a `null` average on a pillar nobody measured.\n\nCompetitors themselves are not scored here. Read them through `GET /competitors`, whose `assetFreshness` measures something else entirely — crawl assets, roughly a day — and not the audit freshness `competitorPosture` is built on. Two different clocks.",
        "operationId": "getAuditOverview",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemaVersion": {
                      "type": "string",
                      "description": "Version of this payload's shape. Increments on breaking changes only.",
                      "example": "2"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generatedAt": {
                          "type": "string",
                          "description": "When the overview was assembled — not when each module last ran. A\nmodule's own freshness is on its `moduleScores` entry.",
                          "example": "2026-09-24T06:12:03.000Z"
                        },
                        "period": {
                          "type": "string",
                          "description": "The period the figures cover.",
                          "example": "2026-08-25/2026-09-24"
                        },
                        "trendWindow": {
                          "type": "string",
                          "description": "The window each `trend` compares against.",
                          "example": "30d"
                        }
                      },
                      "description": "When and over what window this payload was computed."
                    },
                    "project": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable identifier of the project.",
                          "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                        },
                        "brandName": {
                          "type": "string",
                          "description": "The brand's name, as the project declared it. Display it; do not\nkey off it.",
                          "example": "Acme Corporation"
                        },
                        "websiteUrl": {
                          "type": "string",
                          "description": "The project's website. An empty string, not `null`, for a project\nwith no site.",
                          "example": "https://www.acme.example"
                        }
                      },
                      "description": "The project the overview is about, as it is named in the product."
                    },
                    "kpis": {
                      "type": "object",
                      "properties": {
                        "brandPresenceScore": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "number",
                              "description": "The headline score, out of 100. `null` means it could not be computed —\nnever read it as a zero.",
                              "example": 72
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "points"
                              ],
                              "description": "Unit of `value`. Always `points`; it is stated so a client never has to\nassume the scale.",
                              "example": "points"
                            },
                            "label": {
                              "type": "string",
                              "description": "Localised label for the KPI. Display it; never key off it.",
                              "example": "Brand presence"
                            },
                            "dimensions": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "key": {
                                    "type": "string",
                                    "enum": [
                                      "authority",
                                      "coherence",
                                      "coverage",
                                      "relevance"
                                    ],
                                    "description": "Which of the four dimensions this entry scores.",
                                    "example": "coverage"
                                  },
                                  "value": {
                                    "type": "number",
                                    "description": "The dimension's score, out of 100. `null` when that dimension could\nnot be measured — the others stay readable.",
                                    "example": 74
                                  }
                                }
                              }
                            },
                            "trend": {
                              "type": "object",
                              "properties": {
                                "delta": {
                                  "type": "number",
                                  "description": "Change in `value` over `period`, in the KPI's own unit. Negative\nmeans it fell.",
                                  "example": 3.4
                                },
                                "unit": {
                                  "type": "string",
                                  "description": "Unit of `delta`.",
                                  "example": "points"
                                },
                                "period": {
                                  "type": "string",
                                  "description": "The window `delta` is measured over, such as `30d`.",
                                  "example": "30d"
                                }
                              },
                              "description": "Movement over `period`, absent for a first run with nothing to compare\nagainst."
                            }
                          },
                          "description": "The headline KPI of the overview, broken down into its four\ndimensions. Absent while no module has produced enough to compose it."
                        },
                        "globalScore": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "number",
                              "description": "The audit's overall score, out of 100.",
                              "example": 68
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "points"
                              ],
                              "description": "Unit of `value`. Always `points`.",
                              "example": "points"
                            },
                            "trend": {
                              "type": "object",
                              "properties": {
                                "delta": {
                                  "type": "number",
                                  "description": "Change in the overall score over `period`, in points. Negative\nmeans it fell.",
                                  "example": 2.1
                                },
                                "unit": {
                                  "type": "string",
                                  "description": "Unit of `delta`.",
                                  "example": "points"
                                },
                                "period": {
                                  "type": "string",
                                  "description": "The window `delta` is measured over, such as `30d`.",
                                  "example": "30d"
                                }
                              }
                            }
                          },
                          "description": "The audit's overall score, the average of the module scores that are\navailable — a module scored `null` is left out, not counted as zero."
                        },
                        "compositeActionScore": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "number",
                              "description": "The action-plan score, out of 100. `null` when no task could be\nweighed.",
                              "example": 61
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "points"
                              ],
                              "description": "Unit of `value`. Always `points`.",
                              "example": "points"
                            },
                            "rationale": {
                              "type": "string",
                              "description": "One sentence naming what drives the score, for a reader who cannot\nsee the tasks themselves.",
                              "example": "Twelve of the thirty-one tasks are done, including two of the three rated critical."
                            }
                          },
                          "description": "How much of the action plan has actually been acted on, weighted by\nthe impact of each task rather than by their count."
                        },
                        "openTasks": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "Every task across modules, done and open together.",
                              "example": 31
                            },
                            "done": {
                              "type": "number",
                              "description": "Tasks already completed.",
                              "example": 12
                            },
                            "open": {
                              "type": "number",
                              "description": "Tasks still to do. `done + open` equals `total`.",
                              "example": 19
                            },
                            "trend": {
                              "type": "object",
                              "properties": {
                                "delta": {
                                  "type": "number",
                                  "description": "Change in the number of open tasks over `period`. Negative is\nprogress: fewer tasks open than before.",
                                  "example": -4
                                },
                                "unit": {
                                  "type": "string",
                                  "description": "Unit of `delta`.",
                                  "example": "tasks"
                                },
                                "period": {
                                  "type": "string",
                                  "description": "The window `delta` is measured over, such as `30d`.",
                                  "example": "30d"
                                }
                              }
                            }
                          },
                          "description": "The action plan in counts. `total` covers every task across modules,\nso `done + open` equals it."
                        },
                        "potentialGrowthPct": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "number",
                              "description": "The modelled upside from the open tasks, in percent, capped at\n`cappedAt`.",
                              "example": 24
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "%"
                              ],
                              "description": "Unit of `value`. Always `%`.",
                              "example": "%"
                            },
                            "cappedAt": {
                              "type": "number",
                              "description": "The ceiling applied to `value`. A model that projects beyond this is\ncut back rather than published, because nothing above it is credible.",
                              "example": 40
                            },
                            "rationale": {
                              "type": "string",
                              "description": "What the projection rests on, in one sentence.",
                              "example": "Closing the nineteen open tasks would lift coverage on four of the six pillars."
                            }
                          },
                          "description": "The upside modelled from the open tasks, as a percentage. A model, not\na promise, and capped on purpose — see `cappedAt`."
                        }
                      }
                    },
                    "scoreEvolution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "The global score over time, one entry per run kept. The shape of an\nentry is not part of this contract: read it for charting, not for logic."
                      },
                      "description": "The global score over time, one entry per run kept. The shape of an\nentry is not part of this contract: read it for charting, not for logic."
                    },
                    "radar": {
                      "type": "object",
                      "properties": {
                        "normalized": {
                          "type": "boolean",
                          "description": "Whether the axis values are already on the same scale. `true` means\nno client-side rescaling is needed.",
                          "example": true
                        },
                        "max": {
                          "type": "number",
                          "description": "The top of the scale used by every axis.",
                          "example": 100
                        },
                        "axes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string",
                                "description": "Stable identifier of the radar pillar.",
                                "example": "content"
                              },
                              "label": {
                                "type": "string",
                                "description": "Human label for the pillar, localised.",
                                "example": "Content"
                              },
                              "you": {
                                "type": "number",
                                "description": "This project's score on the pillar, out of 100.",
                                "example": 64
                              },
                              "sectorAvg": {
                                "type": "number",
                                "description": "Sector benchmark for the pillar, out of 100. A modelled reference, not a measured cohort.",
                                "example": 58
                              },
                              "competitorAvg": {
                                "type": "number",
                                "description": "Average across the retained competitors measured on this pillar, out of\n100. `null` whenever no retained competitor was measured here — and\n`competitorAvgReason` then says which of the two causes applies. Do not read\n`null` as a zero, and do not infer coverage from `competitorPosture`: that\nverdict qualifies the cohort, never an individual axis.",
                                "example": 61
                              },
                              "competitorAvgReason": {
                                "type": "string",
                                "enum": [
                                  "cohort-below-minimum",
                                  "no-competitor-measured-this-pillar"
                                ],
                                "description": "Why `competitorAvg` is `null`; absent when a value exists.\n`cohort-below-minimum`: the cohort itself is under the retention threshold.\n`no-competitor-measured-this-pillar`: the cohort is sufficient, but no\nretained competitor measured this pillar.",
                                "example": "no-competitor-measured-this-pillar"
                              }
                            }
                          }
                        },
                        "competitorSampleSize": {
                          "type": "number",
                          "description": "How many retained competitors `competitorAvg` averages. `0` means the\ncomparison columns carry no measurement at all.",
                          "example": 4
                        },
                        "competitorCoverage": {
                          "type": "string",
                          "enum": [
                            "complete",
                            "partial",
                            "unavailable"
                          ],
                          "description": "Whether the competitor cohort is large enough to compare against.\n`partial` means it is under the retention threshold, `unavailable` that no\ncompetitor was audited. It qualifies the COHORT, never a single axis.",
                          "example": "complete"
                        },
                        "competitorPosture": {
                          "type": "string",
                          "description": "The confidence verdict on the benchmark — coverage AND freshness of\nthe competitor audits, where `competitorCoverage` only speaks to size.",
                          "example": "High Confidence"
                        }
                      },
                      "description": "The pillar-by-pillar comparison: this project, its sector, and the\nretained competitors."
                    },
                    "moduleScores": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "moduleId": {
                            "type": "string",
                            "description": "Stable identifier of the audit module. Branch on this, not on `moduleName`.",
                            "example": "content-audit"
                          },
                          "moduleName": {
                            "type": "string",
                            "description": "Human label for the module, localised. Display it; never key off it.",
                            "example": "Content"
                          },
                          "score": {
                            "type": "number",
                            "description": "Score out of 100. `null` means the score was not produced — read\n`scoreAvailabilityState` and `scoreSuppressionReason` to learn why. A `null`\nscore is never a zero score.",
                            "example": 58
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "failed",
                              "in-progress",
                              "not-started",
                              "queued"
                            ],
                            "description": "Where the module's run stands. `completed` says the run finished, not\nthat a score exists: a completed run can still carry `score: null`.",
                            "example": "completed"
                          },
                          "readinessState": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "degraded",
                              "failed",
                              "in-progress",
                              "not-started",
                              "queued"
                            ],
                            "description": "Where this module's own run stands. `status` and this field agree; this\none carries `degraded`, which says the run produced something usable while\nfailing part of its work.",
                            "example": "completed"
                          },
                          "freshnessState": {
                            "type": "string",
                            "enum": [
                              "current",
                              "degraded-current",
                              "stale-last-known",
                              "unavailable"
                            ],
                            "description": "How recent the underlying data is, independently of whether the run succeeded.",
                            "example": "current"
                          },
                          "scoreAvailabilityState": {
                            "type": "string",
                            "enum": [
                              "available",
                              "degraded-computation",
                              "stale-last-known",
                              "suppressed-incomplete-coverage",
                              "unavailable"
                            ],
                            "description": "Why the score is, or is not, usable. `suppressed-incomplete-coverage`\nmeans the module ran but measured too little to score honestly;\n`stale-last-known` means the number is real but from an earlier run.",
                            "example": "available"
                          },
                          "entitlement": {
                            "type": "object",
                            "properties": {
                              "requiredPlan": {
                                "type": "string",
                                "description": "The plan that would unlock this module, when the current one does not.\nAbsent when nothing is gated.",
                                "example": "growth"
                              }
                            }
                          },
                          "scoreSuppressionReason": {
                            "type": "string",
                            "description": "Free-text reason accompanying a suppressed score. Present only when `score` is `null`.",
                            "example": "Only 8 of 40 pages could be measured."
                          },
                          "completedAt": {
                            "type": "string",
                            "description": "When this module's run finished. Absent while it has not.",
                            "example": "2026-09-24T06:04:22.000Z"
                          },
                          "tasks": {
                            "type": "object",
                            "properties": {
                              "total": {
                                "type": "number",
                                "description": "Every task of this module, done and open together.",
                                "example": 9
                              },
                              "done": {
                                "type": "number",
                                "description": "Tasks of this module already completed.",
                                "example": 4
                              },
                              "open": {
                                "type": "number",
                                "description": "Tasks of this module still to do. `done + open` equals `total`.",
                                "example": 5
                              }
                            },
                            "description": "This module's share of the action plan, in counts."
                          }
                        }
                      },
                      "description": "One entry per audit module, with its score and why it is usable."
                    },
                    "pillars": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "The pillars behind the radar, with their composition. The shape of an\nentry is not part of this contract."
                      },
                      "description": "The pillars behind the radar, with their composition. The shape of an\nentry is not part of this contract."
                    },
                    "entitlement": {
                      "type": "object",
                      "properties": {
                        "requiredPlan": {
                          "type": "string",
                          "description": "The plan that would unlock this module, when the current one does not.\nAbsent when nothing is gated.",
                          "example": "growth"
                        }
                      }
                    },
                    "coherenceFlags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "data_mismatch",
                              "missing_data",
                              "score_gap",
                              "stale_data"
                            ],
                            "description": "What kind of inconsistency was detected between modules.",
                            "example": "score_gap"
                          },
                          "severity": {
                            "type": "string",
                            "enum": [
                              "error",
                              "info",
                              "warning"
                            ],
                            "description": "How much it should weigh on how you read the overview. `error` means\nthe numbers of the named modules should not be compared as they stand.",
                            "example": "warning"
                          },
                          "modules": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The modules the flag is about, by `moduleId`.",
                              "example": "content-audit"
                            },
                            "example": [
                              "content-audit",
                              "analytics-audit"
                            ],
                            "description": "The modules the flag is about, by `moduleId`."
                          },
                          "message": {
                            "type": "string",
                            "description": "The finding in one sentence, ready to show to a reader.",
                            "example": "Content and analytics disagree on the number of indexed pages by more than 20%."
                          }
                        }
                      }
                    },
                    "readinessState": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "degraded",
                        "failed",
                        "in-progress",
                        "not-started",
                        "queued"
                      ],
                      "description": "Where the overview's own run stands. `completed` says the run\nfinished, not that every score exists — read `scoreAvailabilityState` for\nthat, and the per-module states for which one is missing.",
                      "example": "completed"
                    },
                    "freshnessState": {
                      "type": "string",
                      "enum": [
                        "current",
                        "degraded-current",
                        "stale-last-known",
                        "unavailable"
                      ],
                      "description": "How recent the data behind the figures is, independently of whether\nthe run succeeded. `stale-last-known` means real numbers from an earlier\nrun, which is not the same as no numbers.",
                      "example": "current"
                    },
                    "scoreAvailabilityState": {
                      "type": "string",
                      "enum": [
                        "available",
                        "degraded-computation",
                        "stale-last-known",
                        "suppressed-incomplete-coverage",
                        "unavailable"
                      ],
                      "description": "Whether the global score can be used as it stands.\n`suppressed-incomplete-coverage` means too little was measured to score\nhonestly — the figure is withheld rather than lowered.",
                      "example": "available"
                    },
                    "staleFallbackExpiresAt": {
                      "type": "string",
                      "description": "When a `stale-last-known` payload stops being served. Past this\nmoment the figures are withheld instead of shown as old ones.",
                      "example": "2026-09-27T06:12:03.000Z"
                    },
                    "scoreSuppressionReason": {
                      "type": "string",
                      "description": "Why the global score is withheld, in one sentence. Present only when\n`scoreAvailabilityState` is not `available`.",
                      "example": "Content coverage below the scoring threshold: 8 of 40 pages measured."
                    },
                    "blockingComponents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "componentKey": {
                            "type": "string",
                            "description": "Which component, in the same vocabulary as `moduleScores[].moduleId`\nwhere the two overlap.",
                            "example": "content-audit.crawl"
                          },
                          "readinessState": {
                            "type": "string",
                            "description": "Where this component's run stands. Same vocabulary as the module\nstates above.",
                            "example": "failed"
                          },
                          "freshnessState": {
                            "type": "string",
                            "description": "How recent this component's data is.",
                            "example": "unavailable"
                          },
                          "scoreAvailabilityState": {
                            "type": "string",
                            "description": "Whether this component's score can be used. A component that is\nblocking is usually one whose score is not `available`.",
                            "example": "unavailable"
                          },
                          "blocking": {
                            "type": "boolean",
                            "description": "Whether this component is what holds the overview back. A component\ncan be failed without blocking anything you asked for.",
                            "example": true
                          },
                          "retryable": {
                            "type": "boolean",
                            "description": "Whether retrying can change the outcome. `false` means the failure is\nstructural — retrying spends a run for nothing.",
                            "example": true
                          },
                          "retryCount": {
                            "type": "number",
                            "description": "How many times it has already been retried.",
                            "example": 2
                          },
                          "startedAt": {
                            "type": "string",
                            "description": "When the component's latest run started.",
                            "example": "2026-09-24T05:58:11.000Z"
                          },
                          "completedAt": {
                            "type": "string",
                            "description": "When it finished. Absent while it has not, or when it failed\nbefore completing.",
                            "example": "2026-09-24T06:01:47.000Z"
                          },
                          "lastErrorCode": {
                            "type": "string",
                            "description": "Machine-readable cause, stable across wordings. Branch on this.",
                            "example": "crawl_blocked_by_robots"
                          },
                          "lastErrorMessage": {
                            "type": "string",
                            "description": "The cause in prose, for a reader. Do not parse it.",
                            "example": "robots.txt disallows the crawler on 31 of 40 pages."
                          }
                        }
                      },
                      "description": "The components standing between this overview and a complete one. Empty\nor absent when nothing is missing. This is what to show a user asking why a\nfigure is not there — and what to retry on."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Cross-module audit overview",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/online-presence": {
      "get": {
        "description": "Results of the project's most recent online-presence audit: social networks, search engines, platforms, and the recommendations derived from them.\n\nAn absent platform and an unreachable platform are **not** the same thing, and the payload keeps them apart. Treating a fetch failure as an absence would report a problem the project does not have.",
        "operationId": "getOnlinePresenceAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "Project ID this audit belongs to",
                      "example": "550e8400-e29b-41d4-a716-446655440000"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "failed",
                        "in-progress",
                        "queued"
                      ],
                      "description": "Current audit status",
                      "example": "completed"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "UTC timestamp when audit was created",
                      "example": "2026-01-15T10:00:00Z"
                    },
                    "completedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "UTC timestamp when audit completed",
                      "example": "2026-01-15T10:03:30Z"
                    },
                    "readinessState": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "degraded",
                        "failed",
                        "in-progress",
                        "not-started",
                        "queued"
                      ],
                      "description": "Ledger-backed readiness state for this module",
                      "example": "completed"
                    },
                    "freshnessState": {
                      "type": "string",
                      "enum": [
                        "current",
                        "degraded-current",
                        "stale-last-known",
                        "unavailable"
                      ],
                      "description": "Ledger-backed freshness state for this module",
                      "example": "current"
                    },
                    "scoreAvailabilityState": {
                      "type": "string",
                      "enum": [
                        "available",
                        "degraded-computation",
                        "stale-last-known",
                        "suppressed-incomplete-coverage",
                        "unavailable"
                      ],
                      "description": "Score availability state for this module",
                      "example": "available"
                    },
                    "scoreSuppressionReason": {
                      "type": "string",
                      "description": "Reason why the module score is suppressed or degraded",
                      "example": "Fewer than two platforms could be measured, so the score was withheld."
                    },
                    "globalScore": {
                      "type": "number",
                      "description": "Global online presence score (0-100). Weighted: 40% social + 30% search + 30% platforms",
                      "example": 65
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "socialNetworks": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "How many social networks were checked.",
                              "example": 6
                            },
                            "active": {
                              "type": "number",
                              "description": "Networks showing recent activity. A subset of those present.",
                              "example": 3
                            },
                            "present": {
                              "type": "number",
                              "description": "Networks where the brand has a profile, active or not. `present =\nactive + inactive`.",
                              "example": 5
                            },
                            "inactive": {
                              "type": "number",
                              "description": "Networks where a profile exists but shows no recent activity.",
                              "example": 2
                            },
                            "totalFollowers": {
                              "type": "number",
                              "description": "Followers summed over the profiles that could be measured. A profile\nwhose count could not be read adds nothing — it does not add zero and it\nis not counted as measured.",
                              "example": 8420
                            },
                            "totalPosts": {
                              "type": "number",
                              "description": "Posts summed over the profiles that could be measured.",
                              "example": 312
                            },
                            "engagementRate": {
                              "type": "number",
                              "description": "Average engagement rate over the measured profiles, in percent.",
                              "example": 3.8
                            },
                            "lastActivity": {
                              "type": "string",
                              "description": "The most recent activity seen on any profile, as a date. `null` when\nno profile could be measured.",
                              "example": "2026-09-21"
                            }
                          }
                        },
                        "searchEngines": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "How many search engines were checked.",
                              "example": 3
                            },
                            "indexed": {
                              "type": "number",
                              "description": "Engines that index the site.",
                              "example": 2
                            },
                            "notIndexed": {
                              "type": "number",
                              "description": "Engines that do not — or that could not be checked, which is worth\nconfirming before acting on it.",
                              "example": 1
                            },
                            "coverage": {
                              "type": "number",
                              "description": "Share of engines that index the site, in percent.",
                              "example": 67
                            }
                          }
                        },
                        "platforms": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "How many platforms and directories were checked.",
                              "example": 8
                            },
                            "present": {
                              "type": "number",
                              "description": "Where the brand was found.",
                              "example": 5
                            },
                            "missing": {
                              "type": "number",
                              "description": "Where it was expected and not found.",
                              "example": 3
                            },
                            "coverage": {
                              "type": "number",
                              "description": "Share of platforms where the brand is present, in percent.",
                              "example": 63
                            }
                          }
                        },
                        "globalScore": {
                          "type": "number",
                          "description": "The overall online-presence score, out of 100. Weighted: 40% social\nnetworks, 30% search engines, 30% platforms — so it is not the mean of\nthe three coverage figures.",
                          "example": 65
                        }
                      },
                      "description": "Online presence statistics breakdown"
                    },
                    "platforms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the platform. Tasks refer to it as\n`platformId`.",
                            "example": "instagram"
                          },
                          "name": {
                            "type": "string",
                            "description": "The platform's display name.",
                            "example": "Instagram"
                          },
                          "category": {
                            "type": "string",
                            "enum": [
                              "directory",
                              "marketplace",
                              "search",
                              "social"
                            ],
                            "description": "What kind of platform it is. It groups results and decides which\nweights apply.",
                            "example": "social"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "missing",
                              "partial",
                              "present"
                            ],
                            "description": "Whether the brand is there. `partial` means a profile exists but is\nincomplete or could not be confirmed as the brand's own.",
                            "example": "present"
                          },
                          "priority": {
                            "type": "string",
                            "description": "How much this platform matters for this business, in the audit's own\nwords. A label, not a rank to sort on.",
                            "example": "high"
                          },
                          "weight": {
                            "type": "number",
                            "description": "How much it weighs in the global score, between 0 and 1.",
                            "example": 0.15
                          },
                          "score": {
                            "type": "number",
                            "description": "This platform's score, out of 100. `null` for a profile that is\npresent but could not be measured — never a misleading 100.",
                            "example": 72
                          },
                          "link": {
                            "type": "string",
                            "description": "The profile or listing that was found. Absent when the brand is\nmissing.",
                            "example": "https://social.example/acme"
                          },
                          "data": {
                            "type": "object",
                            "properties": {
                              "followers": {
                                "type": "number",
                                "description": "Follower count. `null` when it could not be read (scrape blocked)\n— never a fabricated 0.",
                                "example": 2140
                              },
                              "posts": {
                                "type": "number",
                                "description": "Number of posts. `null` when it could not be read.",
                                "example": 186
                              },
                              "engagement": {
                                "type": "number",
                                "description": "Engagement rate in percent. `null` when it could not be read.",
                                "example": 3.2
                              },
                              "measured": {
                                "type": "boolean",
                                "description": "`true` only when the profile is present AND at least one real\nmetric was observed. A `false` on a present profile means it exists but\nnothing could be read from it.",
                                "example": true
                              },
                              "lastPostDate": {
                                "type": "string",
                                "description": "The date of the latest post seen.",
                                "example": "2026-09-18"
                              },
                              "description": {
                                "type": "string",
                                "description": "The profile's own description text.",
                                "example": "Menuiserie sur mesure à Lyon depuis 2011."
                              },
                              "title": {
                                "type": "string",
                                "description": "The profile's own title or display name.",
                                "example": "Acme Corporation"
                              },
                              "brandMatchScore": {
                                "type": "number",
                                "description": "For an auto-discovered profile only: how well it matches the\nbrand, from 0 to 100. Below the middle of the range, verify it is\nreally the brand's.",
                                "example": 88
                              },
                              "matchConfidence": {
                                "type": "string",
                                "enum": [
                                  "high",
                                  "low",
                                  "medium"
                                ],
                                "description": "How much to trust the match. Applies to auto-discovered profiles.",
                                "example": "high"
                              },
                              "autoDetected": {
                                "type": "boolean",
                                "description": "`true` when the profile was found by search rather than linked\nfrom the homepage — which is what makes `brandMatchScore` matter.",
                                "example": false
                              },
                              "measurementError": {
                                "type": "string",
                                "description": "The raw code explaining why a measurement failed or was blocked,\nsuch as `scrape-blocked-429`. Machine-readable; branch on it, do not\ndisplay it as is.",
                                "example": "scrape-blocked-429"
                              }
                            }
                          }
                        }
                      },
                      "description": "Platform-level results (social networks, search engines, directories)"
                    },
                    "tasks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the task.",
                            "example": "task-instagram-bio"
                          },
                          "title": {
                            "type": "string",
                            "description": "What to do, in one line.",
                            "example": "Add your website link to your Instagram bio"
                          },
                          "description": {
                            "type": "string",
                            "description": "How to do it and why it matters.",
                            "example": "The bio shows no link, so visitors who find you on Instagram have no way to reach the site."
                          },
                          "status": {
                            "type": "string",
                            "description": "Where the task stands.",
                            "example": "todo"
                          },
                          "priority": {
                            "type": "string",
                            "description": "How soon to do it.",
                            "example": "high"
                          },
                          "impactPotential": {
                            "type": "number",
                            "description": "How many points of the global score doing it could recover, out of\n100. An estimate, not a guarantee.",
                            "example": 6
                          },
                          "scoreWeight": {
                            "type": "number",
                            "description": "How much this task weighs in the score, between 0 and 1.",
                            "example": 0.1
                          },
                          "category": {
                            "type": "string",
                            "enum": [
                              "directory",
                              "marketplace",
                              "search",
                              "social"
                            ],
                            "description": "Which kind of platform the task is about — the same vocabulary as\n`platforms[].category`.",
                            "example": "social"
                          },
                          "platformId": {
                            "type": "string",
                            "description": "The platform it concerns, by its `id`. Absent for a task that spans\nseveral.",
                            "example": "instagram"
                          },
                          "suggestions": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Concrete ways to do it.",
                              "example": "Paste the site URL in the bio"
                            },
                            "example": [
                              "Paste the site URL in the bio",
                              "Use a link the audit can verify"
                            ],
                            "description": "Concrete ways to do it."
                          },
                          "improvements": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What improves once it is done.",
                              "example": "Visitors from Instagram can reach the site"
                            },
                            "example": [
                              "Visitors from Instagram can reach the site"
                            ],
                            "description": "What improves once it is done."
                          }
                        }
                      },
                      "description": "Structured tasks and recommendations"
                    },
                    "recommendations": {
                      "type": "object",
                      "properties": {
                        "immediate": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do now, most consequential first.",
                            "example": "Add your website link to your Instagram bio"
                          },
                          "example": [
                            "Add your website link to your Instagram bio"
                          ],
                          "description": "What to do now, most consequential first."
                        },
                        "shortTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do within weeks.",
                            "example": "Claim the two directory listings that show no owner"
                          },
                          "example": [
                            "Claim the two directory listings that show no owner"
                          ],
                          "description": "What to do within weeks."
                        },
                        "longTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do over months.",
                            "example": "Build a steady posting rhythm on the two networks that matter most"
                          },
                          "example": [
                            "Build a steady posting rhythm on the two networks that matter most"
                          ],
                          "description": "What to do over months."
                        }
                      },
                      "description": "Structured recommendations by timeline"
                    },
                    "competitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the competitor entry.",
                            "example": "5b8d1c4e-0a72-4f33-9e61-7d2c8a95b104"
                          },
                          "name": {
                            "type": "string",
                            "description": "The competitor as the benchmark names it.",
                            "example": "Globex Joinery"
                          },
                          "url": {
                            "type": "string",
                            "description": "The URL the competitor was discovered from.",
                            "example": "https://globex.example"
                          },
                          "website": {
                            "type": "string",
                            "description": "The competitor's own website, when it differs from `url`.",
                            "example": "https://www.globex.example"
                          },
                          "domain": {
                            "type": "string",
                            "description": "The competitor's domain, in canonical form.",
                            "example": "globex.example"
                          },
                          "eligibility": {
                            "type": "string",
                            "enum": [
                              "accepted-primary",
                              "withheld"
                            ],
                            "description": "Whether the competitor is used as a primary benchmark. `withheld`\nmeans it was kept out of the comparison, not that it is irrelevant.",
                            "example": "accepted-primary"
                          },
                          "eligibilityReason": {
                            "type": "string",
                            "description": "Why it was accepted or withheld, in one sentence.",
                            "example": "Same trade, same metropolitan area, measurable on all platforms."
                          },
                          "qualificationState": {
                            "type": "string",
                            "enum": [
                              "accepted",
                              "rejected",
                              "withheld"
                            ],
                            "description": "The judge's verdict on keeping it. `withheld` means deferred.",
                            "example": "accepted"
                          },
                          "confidenceLabel": {
                            "type": "string",
                            "enum": [
                              "high-confidence",
                              "low-confidence",
                              "partial",
                              "withheld"
                            ],
                            "description": "How much to trust the comparison against this competitor. It\nqualifies the measurement, not the competitor.",
                            "example": "high-confidence"
                          },
                          "freshnessState": {
                            "type": "string",
                            "enum": [
                              "current",
                              "stale"
                            ],
                            "description": "Whether the competitor's figures are recent. `stale` ones are real\nbut from an earlier run.",
                            "example": "current"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "Its online-presence score out of 100, computed like `globalScore`\nso the two compare directly.",
                            "example": 58
                          },
                          "socialPresence": {
                            "type": "number",
                            "description": "Its social-network score, out of 100. Compare with the brand's own.",
                            "example": 64
                          },
                          "searchCoverage": {
                            "type": "number",
                            "description": "Its search-engine coverage, in percent.",
                            "example": 67
                          },
                          "platformCoverage": {
                            "type": "number",
                            "description": "Its platform and directory coverage, in percent.",
                            "example": 50
                          },
                          "stats": {
                            "type": "object"
                          }
                        }
                      },
                      "description": "Competitor comparison data for benchmarking"
                    },
                    "scoreHistory": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "description": "When that run completed. Points are runs, not calendar periods, so\ntheir spacing is irregular. The last ten are kept.",
                            "example": "2026-08-27T06:21:40.000Z"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "The overall score that run produced, out of 100.",
                            "example": 61
                          }
                        }
                      },
                      "description": "Score history of the last 10 audit runs"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Online presence audit results",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/local-visibility": {
      "get": {
        "description": "Results of the project's most recent local-visibility audit — the Google Business Profile, its completeness, and local search positioning.\n\nA profile the audit *detected* is not a profile the project has *connected*: detection comes from a public lookup, connection from an OAuth grant. Only the second gives write access.",
        "operationId": "getLocalVisibilityAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "Project ID this audit belongs to",
                      "example": "550e8400-e29b-41d4-a716-446655440000"
                    },
                    "businessName": {
                      "type": "string",
                      "description": "Business name used for audit",
                      "example": "Acme Corporation"
                    },
                    "address": {
                      "type": "string",
                      "description": "Full business address used for audit",
                      "example": "123 Main Street, New York, NY 10001, USA"
                    },
                    "phone": {
                      "type": "string",
                      "description": "Business phone number used for consistency checks",
                      "example": "+1 (212) 555-1234"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "failed",
                        "in-progress",
                        "queued"
                      ],
                      "description": "Current audit status",
                      "example": "completed"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "UTC timestamp when audit was created",
                      "example": "2026-01-15T10:00:00Z"
                    },
                    "completedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "UTC timestamp when audit completed (null if in progress or failed)",
                      "example": "2026-01-15T10:03:30Z"
                    },
                    "readinessState": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "degraded",
                        "failed",
                        "in-progress",
                        "not-started",
                        "queued"
                      ],
                      "description": "Ledger-backed readiness state for this module",
                      "example": "completed"
                    },
                    "freshnessState": {
                      "type": "string",
                      "enum": [
                        "current",
                        "degraded-current",
                        "stale-last-known",
                        "unavailable"
                      ],
                      "description": "Ledger-backed freshness state for this module",
                      "example": "current"
                    },
                    "scoreAvailabilityState": {
                      "type": "string",
                      "enum": [
                        "available",
                        "degraded-computation",
                        "stale-last-known",
                        "suppressed-incomplete-coverage",
                        "unavailable"
                      ],
                      "description": "Score availability state for this module",
                      "example": "available"
                    },
                    "scoreSuppressionReason": {
                      "type": "string",
                      "description": "Reason why the module score is suppressed or degraded",
                      "example": "Fewer than two citation sources answered, so the score was withheld."
                    },
                    "canonicalScores": {
                      "type": "object",
                      "properties": {
                        "healthScore": {
                          "type": "number",
                          "description": "The overall local-visibility score, out of 100. `null` when it could\nnot be computed — never a zero.",
                          "example": 74
                        },
                        "gmbScore": {
                          "type": "number",
                          "description": "The Google Business Profile score, out of 100. `null` when no\nprofile could be read.",
                          "example": 81
                        },
                        "citationScore": {
                          "type": "number",
                          "description": "The directory-citation score, out of 100.",
                          "example": 66
                        },
                        "citationCount": {
                          "type": "number",
                          "description": "How many directories or platforms the business was found on. A\ncount, not a score.",
                          "example": 14
                        },
                        "reviewScore": {
                          "type": "number",
                          "description": "The review score, out of 100, from ratings, volume and recency\ntogether.",
                          "example": 70
                        }
                      },
                      "description": "Canonical typed scores used as source of truth for rendering"
                    },
                    "provenance": {
                      "type": "object",
                      "description": "Source provenance and confidence by signal family"
                    },
                    "businessModel": {
                      "type": "string",
                      "enum": [
                        "local-single-location",
                        "marketplace-directory",
                        "marketplace-directory-multi-city",
                        "multi-city",
                        "national-b2b"
                      ],
                      "description": "Business model classification for contextual scoring and copy",
                      "example": "local-single-location"
                    },
                    "metricSemantics": {
                      "type": "object",
                      "description": "Semantics for metric-type distinction on released local-visibility surfaces"
                    },
                    "comparisonEligibility": {
                      "type": "object",
                      "description": "Competitor comparison eligibility by section key"
                    },
                    "reviewSummary": {
                      "type": "object",
                      "properties": {
                        "totalReviews": {
                          "type": "number",
                          "description": "How many reviews were counted across the sources read.",
                          "example": 128
                        },
                        "avgRating": {
                          "type": "number",
                          "description": "Mean rating on the sources' own 5-point scale.",
                          "example": 4.4
                        },
                        "sentiment": {
                          "type": "object",
                          "properties": {
                            "positivePercentage": {
                              "type": "number",
                              "description": "Share of reviews that read as positive, in percent. The three\nshares sum to 100.",
                              "example": 78
                            },
                            "neutralPercentage": {
                              "type": "number",
                              "description": "Share of reviews that read as neutral, in percent.",
                              "example": 15
                            },
                            "negativePercentage": {
                              "type": "number",
                              "description": "Share of reviews that read as negative, in percent.",
                              "example": 7
                            }
                          },
                          "description": "Sentiment shares over all reviews counted. The three sum to 100."
                        },
                        "sourceCoverageRatio": {
                          "type": "number",
                          "description": "The share of expected review sources that answered, between 0 and 1.\nAbsent when it could not be computed.",
                          "example": 0.67
                        },
                        "coherenceState": {
                          "type": "string",
                          "enum": [
                            "coherent",
                            "mixed-source-incoherent"
                          ],
                          "description": "Whether the sources agree. `mixed-source-incoherent` means they\ndisagree enough that the average should not be read as one figure.",
                          "example": "coherent"
                        },
                        "warningReason": {
                          "type": "string",
                          "description": "Why the summary carries a warning. `null` when it does not.",
                          "example": "Sources disagree by more than one star, so the average is not one figure."
                        }
                      },
                      "description": "Coherent review summary for UI and report semantics"
                    },
                    "healthScore": {
                      "type": "number",
                      "description": "Overall health score (0-100). Weighted formula: 30% GMB + 25% Citations + 20% Reviews + 15% StructuredData + 10% LocalPages",
                      "example": 78
                    },
                    "gmbScore": {
                      "type": "number",
                      "description": "GMB score (0-100), computed from GmbProfile analysis",
                      "example": 85
                    },
                    "recommendations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the recommendation. It is what a plan-progress\nupdate refers to.",
                            "example": "rec-gmb-hours"
                          },
                          "title": {
                            "type": "string",
                            "description": "What to do, in one line.",
                            "example": "Complete your Google Business Profile hours"
                          },
                          "categoryId": {
                            "type": "string",
                            "enum": [
                              "analytics",
                              "citations",
                              "content",
                              "gbp",
                              "maps",
                              "nap",
                              "platform",
                              "review",
                              "schema",
                              "search",
                              "seo",
                              "social",
                              "technical"
                            ],
                            "description": "Which area of the audit the recommendation belongs to. It groups\nrecommendations; it does not rank them.",
                            "example": "gbp"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "high",
                              "info",
                              "low",
                              "medium"
                            ],
                            "description": "How soon to act on it. `info` is a note with nothing to do.",
                            "example": "high"
                          },
                          "observations": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "improvements": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "suggestions": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "effortEstimation": {
                            "type": "string",
                            "enum": [
                              "15m",
                              "1d",
                              "1h",
                              "1w",
                              "2d",
                              "2h",
                              "30m",
                              "3d",
                              "4h",
                              "5d",
                              "5m"
                            ],
                            "description": "Rough effort for the whole recommendation, as a duration label.",
                            "example": "30m"
                          },
                          "impactPotential": {
                            "type": "number",
                            "description": "How much acting on it could improve the score, out of 100. An\nestimate from the audit, not a guarantee.",
                            "example": 12
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "error",
                              "n/a",
                              "ok",
                              "todo",
                              "warning"
                            ],
                            "description": "What the diagnostic found. Distinct from `planStatus`, which tracks\nthe client's own progress on the action.",
                            "example": "warning"
                          },
                          "planStatus": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "in_progress",
                              "pending"
                            ],
                            "description": "The client's progress on the action — `pending`, `in_progress` or\n`completed`. Set by the user, never by the audit: a re-audit does not\nreset it.",
                            "example": "pending"
                          },
                          "meta": {
                            "type": "object"
                          }
                        }
                      },
                      "description": "Structured recommendations with observations, improvements, and suggestions"
                    },
                    "tasks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the recommendation. It is what a plan-progress\nupdate refers to.",
                            "example": "rec-gmb-hours"
                          },
                          "title": {
                            "type": "string",
                            "description": "What to do, in one line.",
                            "example": "Complete your Google Business Profile hours"
                          },
                          "categoryId": {
                            "type": "string",
                            "enum": [
                              "analytics",
                              "citations",
                              "content",
                              "gbp",
                              "maps",
                              "nap",
                              "platform",
                              "review",
                              "schema",
                              "search",
                              "seo",
                              "social",
                              "technical"
                            ],
                            "description": "Which area of the audit the recommendation belongs to. It groups\nrecommendations; it does not rank them.",
                            "example": "gbp"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "high",
                              "info",
                              "low",
                              "medium"
                            ],
                            "description": "How soon to act on it. `info` is a note with nothing to do.",
                            "example": "high"
                          },
                          "observations": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "improvements": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "suggestions": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The observation, improvement or suggestion itself, phrased for a\nreader. Written for the business, not as a technical log line.",
                                  "example": "Your Google listing shows no opening hours for Sunday."
                                },
                                "source": {
                                  "type": "string",
                                  "description": "Where the finding comes from — the check or data source that\nproduced it. Absent when the finding is a synthesis.",
                                  "example": "gmb-profile"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "info",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much this one item matters within its recommendation.\nIndependent of the recommendation's own priority: a minor recommendation\ncan carry a critical item.",
                                  "example": "high"
                                },
                                "effortEstimation": {
                                  "type": "string",
                                  "enum": [
                                    "0m",
                                    "15m",
                                    "1d",
                                    "1h",
                                    "1m",
                                    "1w",
                                    "2d",
                                    "2h",
                                    "2w",
                                    "30m",
                                    "3d",
                                    "45m",
                                    "4h",
                                    "5d",
                                    "5m"
                                  ],
                                  "description": "Rough effort to act on it, as a duration label rather than a number\nto parse.",
                                  "example": "15m"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "error",
                                    "n/a",
                                    "ok",
                                    "todo",
                                    "warning"
                                  ],
                                  "description": "What the check found. `ok` means nothing to do, `n/a` means the\ncheck does not apply to this business — not that it passed.",
                                  "example": "warning"
                                },
                                "confidence": {
                                  "type": "number",
                                  "description": "How sure the audit is of this finding, from 0 to 100. Below the\nmiddle of the range, treat it as a lead to verify rather than a fact.",
                                  "example": 85
                                },
                                "meta": {
                                  "type": "object"
                                }
                              }
                            }
                          },
                          "effortEstimation": {
                            "type": "string",
                            "enum": [
                              "15m",
                              "1d",
                              "1h",
                              "1w",
                              "2d",
                              "2h",
                              "30m",
                              "3d",
                              "4h",
                              "5d",
                              "5m"
                            ],
                            "description": "Rough effort for the whole recommendation, as a duration label.",
                            "example": "30m"
                          },
                          "impactPotential": {
                            "type": "number",
                            "description": "How much acting on it could improve the score, out of 100. An\nestimate from the audit, not a guarantee.",
                            "example": 12
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "error",
                              "n/a",
                              "ok",
                              "todo",
                              "warning"
                            ],
                            "description": "What the diagnostic found. Distinct from `planStatus`, which tracks\nthe client's own progress on the action.",
                            "example": "warning"
                          },
                          "planStatus": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "in_progress",
                              "pending"
                            ],
                            "description": "The client's progress on the action — `pending`, `in_progress` or\n`completed`. Set by the user, never by the audit: a re-audit does not\nreset it.",
                            "example": "pending"
                          },
                          "meta": {
                            "type": "object"
                          }
                        }
                      },
                      "description": "Deprecated: use recommendations instead"
                    },
                    "entityDiagnostic": {
                      "type": "object",
                      "properties": {
                        "confidenceTier": {
                          "type": "string",
                          "enum": [
                            "inferred",
                            "low-confidence",
                            "measured",
                            "unavailable"
                          ],
                          "description": "How much weight the diagnostic deserves. It qualifies the\nmeasurement, not the business: `unavailable` means it could not be read.",
                          "example": "measured"
                        },
                        "sameAsCount": {
                          "type": "number",
                          "description": "How many distinct external profiles the entity links to.",
                          "example": 3
                        },
                        "isMachineReadable": {
                          "type": "boolean",
                          "description": "Whether the entity is declared in structured data an assistant can\nread, as opposed to being only visible on the page.",
                          "example": true
                        },
                        "evidenceScopeNote": {
                          "type": "string",
                          "description": "What the diagnostic covered and what it did not. Always present —\nread it before reading any claim about how assistants see the business.",
                          "example": "Based on the homepage only; no external assistant was queried."
                        },
                        "aiClaimSuppressed": {
                          "type": "boolean",
                          "description": "Whether claims about AI visibility were withheld. Always `true` on\nthis module: it has no external pipeline to back such a claim, which is\nwhat the AI-discoverability audit is for.",
                          "example": true
                        },
                        "suppressionReason": {
                          "type": "string",
                          "description": "Why AI-visibility claims were withheld, when they were.",
                          "example": "No external pipeline is attached to this module."
                        },
                        "topGaps": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The biggest missing entity signals, most consequential first.",
                            "example": "sameAs"
                          },
                          "example": [
                            "sameAs",
                            "logo"
                          ],
                          "description": "The biggest missing entity signals, most consequential first."
                        }
                      }
                    },
                    "napConsistency": {
                      "type": "object",
                      "properties": {
                        "overallScore": {
                          "type": "number",
                          "description": "How consistently name, address and phone appear across sources, out\nof 100. `null` when it could not be measured — **never a fabricated 100**.",
                          "example": 88
                        },
                        "consistencyLevel": {
                          "type": "string",
                          "enum": [
                            "EXCELLENT",
                            "FAIR",
                            "GOOD",
                            "POOR"
                          ],
                          "description": "The score as a level. `null` whenever `overallScore` is: an\nunmeasured consistency is not `EXCELLENT`.",
                          "example": "GOOD"
                        },
                        "nameScore": {
                          "type": "number",
                          "description": "The name's consistency, out of 100. `null` when not measurable.",
                          "example": 100
                        },
                        "addressScore": {
                          "type": "number",
                          "description": "The address's consistency, out of 100. `null` when not measurable.",
                          "example": 80
                        },
                        "phoneScore": {
                          "type": "number",
                          "description": "The phone number's consistency, out of 100. `null` when not measurable.",
                          "example": 85
                        },
                        "measurability": {
                          "type": "object",
                          "properties": {
                            "measured": {
                              "type": "boolean",
                              "description": "Whether there was an independent source to compare against. When\n`false`, the scores above are suppressed rather than defaulted.",
                              "example": true
                            },
                            "confidenceTier": {
                              "type": "string",
                              "enum": [
                                "measured",
                                "partial",
                                "unavailable"
                              ],
                              "description": "How complete the comparison was. `partial` means some sources were\nmissing, so the score covers fewer sources than expected.",
                              "example": "measured"
                            },
                            "scoreSuppressed": {
                              "type": "boolean",
                              "description": "Whether the score was withheld. It tells a `null` score from a\nmissing field.",
                              "example": false
                            },
                            "suppressionReason": {
                              "type": "string",
                              "description": "Why the score was withheld, when it was.",
                              "example": "No independent source to compare against."
                            },
                            "evidenceScopeNote": {
                              "type": "string",
                              "description": "What the comparison covered and what it did not.",
                              "example": "Compared against two directories; no aggregator was reachable."
                            }
                          }
                        },
                        "issues": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "source": {
                                "type": "string",
                                "description": "The source that disagrees.",
                                "example": "yellow-pages.example"
                              },
                              "field": {
                                "type": "string",
                                "description": "Which of `name`, `address` or `phone` differs.",
                                "example": "phone"
                              },
                              "issue": {
                                "type": "string",
                                "description": "What differs, in one line.",
                                "example": "Phone number lists an old area code."
                              },
                              "severity": {
                                "type": "string",
                                "enum": [
                                  "BLOCKER",
                                  "HIGH",
                                  "LOW",
                                  "MEDIUM"
                                ],
                                "description": "How much the inconsistency costs. `BLOCKER` is one that can send a\ncustomer to the wrong place or number.",
                                "example": "HIGH"
                              },
                              "details": {
                                "type": "string",
                                "description": "The two values side by side, for a reader to check.",
                                "example": "Listing: +33 4 72 00 00 00 — Profile: +33 4 78 00 00 00"
                              }
                            }
                          },
                          "description": "Where name, address or phone disagree between sources. Empty when they agree."
                        },
                        "recommendations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "source": {
                                "type": "string",
                                "description": "The source to correct.",
                                "example": "yellow-pages.example"
                              },
                              "field": {
                                "type": "string",
                                "description": "The field to correct.",
                                "example": "phone"
                              },
                              "issue": {
                                "type": "string",
                                "description": "The inconsistency it answers.",
                                "example": "Phone number lists an old area code."
                              },
                              "suggestion": {
                                "type": "string",
                                "description": "What to change it to.",
                                "example": "Replace it with the number shown on your Google profile."
                              },
                              "priority": {
                                "type": "string",
                                "enum": [
                                  "HIGH",
                                  "LOW",
                                  "MEDIUM"
                                ],
                                "description": "How soon to correct it.",
                                "example": "HIGH"
                              }
                            }
                          },
                          "description": "What to correct, one entry per inconsistency worth fixing."
                        },
                        "detailedComparison": {
                          "type": "object"
                        }
                      }
                    },
                    "scoreHistory": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "description": "When that run completed. Points are runs, not calendar periods, so\ntheir spacing is irregular. The last ten are kept.",
                            "example": "2026-08-27T06:21:40.000Z"
                          },
                          "healthScore": {
                            "type": "number",
                            "description": "The overall score that run produced, out of 100.",
                            "example": 70
                          },
                          "gmbScore": {
                            "type": "number",
                            "description": "Its Google Business Profile score. Absent for a run older than the breakdown.",
                            "example": 78
                          },
                          "citationScore": {
                            "type": "number",
                            "description": "Its citation score.",
                            "example": 62
                          },
                          "reviewScore": {
                            "type": "number",
                            "description": "Its review score.",
                            "example": 66
                          }
                        }
                      },
                      "description": "Score history of the last 10 audit runs"
                    },
                    "localPagesQueryExplanation": {
                      "type": "object",
                      "properties": {
                        "query": {
                          "type": "string",
                          "description": "The search query the local-pages check ran, exactly as sent.",
                          "example": "menuisier sur mesure Lyon"
                        },
                        "businessModel": {
                          "type": "string",
                          "enum": [
                            "local-single-location",
                            "marketplace-directory",
                            "marketplace-directory-multi-city",
                            "multi-city",
                            "national-b2b"
                          ],
                          "description": "The business model the query was built for. It decides which pages\ncount as local evidence.",
                          "example": "local-single-location"
                        },
                        "purpose": {
                          "type": "string",
                          "description": "Why this query was chosen, in one sentence a business owner can\nread.",
                          "example": "Check whether pages that mention your city and trade show up for a local search."
                        },
                        "limitations": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What the check cannot tell you. Read these before drawing a\nconclusion from the result.",
                            "example": "One query does not represent every way a customer searches."
                          },
                          "example": [
                            "One query does not represent every way a customer searches.",
                            "Results reflect the day of the audit."
                          ],
                          "description": "What the check cannot tell you. Read these before drawing a\nconclusion from the result."
                        }
                      },
                      "description": "Business-readable local-pages query explanation and limitations"
                    },
                    "topCompetitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string",
                            "description": "The competitor's domain.",
                            "example": "globex.example"
                          },
                          "appearanceCount": {
                            "type": "number",
                            "description": "How many of the audit's local searches it appeared in.",
                            "example": 4
                          },
                          "strengthScore": {
                            "type": "number",
                            "description": "How strong its local presence looks, between 0 and 1.",
                            "example": 0.7
                          },
                          "weightedSignal": {
                            "type": "number",
                            "description": "The combined signal used to decide promotion, between 0 and 1: `0.6\n× min(appearanceCount / 5, 1) + 0.4 × strengthScore`.",
                            "example": 0.81
                          },
                          "promoted": {
                            "type": "boolean",
                            "description": "Whether it cleared the threshold and is shown as a top competitor. A\n`false` here is in `suppressedCompetitors`, not lost.",
                            "example": true
                          }
                        }
                      },
                      "description": "Top competitors promoted only after threshold policy validation"
                    },
                    "suppressedCompetitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string",
                            "description": "The competitor's domain.",
                            "example": "globex.example"
                          },
                          "appearanceCount": {
                            "type": "number",
                            "description": "How many of the audit's local searches it appeared in.",
                            "example": 4
                          },
                          "strengthScore": {
                            "type": "number",
                            "description": "How strong its local presence looks, between 0 and 1.",
                            "example": 0.7
                          },
                          "weightedSignal": {
                            "type": "number",
                            "description": "The combined signal used to decide promotion, between 0 and 1: `0.6\n× min(appearanceCount / 5, 1) + 0.4 × strengthScore`.",
                            "example": 0.81
                          },
                          "promoted": {
                            "type": "boolean",
                            "description": "Whether it cleared the threshold and is shown as a top competitor. A\n`false` here is in `suppressedCompetitors`, not lost.",
                            "example": true
                          }
                        }
                      },
                      "description": "Competitors detected but suppressed by threshold policy"
                    },
                    "competitorThresholdPolicy": {
                      "type": "object",
                      "properties": {
                        "minAppearanceCount": {
                          "type": "number",
                          "description": "The fewest searches a domain must appear in to be promoted.",
                          "example": 2
                        },
                        "minWeightedSignal": {
                          "type": "number",
                          "description": "The lowest `weightedSignal` at which a domain is promoted.",
                          "example": 0.55
                        },
                        "formula": {
                          "type": "string",
                          "description": "How `weightedSignal` is computed, as text. Published so the\nthreshold is checkable rather than taken on trust.",
                          "example": "0.6 * min(appearanceCount / 5, 1) + 0.4 * strengthScore"
                        }
                      },
                      "description": "Spec 029/T001: Competitor threshold policy used to promote/suppress signals"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Local visibility audit results",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/reputation": {
      "get": {
        "description": "Results of the project's most recent reputation audit: reviews, ratings, their distribution over time, and the sources they were collected from.",
        "operationId": "getReputationAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemaVersion": {
                      "type": "string",
                      "description": "Version of this payload's shape. Increments on breaking changes only.",
                      "example": "2"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generatedAt": {
                          "type": "string",
                          "description": "When the report was assembled. The sources behind it may be older —\n`stats.reviews.lastReviewDate` and `stats.mentions.lastMentionDate` say\nhow much older.",
                          "example": "2026-09-24T06:14:52.000Z"
                        }
                      }
                    },
                    "project": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable identifier of the project.",
                          "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                        },
                        "brandName": {
                          "type": "string",
                          "description": "The brand's name, as the project declared it. Display it; do not\nkey off it.",
                          "example": "Acme Corporation"
                        },
                        "websiteUrl": {
                          "type": "string",
                          "description": "The project's website. An empty string, not `null`, for a project\nwith no site.",
                          "example": "https://www.acme.example"
                        }
                      },
                      "description": "The project the report is about, as it is named in the product."
                    },
                    "status": {
                      "type": "string",
                      "description": "Where the audit run stands. `completed` says the run finished, not\nthat every source answered — an unreachable source shows up as\n`status: \"missing\"` on its `reviewSources` entry.",
                      "example": "completed"
                    },
                    "createdAt": {
                      "type": "string",
                      "description": "When the run was queued.",
                      "example": "2026-09-24T06:02:11.000Z"
                    },
                    "completedAt": {
                      "type": "string",
                      "description": "When it finished. `null` while it has not.",
                      "example": "2026-09-24T06:14:52.000Z"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "reviews": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "Number of reviews collected, across every source.",
                              "example": 214
                            },
                            "sources": {
                              "type": "number",
                              "description": "How many distinct sources contributed. Not the number of platforms checked.",
                              "example": 3
                            },
                            "averageRating": {
                              "type": "number",
                              "description": "Mean rating on the sources' own scale, or `null` when no review\ncarries one. `null` is not a zero rating.",
                              "example": 4.3
                            },
                            "ratingDistribution": {
                              "type": "object",
                              "description": "How many reviews sit at each rating, keyed by rating.\n\n`null` means **unknown**, not empty: reviews exist but no source\ndisclosed a breakdown. Render it as unknown — never as zeros beside a\nnon-zero `total`, which would read as \"every review is unrated\".",
                              "example": {
                                "1": 13,
                                "2": 9,
                                "3": 18,
                                "4": 46,
                                "5": 128
                              }
                            },
                            "sentiment": {
                              "type": "object",
                              "properties": {
                                "positive": {
                                  "type": "number",
                                  "description": "Reviews that read as positive. With `neutral` and `negative`,\nsums to `total`.",
                                  "example": 158
                                },
                                "neutral": {
                                  "type": "number",
                                  "description": "Reviews that read as neutral.",
                                  "example": 39
                                },
                                "negative": {
                                  "type": "number",
                                  "description": "Reviews that read as negative.",
                                  "example": 17
                                }
                              },
                              "description": "Review counts by sentiment. Counts, not percentages — they sum to `total`."
                            },
                            "lastReviewDate": {
                              "type": "string",
                              "description": "When the most recent review was published. `null` if no review carries a date.",
                              "example": "2026-09-21T16:48:00.000Z"
                            }
                          }
                        },
                        "mentions": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "number",
                              "description": "Mentions found across the whole observation window.",
                              "example": 46
                            },
                            "last7d": {
                              "type": "number",
                              "description": "Of those, how many fall in the last seven days — a subset of `total`, never a separate count.",
                              "example": 5
                            },
                            "sources": {
                              "type": "number",
                              "description": "How many distinct sources mentioned the brand.",
                              "example": 9
                            },
                            "lastMentionDate": {
                              "type": "string",
                              "description": "When the most recent mention was published.",
                              "example": "2026-09-18T07:30:00.000Z"
                            }
                          }
                        },
                        "socialProof": {
                          "type": "object",
                          "properties": {
                            "socialNetworksTracked": {
                              "type": "number",
                              "description": "How many networks are being tracked for this project.",
                              "example": 6
                            },
                            "activeNetworks": {
                              "type": "number",
                              "description": "Tracked networks showing recent activity.",
                              "example": 4
                            },
                            "inactiveNetworks": {
                              "type": "number",
                              "description": "Tracked networks showing none. `activeNetworks + inactiveNetworks`\nequals `socialNetworksTracked`: a network is one or the other, never\nmissing from both.",
                              "example": 2
                            }
                          }
                        }
                      }
                    },
                    "reviewSources": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of this source within the audit.",
                            "example": "9c2f7a41-3e58-4d0b-8b6a-1f5d2c9e4a37"
                          },
                          "platform": {
                            "type": "string",
                            "description": "Machine-readable platform key. Branch on this, not on `name`.",
                            "example": "google"
                          },
                          "name": {
                            "type": "string",
                            "description": "The platform's display name. Show it; never key off it.",
                            "example": "Google Business Profile"
                          },
                          "url": {
                            "type": "string",
                            "description": "Where the reviews were read. Absent for a source with no public page.",
                            "example": "https://maps.example/place/acme-corporation"
                          },
                          "averageRating": {
                            "type": "number",
                            "description": "Mean rating on this source's own scale — compare across sources only\nafter checking they share one.",
                            "example": 4.4
                          },
                          "reviewCount": {
                            "type": "number",
                            "description": "How many reviews this source contributed.",
                            "example": 156
                          },
                          "sentiment": {
                            "type": "object",
                            "properties": {
                              "positive": {
                                "type": "number",
                                "description": "Reviews on this source that read as positive. With `neutral` and\n`negative`, sums to `reviewCount`.",
                                "example": 118
                              },
                              "neutral": {
                                "type": "number",
                                "description": "Reviews on this source that read as neutral.",
                                "example": 26
                              },
                              "negative": {
                                "type": "number",
                                "description": "Reviews on this source that read as negative.",
                                "example": 12
                              },
                              "improvement": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Themes the reviews ask to improve, as the audit phrased them.",
                                  "example": "Waiting time at pickup"
                                },
                                "example": [
                                  "Waiting time at pickup",
                                  "Unclear delivery window"
                                ],
                                "description": "Themes the reviews ask to improve, as the audit phrased them."
                              },
                              "negativeComment": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Verbatim-style summaries of what negative reviews say. Summaries,\nnot quotes: no review is reproduced word for word.",
                                  "example": "Order arrived four days late with no notice"
                                },
                                "example": [
                                  "Order arrived four days late with no notice"
                                ],
                                "description": "Verbatim-style summaries of what negative reviews say. Summaries,\nnot quotes: no review is reproduced word for word."
                              },
                              "positiveComment": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "The same, for what positive reviews praise.",
                                  "example": "Staff went out of their way to adjust the fitting"
                                },
                                "example": [
                                  "Staff went out of their way to adjust the fitting"
                                ],
                                "description": "The same, for what positive reviews praise."
                              }
                            },
                            "description": "Sentiment on this source alone. The three counts sum to\n`reviewCount`; they are counts, not percentages."
                          },
                          "lastReviewDate": {
                            "type": "string",
                            "description": "When this source's most recent review was published. `null` when no\nreview on it carries a date.",
                            "example": "2026-09-21T16:48:00.000Z"
                          },
                          "sampleReviews": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "description": "The review text, as the reviewer wrote it. A sample kept for\nillustration, and it can be in any language.",
                                  "example": "Fitting was adjusted twice at no charge — rare these days."
                                },
                                "sentiment": {
                                  "type": "string",
                                  "description": "How the audit graded this review: `positive`, `neutral` or\n`negative`.",
                                  "example": "positive"
                                }
                              }
                            },
                            "description": "A handful of reviews, kept to illustrate the sentiment split rather\nthan to be counted. Never the full set."
                          },
                          "weight": {
                            "type": "number",
                            "description": "How much this source weighs in the reviews pillar, between 0 and 1.\nA source with few reviews weighs less even when its rating is high.",
                            "example": 0.45
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How urgently this source needs attention — driven by its weight and\nby what its reviews say, not by its rating alone.",
                            "example": "high"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "missing",
                              "present"
                            ],
                            "description": "Whether the brand actually has a presence on this source. `missing`\nis a finding, not an error: the platform matters for this sector and\nnothing was found there.",
                            "example": "present"
                          }
                        }
                      }
                    },
                    "mentions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of this mention within the audit.",
                            "example": "b71e4d90-6c22-4a18-9f03-5e8a7c1d2b64"
                          },
                          "source": {
                            "type": "string",
                            "description": "Where the mention was published, as a host or publication name.",
                            "example": "presse-eco.example"
                          },
                          "url": {
                            "type": "string",
                            "description": "The mention's own URL. Absent when the source exposes none.",
                            "example": "https://presse-eco.example/2026/09/acme-ouvre-un-atelier"
                          },
                          "date": {
                            "type": "string",
                            "description": "When it was published.",
                            "example": "2026-09-18T07:30:00.000Z"
                          },
                          "sentiment": {
                            "type": "string",
                            "enum": [
                              "negative",
                              "neutral",
                              "positive"
                            ],
                            "description": "How the mention speaks of the brand.",
                            "example": "neutral"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How much it deserves a reply, given the source's reach and its\nsentiment.",
                            "example": "medium"
                          },
                          "title": {
                            "type": "string",
                            "description": "Title of the piece, when it has one.",
                            "example": "Acme Corporation opens a second workshop"
                          },
                          "excerpt": {
                            "type": "string",
                            "description": "The passage that names the brand, trimmed. Absent when the source\ncould not be read.",
                            "example": "…the workshop, run by Acme Corporation, now employs twelve…"
                          }
                        }
                      }
                    },
                    "sentimentEvolution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "createdAt": {
                            "type": "string",
                            "description": "When that past audit ran.",
                            "example": "2026-08-27T06:09:40.000Z"
                          },
                          "auditId": {
                            "type": "string",
                            "description": "The past audit this point comes from.",
                            "example": "5d3b8e17-2a94-4c61-b0f7-8e1a6d9c3f25"
                          },
                          "positive": {
                            "type": "number",
                            "description": "Positive reviews at that audit — a count, not a percentage.",
                            "example": 141
                          },
                          "neutral": {
                            "type": "number",
                            "description": "Neutral reviews at that audit.",
                            "example": 44
                          },
                          "negative": {
                            "type": "number",
                            "description": "Negative reviews at that audit.",
                            "example": 22
                          }
                        }
                      },
                      "description": "Sentiment counts per past audit, oldest first — the series behind a\ntrend line. Each entry is a distinct audit run, not a calendar period, so\nthe spacing between points is irregular."
                    },
                    "tasks": {
                      "type": "object",
                      "properties": {
                        "todo": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                              },
                              "title": {
                                "type": "string",
                                "description": "What to do, in one line.",
                                "example": "Reply to the twelve unanswered reviews on Google"
                              },
                              "description": {
                                "type": "string",
                                "description": "Why it matters and what doing it involves.",
                                "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                              },
                              "status": {
                                "type": "string",
                                "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                "example": "todo"
                              },
                              "priority": {
                                "type": "string",
                                "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                "example": "high"
                              },
                              "impactPotential": {
                                "type": "number",
                                "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                "example": 6
                              },
                              "scoreWeight": {
                                "type": "number",
                                "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                "example": 0.12
                              },
                              "category": {
                                "type": "string",
                                "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                "example": "reviews"
                              },
                              "estimatedTime": {
                                "type": "string",
                                "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                "example": "2h"
                              },
                              "suggestions": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Concrete ways to do it.",
                                  "example": "Answer the four lowest-rated reviews first"
                                },
                                "example": [
                                  "Answer the four lowest-rated reviews first",
                                  "Publish a standing reply template for delivery complaints"
                                ],
                                "description": "Concrete ways to do it."
                              },
                              "improvements": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "What improves once it is done, in the audit's own words.",
                                  "example": "Reply rate above 80% on the source that weighs most"
                                },
                                "example": [
                                  "Reply rate above 80% on the source that weighs most"
                                ],
                                "description": "What improves once it is done, in the audit's own words."
                              }
                            }
                          }
                        },
                        "done": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                              },
                              "title": {
                                "type": "string",
                                "description": "What to do, in one line.",
                                "example": "Reply to the twelve unanswered reviews on Google"
                              },
                              "description": {
                                "type": "string",
                                "description": "Why it matters and what doing it involves.",
                                "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                              },
                              "status": {
                                "type": "string",
                                "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                "example": "todo"
                              },
                              "priority": {
                                "type": "string",
                                "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                "example": "high"
                              },
                              "impactPotential": {
                                "type": "number",
                                "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                "example": 6
                              },
                              "scoreWeight": {
                                "type": "number",
                                "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                "example": 0.12
                              },
                              "category": {
                                "type": "string",
                                "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                "example": "reviews"
                              },
                              "estimatedTime": {
                                "type": "string",
                                "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                "example": "2h"
                              },
                              "suggestions": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Concrete ways to do it.",
                                  "example": "Answer the four lowest-rated reviews first"
                                },
                                "example": [
                                  "Answer the four lowest-rated reviews first",
                                  "Publish a standing reply template for delivery complaints"
                                ],
                                "description": "Concrete ways to do it."
                              },
                              "improvements": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "What improves once it is done, in the audit's own words.",
                                  "example": "Reply rate above 80% on the source that weighs most"
                                },
                                "example": [
                                  "Reply rate above 80% on the source that weighs most"
                                ],
                                "description": "What improves once it is done, in the audit's own words."
                              }
                            }
                          }
                        },
                        "priority": {
                          "type": "object",
                          "properties": {
                            "critical": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                    "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Reply to the twelve unanswered reviews on Google"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                    "example": "high"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                    "example": 6
                                  },
                                  "scoreWeight": {
                                    "type": "number",
                                    "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                    "example": 0.12
                                  },
                                  "category": {
                                    "type": "string",
                                    "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                    "example": "reviews"
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "2h"
                                  }
                                }
                              }
                            },
                            "high": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                    "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Reply to the twelve unanswered reviews on Google"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                    "example": "high"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                    "example": 6
                                  },
                                  "scoreWeight": {
                                    "type": "number",
                                    "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                    "example": 0.12
                                  },
                                  "category": {
                                    "type": "string",
                                    "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                    "example": "reviews"
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "2h"
                                  }
                                }
                              }
                            },
                            "medium": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                    "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Reply to the twelve unanswered reviews on Google"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                    "example": "high"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                    "example": 6
                                  },
                                  "scoreWeight": {
                                    "type": "number",
                                    "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                    "example": 0.12
                                  },
                                  "category": {
                                    "type": "string",
                                    "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                    "example": "reviews"
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "2h"
                                  }
                                }
                              }
                            },
                            "low": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier of the task. The same task appears in its status\nlist and in its priority list under this same id — two indexes over one\nplan, not two tasks.",
                                    "example": "3f6c8b21-9d47-4e50-a1b8-7c2e5f0a9d13"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Reply to the twelve unanswered reviews on Google"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Twelve reviews from the last quarter have no reply, four of them below three stars. Answering them publicly is what a reader checks before calling."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task in `tasks.done` carries the matching\nstatus; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which `tasks.priority`\nlist it appears in.",
                                    "example": "high"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of the reputation score doing this could recover, out\nof 100. An estimate from the audit, not a guarantee.",
                                    "example": 6
                                  },
                                  "scoreWeight": {
                                    "type": "number",
                                    "description": "How much this task weighs in the score, between 0 and 1. Two tasks of\nequal `impactPotential` can weigh differently.",
                                    "example": 0.12
                                  },
                                  "category": {
                                    "type": "string",
                                    "description": "Which pillar the task belongs to — the same vocabulary as\n`score.breakdown`.",
                                    "example": "reviews"
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "2h"
                                  }
                                }
                              }
                            }
                          }
                        }
                      },
                      "description": "The action plan, indexed twice: by status in `todo` and `done`, and by\npriority in `priority`. A task appears in one list of each pair — the same\ntask, not a copy, matched by `id`. Counting both would double every total."
                    },
                    "score": {
                      "type": "object",
                      "properties": {
                        "global": {
                          "type": "number",
                          "description": "Overall reputation score out of 100.",
                          "example": 72
                        },
                        "breakdown": {
                          "type": "object",
                          "properties": {
                            "reviews": {
                              "type": "number",
                              "description": "The reviews pillar, out of 100. Weighted into `global`.",
                              "example": 78
                            },
                            "mentions": {
                              "type": "number",
                              "description": "The mentions pillar, out of 100. Weighted into `global`.",
                              "example": 61
                            },
                            "sentiment": {
                              "type": "number",
                              "description": "The sentiment pillar, out of 100. Weighted into `global`.",
                              "example": 74
                            },
                            "socialPresence": {
                              "type": "number",
                              "description": "The social-presence pillar, out of 100. Weighted into `global`.",
                              "example": 55
                            }
                          },
                          "description": "The four pillars behind `global`, each out of 100. They are\nweighted, so `global` is not their arithmetic mean — do not recompute it."
                        }
                      }
                    },
                    "recommendations": {
                      "type": "object",
                      "properties": {
                        "immediate": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do now. Ordered by the audit, most consequential first.",
                            "example": "Reply to the twelve unanswered reviews on Google"
                          },
                          "example": [
                            "Reply to the twelve unanswered reviews on Google"
                          ],
                          "description": "What to do now. Ordered by the audit, most consequential first."
                        },
                        "shortTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do within weeks.",
                            "example": "Claim the two directory listings that show no owner"
                          },
                          "example": [
                            "Claim the two directory listings that show no owner"
                          ],
                          "description": "What to do within weeks."
                        },
                        "longTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do over months.",
                            "example": "Build a steady flow of reviews on the second-weighted source"
                          },
                          "example": [
                            "Build a steady flow of reviews on the second-weighted source"
                          ],
                          "description": "What to do over months."
                        },
                        "strengths": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What already works and should be preserved. Absent when the audit found none.",
                            "example": "Rating above the sector average on the source that weighs most"
                          },
                          "example": [
                            "Rating above the sector average on the source that weighs most"
                          ],
                          "description": "What already works and should be preserved. Absent when the audit found none."
                        }
                      }
                    },
                    "scoreHistory": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "description": "When that run completed. The points are runs, not calendar periods, so\ntheir spacing is irregular.",
                            "example": "2026-08-27T06:09:40.000Z"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "The overall score that run produced, out of 100.",
                            "example": 68
                          },
                          "reviewsScore": {
                            "type": "number",
                            "description": "Its reviews pillar. Absent for a run older than the breakdown.",
                            "example": 74
                          },
                          "mentionsScore": {
                            "type": "number",
                            "description": "Its mentions pillar.",
                            "example": 57
                          },
                          "sentimentScore": {
                            "type": "number",
                            "description": "Its sentiment pillar.",
                            "example": 71
                          },
                          "socialPresenceScore": {
                            "type": "number",
                            "description": "Its social-presence pillar.",
                            "example": 52
                          }
                        }
                      }
                    },
                    "competitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The competitor's name, as the benchmark knows it.",
                            "example": "Globex Joinery"
                          },
                          "url": {
                            "type": "string",
                            "description": "Its site.",
                            "example": "https://globex.example"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "Its overall reputation score out of 100, computed the same way as\n`score.global` — so the two compare directly. `0` when the competitor has\nbeen audited but nothing scoreable was found.",
                            "example": 64
                          },
                          "reviewRating": {
                            "type": "number",
                            "description": "Mean rating on its most weighted review source only — not an average\nacross its sources. `0` when none carries a rating.",
                            "example": 4.1
                          },
                          "reviewCount": {
                            "type": "number",
                            "description": "How many reviews it has, summed across its sources.",
                            "example": 87
                          },
                          "sentimentScore": {
                            "type": "number",
                            "description": "Share of its mentions that read as positive, as a percentage. `0` when\nit has no mention at all — which is not the same as being disliked.",
                            "example": 68
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Online reputation audit results",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/analytics": {
      "get": {
        "description": "Results of the project's most recent analytics audit — traffic, acquisition channels, engagement, and the coverage of the measurement itself.\n\nCoverage is part of the answer, not a footnote: a channel with no data and a channel that was never instrumented look identical in a chart and mean opposite things.",
        "operationId": "getAnalyticsAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schemaVersion": {
                      "type": "string",
                      "description": "Version of this payload's shape. Increments on breaking changes only.",
                      "example": "2"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generatedAt": {
                          "type": "string",
                          "description": "When the report was assembled.",
                          "example": "2026-09-24T06:18:40.000Z"
                        }
                      }
                    },
                    "project": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable identifier of the project.",
                          "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                        },
                        "brandName": {
                          "type": "string",
                          "description": "The brand's name, as the project declared it. Display it; do\nnot key off it.",
                          "example": "Acme Corporation"
                        },
                        "websiteUrl": {
                          "type": "string",
                          "description": "The project's website. An empty string, not `null`, for a\nproject with no site.",
                          "example": "https://www.acme.example"
                        }
                      },
                      "description": "The project the report is about, as it is named in the product."
                    },
                    "status": {
                      "type": "string",
                      "description": "Where the audit run stands. `completed` says the run finished, not\nthat a score exists — read `scoreAvailabilityState` for that.",
                      "example": "completed"
                    },
                    "createdAt": {
                      "type": "string",
                      "description": "When the run was queued.",
                      "example": "2026-09-24T06:11:03.000Z"
                    },
                    "completedAt": {
                      "type": "string",
                      "description": "When it finished. `null` while it has not.",
                      "example": "2026-09-24T06:18:40.000Z"
                    },
                    "readinessState": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "degraded",
                        "failed",
                        "in-progress",
                        "not-started",
                        "queued"
                      ],
                      "description": "Where the run stands. `completed` says the run finished, not that a score exists.",
                      "example": "completed"
                    },
                    "freshnessState": {
                      "type": "string",
                      "enum": [
                        "current",
                        "degraded-current",
                        "stale-last-known",
                        "unavailable"
                      ],
                      "description": "How recent the underlying data is — independent of whether the run succeeded.",
                      "example": "current"
                    },
                    "scoreAvailabilityState": {
                      "type": "string",
                      "enum": [
                        "available",
                        "degraded-computation",
                        "stale-last-known",
                        "suppressed-incomplete-coverage",
                        "unavailable"
                      ],
                      "description": "Whether `score.global` is usable, and why not when it is `null`.\n`suppressed-incomplete-coverage` means the audit ran but measured too\nlittle to score honestly; `stale-last-known` means the number is real but\nfrom an earlier run.",
                      "example": "available"
                    },
                    "scoreSuppressionReason": {
                      "type": "string",
                      "description": "Free-text reason accompanying a suppressed score. Present only when `score.global` is `null`.",
                      "example": "No analytics tool was reachable, so nothing could be scored."
                    },
                    "toolStates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "toolName": {
                            "type": "string",
                            "description": "The tool this state is about, by name.",
                            "example": "Google Tag Manager"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "installed",
                              "misconfigured",
                              "missing",
                              "unavailable"
                            ],
                            "description": "What was found. `misconfigured` means the tool is present but cannot\nmeasure — a different finding from `missing`, and usually a cheaper fix.\n`unavailable` means the audit could not tell: read `reasonCode`.",
                            "example": "installed"
                          },
                          "reasonCode": {
                            "type": "string",
                            "description": "Machine-readable reason behind a `misconfigured` or `unavailable`\nstate. Absent when the state speaks for itself.",
                            "example": "container_loaded_without_tags"
                          }
                        }
                      },
                      "description": "Per-tool state, for the tools whose presence alone does not say\nwhether they work. It is where `misconfigured` shows up — `tools[]` only\nsays found or missing."
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "toolsDetected": {
                          "type": "number",
                          "description": "Measurement tools found on the site.",
                          "example": 6
                        },
                        "toolsMissing": {
                          "type": "number",
                          "description": "Expected tools that were not found. `toolsDetected + toolsMissing` is the number checked.",
                          "example": 4
                        },
                        "pillarCount": {
                          "type": "number",
                          "description": "How many pillars the audit scored.",
                          "example": 6
                        },
                        "averagePillarScore": {
                          "type": "number",
                          "description": "Unweighted mean of the pillar scores. This is NOT `score.global`,\nwhich is weighted — the two differ, and legitimately so.",
                          "example": 63
                        },
                        "consentDetected": {
                          "type": "boolean",
                          "description": "Whether a consent mechanism was detected. Detection, not a compliance verdict.",
                          "example": true
                        },
                        "tagManagerDetected": {
                          "type": "boolean",
                          "description": "Whether a tag manager was detected.",
                          "example": true
                        },
                        "analyticsDetected": {
                          "type": "boolean",
                          "description": "Whether any analytics tool was detected at all.",
                          "example": true
                        }
                      }
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The tool's name, as the industry knows it.",
                            "example": "Google Analytics 4"
                          },
                          "categoryId": {
                            "type": "string",
                            "description": "Which category of measurement it covers, within its pillar.",
                            "example": "web_analytics"
                          },
                          "pillarId": {
                            "type": "string",
                            "enum": [
                              "analytics_measurement",
                              "conversion_pixels",
                              "infra_performance",
                              "marketing_automation",
                              "tag_management",
                              "ux_optimisation"
                            ],
                            "description": "The pillar it belongs to — the same key as `pillars[].id` and as the\nkeys of `score.breakdown`.",
                            "example": "analytics_measurement"
                          },
                          "match": {
                            "type": "string",
                            "description": "What was found on the page that identified it. Absent for a tool that\nis expected rather than detected.",
                            "example": "gtag/js?id=G-XXXXXXX"
                          },
                          "description": {
                            "type": "string",
                            "description": "What the tool does, for a reader who does not know it.",
                            "example": "Measures sessions, traffic sources and conversions."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "detected",
                              "missing",
                              "recommended"
                            ],
                            "description": "Whether it was found (`detected`), expected and absent (`missing`),\nor merely advisable (`recommended`). `recommended` is not a gap: nothing\nis wrong if it stays absent.",
                            "example": "detected"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How much its absence costs. Meaningful on a `missing` tool; on a\ndetected one it says how much the setup depends on it.",
                            "example": "critical"
                          }
                        }
                      },
                      "description": "Every tool the audit looked for, flat — found, missing and merely\nrecommended alike. `status` separates them. The same entries reappear\nunder their pillar, so counting both lists doubles everything."
                    },
                    "pillars": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "analytics_measurement",
                              "conversion_pixels",
                              "infra_performance",
                              "marketing_automation",
                              "tag_management",
                              "ux_optimisation"
                            ],
                            "description": "Stable key of the pillar. Branch on this, not on `label`.",
                            "example": "analytics_measurement"
                          },
                          "label": {
                            "type": "string",
                            "description": "Localised label. Display it; never key off it.",
                            "example": "Analytics and measurement"
                          },
                          "description": {
                            "type": "string",
                            "description": "What the pillar covers, in one line.",
                            "example": "Audience measurement, traffic sources and conversion tracking."
                          },
                          "explanation": {
                            "type": "object",
                            "properties": {
                              "whatItMeans": {
                                "type": "string",
                                "description": "What the pillar measures, in one sentence, ready to show to a reader.",
                                "example": "Whether visits, sources and conversions are measured at all."
                              },
                              "whyItMatters": {
                                "type": "string",
                                "description": "Why it matters, in business terms rather than technical ones.",
                                "example": "Without it, every decision about where to spend is a guess."
                              }
                            }
                          },
                          "score": {
                            "type": "number",
                            "description": "The pillar's score out of 100.",
                            "example": 72
                          },
                          "weight": {
                            "type": "number",
                            "description": "How much it weighs in `score.global`, between 0 and 1. The weights of\nall pillars sum to 1 — which is why `global` is not the mean of the\nscores.",
                            "example": 0.3
                          },
                          "toolsDetected": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "description": "The tool's name, as the industry knows it.",
                                  "example": "Google Analytics 4"
                                },
                                "categoryId": {
                                  "type": "string",
                                  "description": "Which category of measurement it covers, within its pillar.",
                                  "example": "web_analytics"
                                },
                                "pillarId": {
                                  "type": "string",
                                  "enum": [
                                    "analytics_measurement",
                                    "conversion_pixels",
                                    "infra_performance",
                                    "marketing_automation",
                                    "tag_management",
                                    "ux_optimisation"
                                  ],
                                  "description": "The pillar it belongs to — the same key as `pillars[].id` and as the\nkeys of `score.breakdown`.",
                                  "example": "analytics_measurement"
                                },
                                "match": {
                                  "type": "string",
                                  "description": "What was found on the page that identified it. Absent for a tool that\nis expected rather than detected.",
                                  "example": "gtag/js?id=G-XXXXXXX"
                                },
                                "description": {
                                  "type": "string",
                                  "description": "What the tool does, for a reader who does not know it.",
                                  "example": "Measures sessions, traffic sources and conversions."
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "detected",
                                    "missing",
                                    "recommended"
                                  ],
                                  "description": "Whether it was found (`detected`), expected and absent (`missing`),\nor merely advisable (`recommended`). `recommended` is not a gap: nothing\nis wrong if it stays absent.",
                                  "example": "detected"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much its absence costs. Meaningful on a `missing` tool; on a\ndetected one it says how much the setup depends on it.",
                                  "example": "critical"
                                }
                              }
                            },
                            "description": "The pillar's tools that were found. The same entries also appear in\nthe flat `tools` list."
                          },
                          "toolsMissing": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "description": "The tool's name, as the industry knows it.",
                                  "example": "Google Analytics 4"
                                },
                                "categoryId": {
                                  "type": "string",
                                  "description": "Which category of measurement it covers, within its pillar.",
                                  "example": "web_analytics"
                                },
                                "pillarId": {
                                  "type": "string",
                                  "enum": [
                                    "analytics_measurement",
                                    "conversion_pixels",
                                    "infra_performance",
                                    "marketing_automation",
                                    "tag_management",
                                    "ux_optimisation"
                                  ],
                                  "description": "The pillar it belongs to — the same key as `pillars[].id` and as the\nkeys of `score.breakdown`.",
                                  "example": "analytics_measurement"
                                },
                                "match": {
                                  "type": "string",
                                  "description": "What was found on the page that identified it. Absent for a tool that\nis expected rather than detected.",
                                  "example": "gtag/js?id=G-XXXXXXX"
                                },
                                "description": {
                                  "type": "string",
                                  "description": "What the tool does, for a reader who does not know it.",
                                  "example": "Measures sessions, traffic sources and conversions."
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "detected",
                                    "missing",
                                    "recommended"
                                  ],
                                  "description": "Whether it was found (`detected`), expected and absent (`missing`),\nor merely advisable (`recommended`). `recommended` is not a gap: nothing\nis wrong if it stays absent.",
                                  "example": "detected"
                                },
                                "priority": {
                                  "type": "string",
                                  "enum": [
                                    "critical",
                                    "high",
                                    "low",
                                    "medium"
                                  ],
                                  "description": "How much its absence costs. Meaningful on a `missing` tool; on a\ndetected one it says how much the setup depends on it.",
                                  "example": "critical"
                                }
                              }
                            },
                            "description": "Those expected here and absent. This is the pillar's gap, and what\nits `tasks` address."
                          },
                          "categories": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "Stable identifier of the category within its pillar. Branch on\nthis, not on `label`.",
                                  "example": "web_analytics"
                                },
                                "label": {
                                  "type": "string",
                                  "description": "Localised label of the category. Display it; never key off it.",
                                  "example": "Web analytics"
                                },
                                "hasMinimum": {
                                  "type": "boolean",
                                  "description": "Whether this category has at least the minimum expected tooling.\n`false` is what holds the pillar's score down.",
                                  "example": true
                                }
                              }
                            },
                            "description": "The categories the pillar is scored on. A pillar scores well only\nwhen each category has its minimum — one excellent category does not\ncompensate an empty one."
                          }
                        }
                      },
                      "description": "The pillars, each with its score, its weight and its gap."
                    },
                    "tasks": {
                      "type": "object",
                      "properties": {
                        "todo": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                              },
                              "title": {
                                "type": "string",
                                "description": "What to do, in one line.",
                                "example": "Install a consent banner before any analytics tag fires"
                              },
                              "description": {
                                "type": "string",
                                "description": "Why it matters and what doing it involves.",
                                "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                              },
                              "status": {
                                "type": "string",
                                "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                "example": "todo"
                              },
                              "priority": {
                                "type": "string",
                                "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                "example": "critical"
                              },
                              "pillarId": {
                                "type": "string",
                                "enum": [
                                  "analytics_measurement",
                                  "conversion_pixels",
                                  "infra_performance",
                                  "marketing_automation",
                                  "tag_management",
                                  "ux_optimisation"
                                ],
                                "description": "Which pillar it improves, same key as `pillars[].id`.",
                                "example": "tag_management"
                              },
                              "impactPotential": {
                                "type": "number",
                                "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                "example": 9
                              },
                              "estimatedTime": {
                                "type": "string",
                                "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                "example": "half a day"
                              },
                              "suggestions": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Concrete ways to do it.",
                                  "example": "Gate every tag behind the consent state in the tag manager"
                                },
                                "example": [
                                  "Gate every tag behind the consent state in the tag manager",
                                  "Re-check the pillar once the banner is live"
                                ],
                                "description": "Concrete ways to do it."
                              }
                            }
                          }
                        },
                        "done": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                              },
                              "title": {
                                "type": "string",
                                "description": "What to do, in one line.",
                                "example": "Install a consent banner before any analytics tag fires"
                              },
                              "description": {
                                "type": "string",
                                "description": "Why it matters and what doing it involves.",
                                "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                              },
                              "status": {
                                "type": "string",
                                "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                "example": "todo"
                              },
                              "priority": {
                                "type": "string",
                                "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                "example": "critical"
                              },
                              "pillarId": {
                                "type": "string",
                                "enum": [
                                  "analytics_measurement",
                                  "conversion_pixels",
                                  "infra_performance",
                                  "marketing_automation",
                                  "tag_management",
                                  "ux_optimisation"
                                ],
                                "description": "Which pillar it improves, same key as `pillars[].id`.",
                                "example": "tag_management"
                              },
                              "impactPotential": {
                                "type": "number",
                                "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                "example": 9
                              },
                              "estimatedTime": {
                                "type": "string",
                                "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                "example": "half a day"
                              },
                              "suggestions": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "Concrete ways to do it.",
                                  "example": "Gate every tag behind the consent state in the tag manager"
                                },
                                "example": [
                                  "Gate every tag behind the consent state in the tag manager",
                                  "Re-check the pillar once the banner is live"
                                ],
                                "description": "Concrete ways to do it."
                              }
                            }
                          }
                        },
                        "priority": {
                          "type": "object",
                          "properties": {
                            "critical": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                    "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Install a consent banner before any analytics tag fires"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                    "example": "critical"
                                  },
                                  "pillarId": {
                                    "type": "string",
                                    "enum": [
                                      "analytics_measurement",
                                      "conversion_pixels",
                                      "infra_performance",
                                      "marketing_automation",
                                      "tag_management",
                                      "ux_optimisation"
                                    ],
                                    "description": "Which pillar it improves, same key as `pillars[].id`.",
                                    "example": "tag_management"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                    "example": 9
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "half a day"
                                  }
                                }
                              }
                            },
                            "high": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                    "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Install a consent banner before any analytics tag fires"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                    "example": "critical"
                                  },
                                  "pillarId": {
                                    "type": "string",
                                    "enum": [
                                      "analytics_measurement",
                                      "conversion_pixels",
                                      "infra_performance",
                                      "marketing_automation",
                                      "tag_management",
                                      "ux_optimisation"
                                    ],
                                    "description": "Which pillar it improves, same key as `pillars[].id`.",
                                    "example": "tag_management"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                    "example": 9
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "half a day"
                                  }
                                }
                              }
                            },
                            "medium": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                    "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Install a consent banner before any analytics tag fires"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                    "example": "critical"
                                  },
                                  "pillarId": {
                                    "type": "string",
                                    "enum": [
                                      "analytics_measurement",
                                      "conversion_pixels",
                                      "infra_performance",
                                      "marketing_automation",
                                      "tag_management",
                                      "ux_optimisation"
                                    ],
                                    "description": "Which pillar it improves, same key as `pillars[].id`.",
                                    "example": "tag_management"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                    "example": 9
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "half a day"
                                  }
                                }
                              }
                            },
                            "low": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "description": "Stable identifier. The same task carries this id in its status list\nand in its priority list — it is what matches the two.",
                                    "example": "c4a9f1e7-52b8-4d63-9e01-3f7b6c8a2d45"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "What to do, in one line.",
                                    "example": "Install a consent banner before any analytics tag fires"
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "Why it matters and what doing it involves.",
                                    "example": "Analytics loads on the first page view with no consent gate. Beyond the legal exposure, the measurement it produces cannot be relied on for anything consent-dependent."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "Where the task stands. A task listed under `tasks.done` carries the\nmatching status; the two never disagree.",
                                    "example": "todo"
                                  },
                                  "priority": {
                                    "type": "string",
                                    "description": "Its priority cran — the same value that decides which\n`tasks.priority` list it appears in.",
                                    "example": "critical"
                                  },
                                  "pillarId": {
                                    "type": "string",
                                    "enum": [
                                      "analytics_measurement",
                                      "conversion_pixels",
                                      "infra_performance",
                                      "marketing_automation",
                                      "tag_management",
                                      "ux_optimisation"
                                    ],
                                    "description": "Which pillar it improves, same key as `pillars[].id`.",
                                    "example": "tag_management"
                                  },
                                  "impactPotential": {
                                    "type": "number",
                                    "description": "How many points of `score.global` doing it could recover, out of 100.\nAn estimate from the audit, not a guarantee.",
                                    "example": 9
                                  },
                                  "estimatedTime": {
                                    "type": "string",
                                    "description": "Rough effort, as the audit phrases it for a reader. A label, not a\nduration to parse.",
                                    "example": "half a day"
                                  }
                                }
                              }
                            }
                          }
                        }
                      },
                      "description": "The action plan, indexed twice: by status in `todo` and `done`, by\npriority in `priority`. A task appears in one list of each pair — matched\nby `id`, and counted once."
                    },
                    "score": {
                      "type": "object",
                      "properties": {
                        "global": {
                          "type": "number",
                          "description": "Overall measurement-readiness score out of 100, or `null` when it\ncould not be produced — read `scoreAvailabilityState` for why. `null`\nis never a zero.",
                          "example": 58
                        }
                      }
                    },
                    "recommendations": {
                      "type": "object",
                      "properties": {
                        "immediate": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do now, most consequential first.",
                            "example": "Gate every analytics tag behind the consent state"
                          },
                          "example": [
                            "Gate every analytics tag behind the consent state"
                          ],
                          "description": "What to do now, most consequential first."
                        },
                        "shortTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do within weeks.",
                            "example": "Declare the three conversions that matter as events"
                          },
                          "example": [
                            "Declare the three conversions that matter as events"
                          ],
                          "description": "What to do within weeks."
                        },
                        "longTerm": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What to do over months.",
                            "example": "Move the remaining hard-coded tags into the tag manager"
                          },
                          "example": [
                            "Move the remaining hard-coded tags into the tag manager"
                          ],
                          "description": "What to do over months."
                        }
                      }
                    },
                    "competitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The competitor's name, as the benchmark knows it.",
                            "example": "Globex Joinery"
                          },
                          "url": {
                            "type": "string",
                            "description": "Its site.",
                            "example": "https://globex.example"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "Its measurement-readiness score out of 100, computed exactly like\n`score.global` — so the two compare directly.",
                            "example": 51
                          },
                          "toolCount": {
                            "type": "number",
                            "description": "How many measurement tools were detected on its site. A count, not a\nquality judgement: more tools does not mean better measurement.",
                            "example": 4
                          }
                        }
                      },
                      "description": "The competitors measured the same way, for comparison. Empty when\nnone has been audited — which is not the same as having none."
                    },
                    "scoreHistory": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "description": "When that run completed. The points are runs, not calendar periods,\nso their spacing is irregular. At most the last ten are kept.",
                            "example": "2026-08-27T06:14:22.000Z"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "The overall score that run produced, out of 100.",
                            "example": 52
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Analytics audit results",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/ai-discoverability": {
      "get": {
        "description": "How the project surfaces in AI assistants — whether it is cited, on which prompts, and against which competitors.\n\nMeasured from neutral accounts, deliberately: an assistant queried from your own logged-in account personalises its answers, and would report a visibility nobody else sees.",
        "operationId": "getAiDiscoverabilityAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable identifier of the project. It scopes every audit and content\nitem, and is the only project identifier a client should store.",
                          "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                        },
                        "brandName": {
                          "type": "string",
                          "description": "The brand's name, as the project declared it. Display it; do not key\noff it — two projects can share a brand name.",
                          "example": "Acme Corporation"
                        },
                        "websiteUrl": {
                          "type": "string",
                          "description": "The project's website. An **empty string**, not `null`, for a project\nwith no site — a local business that exists only on its Google listing.\nTest for the empty string before building a link from it.",
                          "example": "https://www.acme.example"
                        }
                      }
                    },
                    "projectId": {
                      "type": "string",
                      "description": "The project this audit belongs to. One audit per project: it is\noverwritten on each run, which is why `scoreHistory` exists.",
                      "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "failed",
                        "in-progress",
                        "queued"
                      ],
                      "description": "Where the run stands. `completed` says the run finished, not that a\nscore exists: `globalScore` is absent when no probe returned a usable\nanswer.",
                      "example": "completed"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the audit row was first created — not when the latest run\nstarted.",
                      "example": "2026-06-20T09:14:02.000Z"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the row last changed, including while a run is in progress.",
                      "example": "2026-09-24T06:21:40.000Z"
                    },
                    "completedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the latest run finished. Absent while it has not.",
                      "example": "2026-09-24T06:21:40.000Z"
                    },
                    "globalScore": {
                      "type": "number",
                      "description": "How discoverable the brand is to AI assistants, out of 100. Absent\nwhile the audit is running, and absent — not zero — when no probe returned\na usable answer.",
                      "example": 64
                    },
                    "confidence": {
                      "type": "string",
                      "enum": [
                        "inferred",
                        "low-confidence",
                        "measured",
                        "unavailable"
                      ],
                      "description": "How much weight to put on `globalScore`. It qualifies the MEASUREMENT,\nnot the brand: a low confidence means few models answered usefully, not\nthat the brand scores badly.",
                      "example": "measured"
                    },
                    "scoreBreakdown": {
                      "type": "object",
                      "properties": {
                        "recognition": {
                          "type": "number",
                          "description": "Share of probed models that recognised the brand, between 0 and\n100. Carries 40% of the global score.",
                          "example": 75
                        },
                        "accuracy": {
                          "type": "number",
                          "description": "Mean factual accuracy of the answers that recognised the brand,\nout of 100. Carries 25%.",
                          "example": 68
                        },
                        "citation": {
                          "type": "number",
                          "description": "How often grounded answers cited the brand's own sources, out of\n100. Carries 20% — but see `citationMeasured`: a `0` here can mean not\nmeasured.",
                          "example": 40
                        },
                        "citationMeasured": {
                          "type": "boolean",
                          "description": "Whether the citation dimension could be measured at all. When\n`false`, `citation` is `0` only because no grounded probe returned\ncitations, and it was **dropped** from the weighted global. Render it\nas not measured, never as a real 0/100.",
                          "example": true
                        },
                        "recommendation": {
                          "type": "number",
                          "description": "How often the brand surfaced as a recommendation rather than only\nits competitors, out of 100. Carries 15%.",
                          "example": 50
                        },
                        "recommendationMeasured": {
                          "type": "boolean",
                          "description": "Whether the recommendation dimension could be measured. Without a\nknown sector the probe asks a sector-less question, and models rightly\nrefuse to recommend anything — which is not brand signal. When `false`,\n`recommendation` was dropped from the global.",
                          "example": true
                        }
                      },
                      "description": "The components behind `globalScore`. Weighted — do not recompute the total from them."
                    },
                    "probedModels": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Which assistants were actually questioned for this run. The list can\nchange between runs as models are added or retired, so two scores are only\ncomparable when this list matches.",
                        "example": "openai/gpt-4o-mini"
                      },
                      "example": [
                        "openai/gpt-4o-mini",
                        "anthropic/claude-haiku-4-5",
                        "google/gemini-2.5-flash"
                      ],
                      "description": "Which assistants were actually questioned for this run. The list can\nchange between runs as models are added or retired, so two scores are only\ncomparable when this list matches."
                    },
                    "competitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The competitor as the assistants named it.",
                            "example": "Globex Joinery"
                          },
                          "mentions": {
                            "type": "number",
                            "description": "How many times the panel named it, across models and questions. A\ncount of mentions, not of models.",
                            "example": 3
                          },
                          "url": {
                            "type": "string",
                            "description": "Its canonical website, when it could be established with confidence.\nAbsent otherwise — never guessed.",
                            "example": "https://globex.example"
                          },
                          "summary": {
                            "type": "string",
                            "description": "A one-line factual description: who they are and why assistants cite\nthem.",
                            "example": "Regional joinery group with a strong presence in local directories."
                          }
                        }
                      }
                    },
                    "competitorGap": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The competitor whose signals were measured.",
                            "example": "Globex Joinery"
                          },
                          "url": {
                            "type": "string",
                            "description": "The homepage that was measured.",
                            "example": "https://globex.example"
                          },
                          "signals": {
                            "type": "object",
                            "properties": {
                              "extracted": {
                                "type": "boolean",
                                "description": "Whether the homepage could be fetched and read. When `false`, every\nother field describes nothing — it is a failed measurement, not a poor\nsite.",
                                "example": true
                              },
                              "hasEntitySchema": {
                                "type": "boolean",
                                "description": "Whether an Organization or LocalBusiness structured-data block is\npresent.",
                                "example": true
                              },
                              "schemaType": {
                                "type": "string",
                                "description": "The entity type detected, or `null` when there is none.",
                                "example": "LocalBusiness"
                              },
                              "sameAsCount": {
                                "type": "number",
                                "description": "How many distinct external profiles the entity links to, from\nstructured data and from discovered social and authority links.",
                                "example": 4
                              },
                              "signalsPresent": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "enum": [
                                    "description",
                                    "logo",
                                    "name",
                                    "sameAs",
                                    "schema"
                                  ],
                                  "description": "Which of the five entity signals were found.",
                                  "example": "schema"
                                },
                                "example": [
                                  "schema",
                                  "name",
                                  "description"
                                ],
                                "description": "Which of the five entity signals were found."
                              },
                              "completenessScore": {
                                "type": "number",
                                "description": "A weighted score out of 100 for how complete the entity signals are.\nIt is what assistants can ingest, not a quality judgement.",
                                "example": 60
                              }
                            }
                          },
                          "signalsMissingVsBrand": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "description",
                                "logo",
                                "name",
                                "sameAs",
                                "schema"
                              ],
                              "description": "The signals this competitor has and the brand lacks. This is the\nactionable gap: each entry is something to add.",
                              "example": "sameAs"
                            },
                            "example": [
                              "sameAs",
                              "logo"
                            ],
                            "description": "The signals this competitor has and the brand lacks. This is the\nactionable gap: each entry is something to add."
                          }
                        }
                      },
                      "description": "Where a competitor is cited by assistants and this brand is not. The\ngap is per entity and per probe, so the same competitor can appear several\ntimes for different questions."
                    },
                    "insights": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the insight.",
                            "example": "insight-entity-signals"
                          },
                          "title": {
                            "type": "string",
                            "description": "What the audit concluded, in one line.",
                            "example": "Assistants recognise the brand but rarely recommend it"
                          },
                          "description": {
                            "type": "string",
                            "description": "The finding in prose, with what it rests on.",
                            "example": "Three of four models named the brand when asked, but none listed it among recommendations for its sector."
                          },
                          "severity": {
                            "type": "string",
                            "enum": [
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How much it should weigh. Insights are ordered by consequence, so\n`severity` and position mostly agree.",
                            "example": "medium"
                          },
                          "signals": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The measured signals the finding rests on, as short labels.",
                              "example": "recognition"
                            },
                            "example": [
                              "recognition",
                              "recommendation"
                            ],
                            "description": "The measured signals the finding rests on, as short labels."
                          },
                          "evidence": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Passages that support it, taken from the assistants' own answers.\nAbsent when the finding is structural rather than observed.",
                              "example": "Aucune source ne cite le site de la marque."
                            },
                            "example": [
                              "Aucune source ne cite le site de la marque."
                            ],
                            "description": "Passages that support it, taken from the assistants' own answers.\nAbsent when the finding is structural rather than observed."
                          }
                        }
                      },
                      "description": "What the audit concluded, in prose, ordered by consequence."
                    },
                    "tasks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the task.",
                            "example": "task-add-organization-schema"
                          },
                          "title": {
                            "type": "string",
                            "description": "What to do, in one line.",
                            "example": "Add Organization structured data to the homepage"
                          },
                          "description": {
                            "type": "string",
                            "description": "How to do it, specific to the brand and its sector. Absent on rows\nrecorded before descriptions existed.",
                            "example": "Publish an Organization block with name, logo, description and sameAs links to the two social profiles that exist."
                          },
                          "category": {
                            "type": "string",
                            "description": "Which lever the action acts on — entity signals, structured data,\ncitations. Use it to group tasks, not to filter them out.",
                            "example": "structured-data"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How soon to do it.",
                            "example": "high"
                          },
                          "effort": {
                            "type": "string",
                            "enum": [
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How much work it is. Read it against `impact`: low effort and high\nimpact is where to start.",
                            "example": "low"
                          },
                          "impact": {
                            "type": "string",
                            "enum": [
                              "high",
                              "low",
                              "medium"
                            ],
                            "description": "How much it should move the score.",
                            "example": "high"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "ok",
                              "warning"
                            ],
                            "description": "Where the audit found this lever. `ok` means nothing to do;\n`critical` means an assistant is missing the signal outright.",
                            "example": "warning"
                          },
                          "planStatus": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "in_progress",
                              "pending"
                            ],
                            "description": "Where the client stands on this action in the cross-module action plan,\ndistinct from the diagnostic `status`. A re-audit does not reset it.",
                            "example": "in_progress"
                          }
                        }
                      }
                    },
                    "evaluations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "model": {
                            "type": "string",
                            "description": "The assistant that answered, by provider slug. Compare scores only\nacross runs that probed the same models.",
                            "example": "openai/gpt-4o-mini"
                          },
                          "taskId": {
                            "type": "string",
                            "description": "Which question was put to it. It is what links an evaluation to a\n`tasks` entry.",
                            "example": "probe-brand-knowledge"
                          },
                          "intent": {
                            "type": "string",
                            "enum": [
                              "brand-knowledge",
                              "recommendation"
                            ],
                            "description": "What the question was after: what the assistant knows of the brand,\nor whether it recommends the brand without being told its name.",
                            "example": "brand-knowledge"
                          },
                          "recognized": {
                            "type": "boolean",
                            "description": "Whether the assistant demonstrably knew the brand exists. `false`\nmeans it said it had no information — not that it answered wrongly.",
                            "example": true
                          },
                          "accuracy": {
                            "type": "number",
                            "description": "How well its description matched the known facts about the brand,\nout of 100. Meaningful only when `recognized` is `true`.",
                            "example": 82
                          },
                          "recommended": {
                            "type": "boolean",
                            "description": "Whether the brand surfaced as a recommendation for a query about its\nsector that did not name it. This is the unbiased signal.",
                            "example": false
                          },
                          "citedOwnSource": {
                            "type": "boolean",
                            "description": "Whether a web-grounded answer cited at least one of the brand's own\nsources. Always `false` for a model that is not grounded.",
                            "example": true
                          },
                          "competitorsMentioned": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The competing brands the assistant named, raw as it wrote them — not\ndeduplicated across models.",
                              "example": "Globex Joinery"
                            },
                            "example": [
                              "Globex Joinery",
                              "Initech Menuiserie"
                            ],
                            "description": "The competing brands the assistant named, raw as it wrote them — not\ndeduplicated across models."
                          },
                          "rationale": {
                            "type": "string",
                            "description": "Why the answer was graded as it was, in one sentence. Evidence for a\nreader, not a field to parse.",
                            "example": "Named the brand and its city correctly but described it as a distributor rather than a workshop."
                          },
                          "answer": {
                            "type": "string",
                            "description": "The assistant's answer, verbatim and capped in length. It is shown\nas evidence, so it can be wrong; that is the point.",
                            "example": "Acme Corporation is a joinery workshop based in Lyon, founded in 2011."
                          },
                          "answered": {
                            "type": "boolean",
                            "description": "Whether the probe returned an answer that could be graded. `false`\nmeans it errored or came back empty, and the evaluation is left out of\nthe confidence calculation.",
                            "example": true
                          }
                        }
                      },
                      "description": "One entry per question put to one model — the raw material behind the\nscore. Read these when a score surprises you: they say what was asked and\nwhat came back."
                    },
                    "scoreHistory": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "description": "When that run completed. Points are runs, not calendar periods, so\ntheir spacing is irregular. The last ten are kept.",
                            "example": "2026-08-27T06:21:40.000Z"
                          },
                          "globalScore": {
                            "type": "number",
                            "description": "The score that run produced, out of 100. Compare it only with points\nthat share the same `confidence` and probed models.",
                            "example": 61
                          },
                          "confidence": {
                            "type": "string",
                            "enum": [
                              "inferred",
                              "low-confidence",
                              "measured",
                              "unavailable"
                            ],
                            "description": "How much weight that point deserves. A `low-confidence` point in a\ntrend line is a single data point, not a trend.",
                            "example": "measured"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "AI discoverability audit results",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/content": {
      "get": {
        "description": "Aggregated results of the content audit: coverage, quality signals and technical findings across the **pages discovered on the project's site**.\n\n**These are crawled pages, not the editorial pieces of `GET /contents`.** The two are different populations and they do not overlap: this endpoint counts what the crawler found and audited, the other lists what the content engine produced. A project can show sixty-eight here and zero there, and neither number is wrong.\n\n`avgScore` is `null` — never `0` — when nothing in a group has been scored. Its denominator is `scoredCount`, not `count`: a group can carry many pages and no score at all.",
        "operationId": "getContentAudit",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalItems": {
                      "type": "number",
                      "description": "Total number of content items",
                      "example": 42
                    },
                    "scoredItems": {
                      "type": "number",
                      "description": "Number of items that carry a numeric score (the denominator behind avgScore) — may be lower than totalItems",
                      "example": 30
                    },
                    "avgScore": {
                      "type": "number",
                      "description": "Average score across scored content items (0-100), or null when none of totalItems has been scored yet — never a fabricated 0. Its base is scoredItems, not totalItems.",
                      "example": 58
                    },
                    "criticalCount": {
                      "type": "number",
                      "description": "Number of critical urgency items",
                      "example": 5
                    },
                    "highCount": {
                      "type": "number",
                      "description": "Number of high urgency items",
                      "example": 8
                    },
                    "moderateCount": {
                      "type": "number",
                      "description": "Number of moderate urgency items",
                      "example": 15
                    },
                    "lowCount": {
                      "type": "number",
                      "description": "Number of low urgency items",
                      "example": 14
                    },
                    "totalQuickWins": {
                      "type": "number",
                      "description": "Total number of available quick wins",
                      "example": 23
                    },
                    "funnelDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "stage": {
                            "type": "string",
                            "description": "Funnel stage",
                            "example": "TOFU"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items in this stage",
                            "example": 12
                          }
                        }
                      },
                      "description": "Distribution of content items by funnel stage"
                    },
                    "intentDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "intent": {
                            "type": "string",
                            "description": "Intent type",
                            "example": "informationnelle"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items with this intent",
                            "example": 8
                          }
                        }
                      },
                      "description": "Distribution of content items by intent type"
                    },
                    "contentTypeDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "contentType": {
                            "type": "string",
                            "description": "Content type",
                            "example": "article"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items of this type",
                            "example": 15
                          },
                          "scoredCount": {
                            "type": "number",
                            "description": "Number of items of this type that carry a numeric score (the denominator behind avgScore)",
                            "example": 12
                          },
                          "avgScore": {
                            "type": "number",
                            "description": "Average score for this content type (0-100), or null when none of these items has been scored yet — never a fabricated 0",
                            "example": 65
                          }
                        }
                      },
                      "description": "Distribution of content items by content type with avg scores"
                    },
                    "scoreDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bucket": {
                            "type": "string",
                            "description": "Score range bucket",
                            "example": "60-80"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items in this score range",
                            "example": 10
                          }
                        }
                      },
                      "description": "Distribution of content items by score buckets"
                    },
                    "distinctClusters": {
                      "type": "number",
                      "description": "Number of distinct content clusters",
                      "example": 7
                    },
                    "distinctIntents": {
                      "type": "number",
                      "description": "Number of distinct intent types found",
                      "example": 4
                    },
                    "staleContentCount": {
                      "type": "number",
                      "description": "Number of stale content items (not updated in >365 days)",
                      "example": 3
                    },
                    "orphanCount": {
                      "type": "number",
                      "description": "Number of orphan pages (no cluster assigned)",
                      "example": 5
                    },
                    "avgOpportunityScore": {
                      "type": "number",
                      "description": "Average opportunity score across all items (0-100)",
                      "example": 42
                    },
                    "clusterDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "cluster": {
                            "type": "string",
                            "description": "Cluster name",
                            "example": "Immobilier"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items in this cluster",
                            "example": 8
                          },
                          "scoredCount": {
                            "type": "number",
                            "description": "Number of items in this cluster that carry a numeric score (the denominator behind avgScore)",
                            "example": 6
                          },
                          "avgScore": {
                            "type": "number",
                            "description": "Average score for items in this cluster (0-100), or null when none of them has been scored yet — never a fabricated 0",
                            "example": 62
                          }
                        }
                      },
                      "description": "Distribution of content items by cluster with avg scores"
                    },
                    "editorialAxeDistribution": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "axe": {
                            "type": "string",
                            "description": "Editorial axe",
                            "example": "article_expertise"
                          },
                          "count": {
                            "type": "number",
                            "description": "Number of content items with this axe",
                            "example": 6
                          },
                          "scoredCount": {
                            "type": "number",
                            "description": "Number of items with this axe that carry a numeric score (the denominator behind avgScore)",
                            "example": 4
                          },
                          "avgScore": {
                            "type": "number",
                            "description": "Average score for items with this axe (0-100), or null when none of them has been scored yet — never a fabricated 0",
                            "example": 55
                          }
                        }
                      },
                      "description": "Distribution of content items by editorial axe with avg scores"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Content audit aggregates",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/audits/competitors": {
      "get": {
        "description": "Competitors attached to the project. The `view` parameter selects **which population**, and they are not interchangeable:\n\n- `benchmark` (default) — the shortlist arbitrated by SERP recurrence. **The only comparable list**, and the one the overview radar is built on.\n- `market-map` — those accepted by the latest discovery run, typed.\n- `inspiration` — the editorial pool.\n- `pool` — the historical population across all runs: accepted, editorial sources and transient failures. A sorting surface, not an answer to « who are my competitors ».\n\nEach entry carries `assetFreshness`, the state of the **crawl** assets used for editorial inspiration (expiry around a day). This has no relation to the **audit** freshness (30-day window) behind `competitorPosture` in the overview: an entry marked `stale` here can perfectly well belong to a `High Confidence` cohort there.",
        "operationId": "getCompetitors",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch` — it is never used to widen access.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable identifier of this competitor entry within the project.",
                        "example": "5b8d1c4e-0a72-4f33-9e61-7d2c8a95b104"
                      },
                      "displayName": {
                        "type": "string",
                        "description": "Name shown to a reader. Not a key: two projects can name the same domain differently.",
                        "example": "Globex Joinery"
                      },
                      "canonicalDomain": {
                        "type": "string",
                        "description": "The domain in its canonical form. `null` for an entry added from a link that never resolved.",
                        "example": "globex.example"
                      },
                      "sourceUrl": {
                        "type": "string",
                        "description": "The URL this competitor was discovered from. `null` for a manual addition.",
                        "example": "https://search.example/?q=made-to-measure+joinery"
                      },
                      "originType": {
                        "type": "string",
                        "enum": [
                          "audit_detected",
                          "inline_from_link",
                          "manual"
                        ],
                        "description": "How the entry got into the pool — found by an audit, added by hand, or\npulled in from a link. It says how much to trust the rest of the entry.",
                        "example": "audit_detected"
                      },
                      "inspirationEligibility": {
                        "type": "string",
                        "enum": [
                          "blocked",
                          "ready"
                        ],
                        "description": "Whether this competitor can feed editorial inspiration. `blocked`\nmeans its pages cannot be crawled, not that it is irrelevant.",
                        "example": "ready"
                      },
                      "qualificationState": {
                        "type": "string",
                        "enum": [
                          "accepted",
                          "rejected",
                          "withheld"
                        ],
                        "description": "The judge's verdict on keeping this entry. `withheld` means the\ndecision was deferred, not refused. `null` before any judgement.",
                        "example": "accepted"
                      },
                      "benchmarkAuthorityPosture": {
                        "type": "string",
                        "enum": [
                          "authoritative",
                          "non_authoritative"
                        ],
                        "description": "Whether the domain carries enough authority for the benchmark to\ncompare against it. A non-authoritative competitor stays in the pool for\nthe market map and for inspiration.",
                        "example": "authoritative"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "completed",
                          "failed",
                          "not_scheduled",
                          "pending",
                          "running"
                        ],
                        "description": "Where this competitor's own audit stands. `not_scheduled` means no run\nis planned — not that one failed.",
                        "example": "completed"
                      },
                      "reasonCode": {
                        "type": "string",
                        "description": "Machine-readable reason behind the qualification verdict. Pair it with `verdictReason` for the prose.",
                        "example": "same_service_same_area"
                      },
                      "verdictReason": {
                        "type": "string",
                        "description": "The judge's sentence explaining `reasonCode`. `null` for an entry\nthat was never judged, such as a manual addition.",
                        "example": "Sells made-to-measure joinery to the same households within the same metropolitan area."
                      },
                      "arbitration": {
                        "type": "string",
                        "enum": [
                          "confirmed",
                          "overruled"
                        ],
                        "description": "Outcome of the second-model arbitration on this verdict. `null` when\nthe first verdict was not contested, or when there is no verdict.",
                        "example": "confirmed"
                      },
                      "competitorType": {
                        "type": "string",
                        "enum": [
                          "aspirational",
                          "direct",
                          "local",
                          "none",
                          "substitute"
                        ],
                        "description": "The nature of the competition — `direct`, a `substitute` offer, a\n`local` player, or an `aspirational` reference you are not yet compared\nwith. `null` before classification.",
                        "example": "direct"
                      },
                      "benchmarkRank": {
                        "type": "number",
                        "description": "Rank within the benchmark. `null` when the competitor is outside it.",
                        "example": 2
                      },
                      "distinctTerms": {
                        "type": "number",
                        "description": "Number of distinct search intents where this domain appeared.",
                        "example": 17
                      },
                      "proximityScore": {
                        "type": "number",
                        "description": "Competitive proximity as judged by the classifier, 0-100.",
                        "example": 78
                      },
                      "globalScore": {
                        "type": "number",
                        "description": "Overall score of the competitor's site, out of 100. `null` until it has been audited.",
                        "example": 71
                      },
                      "lastAnalyzedAt": {
                        "type": "string",
                        "description": "When the last successful audit of this competitor completed. `null` if never audited.",
                        "example": "2026-09-19T04:31:52.000Z"
                      },
                      "assetFreshness": {
                        "type": "object",
                        "properties": {
                          "status": {
                            "type": "string",
                            "enum": [
                              "fresh",
                              "missing",
                              "stale"
                            ],
                            "description": "Whether usable crawl assets exist for this competitor. `missing` means\nnone was ever prepared; `stale` means they exist but have expired.",
                            "example": "fresh"
                          },
                          "totalAssetCount": {
                            "type": "number",
                            "description": "How many assets were crawled, expired ones included.",
                            "example": 12
                          },
                          "usableAssetCount": {
                            "type": "number",
                            "description": "How many of them are usable right now. Lower than `totalAssetCount`\nwhenever some have expired — the difference is what a new preparation\nwould recover.",
                            "example": 9
                          },
                          "lastPreparedAt": {
                            "type": "string",
                            "description": "When the assets were last prepared. `null` when none ever was.",
                            "example": "2026-09-23T22:04:09.000Z"
                          },
                          "expiresAt": {
                            "type": "string",
                            "description": "When the usable ones expire. Past this moment `status` reads `stale`.",
                            "example": "2026-09-24T22:04:09.000Z"
                          }
                        },
                        "description": "Freshness of the **crawl assets** used for editorial inspiration —\nthey expire in about a day. This is a different clock from the audit\nfreshness behind `competitorPosture` in the overview, whose window is\nthirty days. A `stale` status here does not contradict a `High Confidence`\ncohort there; the two measure different things."
                      },
                      "createdAt": {
                        "type": "string",
                        "example": "2026-08-30T10:22:41.000Z"
                      },
                      "updatedAt": {
                        "type": "string",
                        "example": "2026-09-19T04:31:52.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Competitors tracked for this project",
        "tags": [
          "Audits"
        ]
      }
    },
    "/v1/projects/{projectId}/contents": {
      "get": {
        "description": "The **editorial pieces produced for this project**, paginated — their topic, the formats derived from them, and the production status of each.\n\n**These are produced pieces, not the crawled pages of `GET /audits/content`.** The two are different populations and they do not overlap: this endpoint lists what the content engine wrote, the other counts what the crawler found on the site. A project that has audited sixty-eight pages and produced nothing returns an empty list here — that is an accurate answer, not an error.\n\nThis is the list behind `GET /contents/{contentId}`. It carries enough to choose a piece, not the piece's body.",
        "operationId": "listContents",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          },
          {
            "name": "pageSize",
            "required": false,
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 20,
              "maximum": 50
            },
            "description": "Items per page. **Values above 50 are silently reduced to 50** — the response is not an error, it is simply shorter than asked. Read the returned length rather than assuming it."
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page to read, 1-based."
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "topic": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Stable identifier of the topic. It is what a content item's `topicId`\npoints to.",
                                "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                              },
                              "projectId": {
                                "type": "string",
                                "description": "The project this topic belongs to.",
                                "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                              },
                              "title": {
                                "type": "string",
                                "description": "The topic as a working title. Not the published title of a piece: a\ncontent item derived from it carries its own.",
                                "example": "Why an incomplete Google listing costs calls"
                              },
                              "description": {
                                "type": "string",
                                "description": "What the topic covers, in a couple of sentences.",
                                "example": "How the three fields readers check before calling — hours, photos, answered reviews — decide whether a listing converts."
                              },
                              "strategic_note": {
                                "type": "string",
                                "description": "Why this topic is worth writing now, in the project's own strategy.",
                                "example": "Ranks for the queries the audit found unanswered, and the proof is already on hand."
                              },
                              "marketing_goal": {
                                "type": "string",
                                "description": "What the piece is meant to achieve commercially.",
                                "example": "Get a first audit request from a local business owner"
                              },
                              "target_problem": {
                                "type": "string",
                                "description": "The reader's problem it addresses, in their words rather than ours.",
                                "example": "People find me but never call"
                              },
                              "keywords": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose.",
                                  "example": "fiche google incomplète"
                                },
                                "example": [
                                  "fiche google incomplète",
                                  "visibilité locale artisan"
                                ],
                                "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose."
                              },
                              "intent": {
                                "type": "string",
                                "enum": [
                                  "commerciale",
                                  "informationnelle",
                                  "inspirationnelle",
                                  "locale",
                                  "navigationnelle",
                                  "transactionnelle",
                                  "unclassified",
                                  "éducationnelle"
                                ],
                                "description": "The search intent the topic answers. `null` when it was not\nclassified. **The values are French**, including `éducationnelle` with its\ndiacritic — they are stored values, not display labels, and comparing\nagainst `'informational'` will never match.",
                                "example": "informationnelle"
                              },
                              "persona": {
                                "type": "string",
                                "description": "The persona the topic is written for, by label. One persona, where\n`personaRelevance` scores them all.",
                                "example": "P1 restaurateur"
                              },
                              "category": {
                                "type": "string",
                                "enum": [
                                  "actionable_advice",
                                  "constructive_rant",
                                  "pedagogical_analysis",
                                  "personal_story",
                                  "strong_opinion"
                                ],
                                "description": "The editorial register the piece should take — advice, analysis,\nopinion, story or rant. It shapes the writing, not the format.",
                                "example": "actionable_advice"
                              },
                              "cluster": {
                                "type": "string",
                                "description": "The semantic cluster this topic belongs to. Grouping by meaning, not by keyword.",
                                "example": "visibilité locale"
                              },
                              "funnelStage": {
                                "type": "string",
                                "enum": [
                                  "BOFU",
                                  "MOFU",
                                  "POST",
                                  "TOFU"
                                ],
                                "description": "Which funnel stage the topic serves. `null` when it is not\npositioned — not the same as serving the top.",
                                "example": "TOFU"
                              },
                              "personaRelevance": {
                                "type": "object",
                                "description": "How relevant the topic is to each persona, keyed by persona label.\nA persona absent from the map was not scored — that is not a zero.",
                                "example": {
                                  "P1 restaurateur": 0.82,
                                  "P2 artisan": 0.47
                                }
                              },
                              "recommendedFormats": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "enum": [
                                    "alternative",
                                    "analysis",
                                    "behind_the_scenes",
                                    "best_of",
                                    "carousel",
                                    "case_study",
                                    "checklist",
                                    "comparison",
                                    "data_driven",
                                    "ebook",
                                    "error_list",
                                    "expert_roundup",
                                    "explainer",
                                    "faq",
                                    "glossary",
                                    "guide",
                                    "how_to",
                                    "image",
                                    "infographic",
                                    "landing_page",
                                    "listicle",
                                    "newsletter",
                                    "op_ed",
                                    "pdf",
                                    "playbook",
                                    "podcast_script",
                                    "research_report",
                                    "review_expert",
                                    "roadmap",
                                    "short_text",
                                    "short_video_script",
                                    "storytelling",
                                    "strategic_brief",
                                    "success_story",
                                    "thread",
                                    "trend_breakdown",
                                    "tutorial",
                                    "video_script",
                                    "white_paper"
                                  ],
                                  "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another.",
                                  "example": "how_to"
                                },
                                "example": [
                                  "how_to",
                                  "checklist"
                                ],
                                "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another."
                              },
                              "recommendedCTA": {
                                "type": "string",
                                "description": "The call to action the topic should end on.",
                                "example": "Check your own listing in two minutes"
                              },
                              "seoTitle": {
                                "type": "string",
                                "description": "The SEO `<title>` variant, when one was written. `null` until the\nmulti-surface generator has run — it is a different string from `title`,\nwritten for a search result rather than for a calendar.",
                                "example": "Incomplete Google listing: the 3 fields that cost you calls"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "approved",
                                  "archived",
                                  "drafted",
                                  "proposed",
                                  "published",
                                  "rejected"
                                ],
                                "description": "Where the topic stands in its life cycle. `proposed` comes from a\nresearch session and has not been accepted yet; `approved` is the default\nfor everything else, including topics that predate the life cycle.",
                                "example": "approved"
                              },
                              "archivedAt": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the topic was archived. `null` while it is active — archiving is reversible.",
                                "example": "2026-09-12T16:20:05.000Z"
                              },
                              "archiveReason": {
                                "type": "string",
                                "description": "Why it was archived, in prose. Explains; does not classify — do not branch on it.",
                                "example": "Covered by a piece published in July; keeping both would split the same query."
                              },
                              "createdAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2026-08-30T10:22:41.000Z"
                              },
                              "updatedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2026-09-19T04:31:52.000Z"
                              }
                            }
                          },
                          "topicId": {
                            "type": "string",
                            "description": "The topic this piece derives from. `null` for a piece created without\none — the strategic fields below are then the only framing it has.",
                            "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                          },
                          "canonicalTitle": {
                            "type": "string",
                            "description": "The working title of the piece. Editorial, not an SEO title — that lives on the format.",
                            "example": "Why an incomplete Google listing costs calls"
                          },
                          "clusterName": {
                            "type": "string",
                            "description": "The semantic cluster the piece belongs to. Grouping by meaning.",
                            "example": "visibilité locale"
                          },
                          "pillarName": {
                            "type": "string",
                            "description": "The editorial pillar it serves — broader than the cluster.",
                            "example": "SEO local"
                          },
                          "personaLabel": {
                            "type": "string",
                            "description": "The persona it is written for, by label.",
                            "example": "P1 restaurateur"
                          },
                          "category": {
                            "type": "string",
                            "enum": [
                              "actionable_advice",
                              "constructive_rant",
                              "pedagogical_analysis",
                              "personal_story",
                              "strong_opinion"
                            ],
                            "description": "The editorial register — advice, analysis, opinion, story or rant. It\nshapes the writing, not the format.",
                            "example": "actionable_advice"
                          },
                          "centralMessage": {
                            "type": "string",
                            "description": "The single idea the piece must carry. One sentence, by design.",
                            "example": "Three fields decide whether a listing gets called."
                          },
                          "selectedAngle": {
                            "type": "string",
                            "enum": [
                              "analyze_trend",
                              "break_myths",
                              "contrast_two_approaches",
                              "explain_concept",
                              "give_opinion",
                              "illustrate_with_example",
                              "propose_solution",
                              "share_behind_scenes",
                              "share_experience",
                              "show_common_mistake"
                            ],
                            "description": "The angle chosen among `possibleAngles`. `null` while nobody has chosen.",
                            "example": "show_common_mistake"
                          },
                          "desiredImpact": {
                            "type": "string",
                            "enum": [
                              "build_credibility",
                              "convince",
                              "educate",
                              "engage",
                              "inspire",
                              "make_reflect",
                              "motivate_to_act",
                              "provoke",
                              "reassure"
                            ],
                            "description": "What the piece is meant to change for its reader — the intent behind publishing it.",
                            "example": "motivate_to_act"
                          },
                          "targetProblem": {
                            "type": "string",
                            "description": "The reader's problem the piece addresses.",
                            "example": "People find me but never call"
                          },
                          "marketingGoal": {
                            "type": "string",
                            "description": "What the piece is meant to achieve commercially.",
                            "example": "Get a first audit request from a local business owner"
                          },
                          "possibleAngles": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "analyze_trend",
                                "break_myths",
                                "contrast_two_approaches",
                                "explain_concept",
                                "give_opinion",
                                "illustrate_with_example",
                                "propose_solution",
                                "share_behind_scenes",
                                "share_experience",
                                "show_common_mistake"
                              ],
                              "description": "The angles the strategy proposed for this topic. `selectedAngle` names\nthe one retained; the others stay here as alternatives, not as rejects.",
                              "example": "show_common_mistake"
                            },
                            "example": [
                              "show_common_mistake",
                              "propose_solution",
                              "illustrate_with_example"
                            ],
                            "description": "The angles the strategy proposed for this topic. `selectedAngle` names\nthe one retained; the others stay here as alternatives, not as rejects."
                          },
                          "recommendedFormats": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Formats the strategy suggests for this piece. A suggestion, not a\nconstraint: `formats` lists what actually exists, and may differ.",
                              "example": "how_to"
                            },
                            "example": [
                              "how_to",
                              "checklist"
                            ],
                            "description": "Formats the strategy suggests for this piece. A suggestion, not a\nconstraint: `formats` lists what actually exists, and may differ."
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The keywords the piece targets.",
                              "example": "fiche google incomplète"
                            },
                            "example": [
                              "fiche google incomplète",
                              "visibilité locale artisan"
                            ],
                            "description": "The keywords the piece targets."
                          },
                          "strategicNote": {
                            "type": "string",
                            "description": "Free-form guidance for whoever writes the piece. Prose, not a field to parse.",
                            "example": "Lead with the search that ends without a call; the audit has the figure."
                          },
                          "recommendedStructure": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Suggested section outline, in reading order. It is the **flat legacy**\nview: `brief.plan.sections` carries the same outline with its intentions and\nits evidence, and is what to read when it is present.",
                              "example": "What a reader checks first"
                            },
                            "example": [
                              "What a reader checks first",
                              "What a complete listing changes",
                              "What the numbers say"
                            ],
                            "description": "Suggested section outline, in reading order. It is the **flat legacy**\nview: `brief.plan.sections` carries the same outline with its intentions and\nits evidence, and is what to read when it is present."
                          },
                          "recommendedPhrases": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Turns of phrase to reuse — brand voice made concrete, not keywords.",
                              "example": "dit autrement"
                            },
                            "example": [
                              "dit autrement",
                              "ce que ça change concrètement"
                            ],
                            "description": "Turns of phrase to reuse — brand voice made concrete, not keywords."
                          },
                          "recommendedCTA": {
                            "type": "string",
                            "description": "The call to action the piece should end on.",
                            "example": "Check your own listing in two minutes"
                          },
                          "intent": {
                            "type": "string",
                            "enum": [
                              "commerciale",
                              "informationnelle",
                              "inspirationnelle",
                              "locale",
                              "navigationnelle",
                              "transactionnelle",
                              "unclassified",
                              "éducationnelle"
                            ],
                            "description": "Search intent behind the topic. **The values are French**, including\n`éducationnelle` with its diacritic — they are stored values, not display\nlabels, and comparing against `'informational'` will never match.",
                            "example": "informationnelle"
                          },
                          "funnelStage": {
                            "type": "string",
                            "enum": [
                              "BOFU",
                              "MOFU",
                              "POST",
                              "TOFU"
                            ],
                            "description": "Where the piece sits in the funnel — TOFU, MOFU, BOFU.",
                            "example": "TOFU"
                          },
                          "generationProvenance": {
                            "type": "object",
                            "properties": {
                              "sourceType": {
                                "type": "string",
                                "enum": [
                                  "ai-assisted",
                                  "ai-generated",
                                  "imported",
                                  "manual"
                                ],
                                "description": "How the piece came to exist. `ai-assisted` means a human drove it;\n`ai-generated` means the chain produced it. The distinction is what an\naudit of your own content asks for first.",
                                "example": "ai-assisted"
                              },
                              "auditAligned": {
                                "type": "boolean",
                                "description": "Whether the piece answers something an audit actually found. `false`\nis not a defect — it means the piece was framed outside the audit.",
                                "example": true
                              }
                            }
                          },
                          "generationStatus": {
                            "type": "string",
                            "enum": [
                              "failed",
                              "generating",
                              "ready"
                            ],
                            "description": "Where generation stands for this piece. `ready` means the brief is\ncomplete enough to write from — not that anything has been written.",
                            "example": "ready"
                          },
                          "formats": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "Stable identifier of this format. Its publications hang off it.",
                                  "example": "f1c07a49-3b82-4d16-9a5e-8c4d2b6f7013"
                                },
                                "contentId": {
                                  "type": "string",
                                  "description": "The content item this format belongs to.",
                                  "example": "d94b2f60-1a77-4c85-9e13-6b0f2a7c8d41"
                                },
                                "format": {
                                  "type": "string",
                                  "enum": [
                                    "alternative",
                                    "analysis",
                                    "behind_the_scenes",
                                    "best_of",
                                    "carousel",
                                    "case_study",
                                    "checklist",
                                    "comparison",
                                    "data_driven",
                                    "ebook",
                                    "error_list",
                                    "expert_roundup",
                                    "explainer",
                                    "faq",
                                    "glossary",
                                    "guide",
                                    "how_to",
                                    "image",
                                    "infographic",
                                    "landing_page",
                                    "listicle",
                                    "newsletter",
                                    "op_ed",
                                    "pdf",
                                    "playbook",
                                    "podcast_script",
                                    "research_report",
                                    "review_expert",
                                    "roadmap",
                                    "short_text",
                                    "short_video_script",
                                    "storytelling",
                                    "strategic_brief",
                                    "success_story",
                                    "thread",
                                    "trend_breakdown",
                                    "tutorial",
                                    "video_script",
                                    "white_paper"
                                  ],
                                  "description": "Which format this is. One content item carries several — an article and\nthe carousel drawn from it are two formats of the same piece.",
                                  "example": "how_to"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "archived",
                                    "draft",
                                    "published",
                                    "ready",
                                    "review",
                                    "scheduled"
                                  ],
                                  "description": "Where this format stands. `published` says a publication exists;\n`ready` says it could be published and has not been.",
                                  "example": "published"
                                },
                                "platform": {
                                  "type": "string",
                                  "description": "The platform this format targets, when it targets one. `null` for a\nformat that is platform-agnostic, such as a blog article.",
                                  "example": "linkedin"
                                },
                                "format_family": {
                                  "type": "string",
                                  "description": "The broad family of the format — what kind of artefact it is, where\n`format` is the precise editorial type.",
                                  "example": "long_form"
                                },
                                "format_variant": {
                                  "type": "string",
                                  "description": "The variant within the family.",
                                  "example": "article"
                                },
                                "title": {
                                  "type": "string",
                                  "description": "The title as this format publishes it. It may differ from the content\nitem's `canonicalTitle`: each format writes for its own surface.",
                                  "example": "Incomplete Google listing: the three fields that cost you calls"
                                },
                                "subtitle": {
                                  "type": "string",
                                  "description": "The subtitle, when the format has one.",
                                  "example": "What a reader checks before picking up the phone"
                                },
                                "summary": {
                                  "type": "string",
                                  "description": "A short summary of the format's content, for listings and previews.",
                                  "example": "Opening hours, photos and answered reviews decide the call. Here is how to check each one."
                                },
                                "createdAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "example": "2026-09-16T11:52:44.000Z"
                                },
                                "updatedAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "example": "2026-09-24T08:15:27.000Z"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "number",
                          "description": "Which page this is, starting at 1.",
                          "example": 1
                        },
                        "pageSize": {
                          "type": "number",
                          "description": "How many items a full page holds. The last page holds fewer.",
                          "example": 20
                        },
                        "totalPages": {
                          "type": "number",
                          "description": "How many pages the result spans.",
                          "example": 3
                        },
                        "totalItems": {
                          "type": "number",
                          "description": "How many items match the query in total, across every page — not the\nsize of the page returned. The field is `totalItems` here, where the other\npaginated lists call it `total`: read the name each endpoint uses.",
                          "example": 47
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Content items for this project",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/contents/{contentId}": {
      "get": {
        "description": "One **editorial piece** in full: its topic, its body, the formats derived from it, and where each has been published. Not a crawled page — those live under `GET /audits/content`.\n\nA content item and its formats are not the same object, and they do not share a lifecycle. The item carries the substance; each format is a rendering of it for one channel, with its own production status. An item that reads as finished can still hold formats that are not.",
        "operationId": "getContent",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          },
          {
            "name": "contentId",
            "required": true,
            "in": "path",
            "description": "The content item to read. Obtained from `GET /contents` — this surface does not resolve a title or a slug to an identifier.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "The project this piece belongs to.",
                      "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                    },
                    "topic": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable identifier of the topic. It is what a content item's `topicId`\npoints to.",
                          "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                        },
                        "projectId": {
                          "type": "string",
                          "description": "The project this topic belongs to.",
                          "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                        },
                        "title": {
                          "type": "string",
                          "description": "The topic as a working title. Not the published title of a piece: a\ncontent item derived from it carries its own.",
                          "example": "Why an incomplete Google listing costs calls"
                        },
                        "description": {
                          "type": "string",
                          "description": "What the topic covers, in a couple of sentences.",
                          "example": "How the three fields readers check before calling — hours, photos, answered reviews — decide whether a listing converts."
                        },
                        "strategic_note": {
                          "type": "string",
                          "description": "Why this topic is worth writing now, in the project's own strategy.",
                          "example": "Ranks for the queries the audit found unanswered, and the proof is already on hand."
                        },
                        "marketing_goal": {
                          "type": "string",
                          "description": "What the piece is meant to achieve commercially.",
                          "example": "Get a first audit request from a local business owner"
                        },
                        "target_problem": {
                          "type": "string",
                          "description": "The reader's problem it addresses, in their words rather than ours.",
                          "example": "People find me but never call"
                        },
                        "keywords": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose.",
                            "example": "fiche google incomplète"
                          },
                          "example": [
                            "fiche google incomplète",
                            "visibilité locale artisan"
                          ],
                          "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose."
                        },
                        "intent": {
                          "type": "string",
                          "enum": [
                            "commerciale",
                            "informationnelle",
                            "inspirationnelle",
                            "locale",
                            "navigationnelle",
                            "transactionnelle",
                            "unclassified",
                            "éducationnelle"
                          ],
                          "description": "The search intent the topic answers. `null` when it was not\nclassified. **The values are French**, including `éducationnelle` with its\ndiacritic — they are stored values, not display labels, and comparing\nagainst `'informational'` will never match.",
                          "example": "informationnelle"
                        },
                        "persona": {
                          "type": "string",
                          "description": "The persona the topic is written for, by label. One persona, where\n`personaRelevance` scores them all.",
                          "example": "P1 restaurateur"
                        },
                        "category": {
                          "type": "string",
                          "enum": [
                            "actionable_advice",
                            "constructive_rant",
                            "pedagogical_analysis",
                            "personal_story",
                            "strong_opinion"
                          ],
                          "description": "The editorial register the piece should take — advice, analysis,\nopinion, story or rant. It shapes the writing, not the format.",
                          "example": "actionable_advice"
                        },
                        "cluster": {
                          "type": "string",
                          "description": "The semantic cluster this topic belongs to. Grouping by meaning, not by keyword.",
                          "example": "visibilité locale"
                        },
                        "funnelStage": {
                          "type": "string",
                          "enum": [
                            "BOFU",
                            "MOFU",
                            "POST",
                            "TOFU"
                          ],
                          "description": "Which funnel stage the topic serves. `null` when it is not\npositioned — not the same as serving the top.",
                          "example": "TOFU"
                        },
                        "personaRelevance": {
                          "type": "object",
                          "description": "How relevant the topic is to each persona, keyed by persona label.\nA persona absent from the map was not scored — that is not a zero.",
                          "example": {
                            "P1 restaurateur": 0.82,
                            "P2 artisan": 0.47
                          }
                        },
                        "recommendedFormats": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "alternative",
                              "analysis",
                              "behind_the_scenes",
                              "best_of",
                              "carousel",
                              "case_study",
                              "checklist",
                              "comparison",
                              "data_driven",
                              "ebook",
                              "error_list",
                              "expert_roundup",
                              "explainer",
                              "faq",
                              "glossary",
                              "guide",
                              "how_to",
                              "image",
                              "infographic",
                              "landing_page",
                              "listicle",
                              "newsletter",
                              "op_ed",
                              "pdf",
                              "playbook",
                              "podcast_script",
                              "research_report",
                              "review_expert",
                              "roadmap",
                              "short_text",
                              "short_video_script",
                              "storytelling",
                              "strategic_brief",
                              "success_story",
                              "thread",
                              "trend_breakdown",
                              "tutorial",
                              "video_script",
                              "white_paper"
                            ],
                            "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another.",
                            "example": "how_to"
                          },
                          "example": [
                            "how_to",
                            "checklist"
                          ],
                          "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another."
                        },
                        "recommendedCTA": {
                          "type": "string",
                          "description": "The call to action the topic should end on.",
                          "example": "Check your own listing in two minutes"
                        },
                        "seoTitle": {
                          "type": "string",
                          "description": "The SEO `<title>` variant, when one was written. `null` until the\nmulti-surface generator has run — it is a different string from `title`,\nwritten for a search result rather than for a calendar.",
                          "example": "Incomplete Google listing: the 3 fields that cost you calls"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "approved",
                            "archived",
                            "drafted",
                            "proposed",
                            "published",
                            "rejected"
                          ],
                          "description": "Where the topic stands in its life cycle. `proposed` comes from a\nresearch session and has not been accepted yet; `approved` is the default\nfor everything else, including topics that predate the life cycle.",
                          "example": "approved"
                        },
                        "archivedAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the topic was archived. `null` while it is active — archiving is reversible.",
                          "example": "2026-09-12T16:20:05.000Z"
                        },
                        "archiveReason": {
                          "type": "string",
                          "description": "Why it was archived, in prose. Explains; does not classify — do not branch on it.",
                          "example": "Covered by a piece published in July; keeping both would split the same query."
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-08-30T10:22:41.000Z"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-09-19T04:31:52.000Z"
                        }
                      }
                    },
                    "topicId": {
                      "type": "string",
                      "description": "The topic this piece derives from. `null` for a piece created without\none — the strategic fields below are then the only framing it has.",
                      "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                    },
                    "canonicalTitle": {
                      "type": "string",
                      "description": "The working title of the piece. Editorial, not an SEO title — that lives on the format.",
                      "example": "Why an incomplete Google listing costs calls"
                    },
                    "clusterName": {
                      "type": "string",
                      "description": "The semantic cluster the piece belongs to. Grouping by meaning.",
                      "example": "visibilité locale"
                    },
                    "pillarName": {
                      "type": "string",
                      "description": "The editorial pillar it serves — broader than the cluster.",
                      "example": "SEO local"
                    },
                    "personaLabel": {
                      "type": "string",
                      "description": "The persona it is written for, by label.",
                      "example": "P1 restaurateur"
                    },
                    "category": {
                      "type": "string",
                      "enum": [
                        "actionable_advice",
                        "constructive_rant",
                        "pedagogical_analysis",
                        "personal_story",
                        "strong_opinion"
                      ],
                      "description": "The editorial register — advice, analysis, opinion, story or rant. It\nshapes the writing, not the format.",
                      "example": "actionable_advice"
                    },
                    "centralMessage": {
                      "type": "string",
                      "description": "The single idea the piece must carry. One sentence, by design.",
                      "example": "Three fields decide whether a listing gets called."
                    },
                    "selectedAngle": {
                      "type": "string",
                      "enum": [
                        "analyze_trend",
                        "break_myths",
                        "contrast_two_approaches",
                        "explain_concept",
                        "give_opinion",
                        "illustrate_with_example",
                        "propose_solution",
                        "share_behind_scenes",
                        "share_experience",
                        "show_common_mistake"
                      ],
                      "description": "The angle chosen among `possibleAngles`. `null` while nobody has chosen.",
                      "example": "show_common_mistake"
                    },
                    "desiredImpact": {
                      "type": "string",
                      "enum": [
                        "build_credibility",
                        "convince",
                        "educate",
                        "engage",
                        "inspire",
                        "make_reflect",
                        "motivate_to_act",
                        "provoke",
                        "reassure"
                      ],
                      "description": "What the piece is meant to change for its reader — the intent behind publishing it.",
                      "example": "motivate_to_act"
                    },
                    "targetProblem": {
                      "type": "string",
                      "description": "The reader's problem the piece addresses.",
                      "example": "People find me but never call"
                    },
                    "marketingGoal": {
                      "type": "string",
                      "description": "What the piece is meant to achieve commercially.",
                      "example": "Get a first audit request from a local business owner"
                    },
                    "possibleAngles": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "analyze_trend",
                          "break_myths",
                          "contrast_two_approaches",
                          "explain_concept",
                          "give_opinion",
                          "illustrate_with_example",
                          "propose_solution",
                          "share_behind_scenes",
                          "share_experience",
                          "show_common_mistake"
                        ],
                        "description": "The angles the strategy proposed for this topic. `selectedAngle` names\nthe one retained; the others stay here as alternatives, not as rejects.",
                        "example": "show_common_mistake"
                      },
                      "example": [
                        "show_common_mistake",
                        "propose_solution",
                        "illustrate_with_example"
                      ],
                      "description": "The angles the strategy proposed for this topic. `selectedAngle` names\nthe one retained; the others stay here as alternatives, not as rejects."
                    },
                    "recommendedFormats": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Formats the strategy suggests for this piece. A suggestion, not a\nconstraint: `formats` lists what actually exists, and may differ.",
                        "example": "how_to"
                      },
                      "example": [
                        "how_to",
                        "checklist"
                      ],
                      "description": "Formats the strategy suggests for this piece. A suggestion, not a\nconstraint: `formats` lists what actually exists, and may differ."
                    },
                    "keywords": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "The keywords the piece targets.",
                        "example": "fiche google incomplète"
                      },
                      "example": [
                        "fiche google incomplète",
                        "visibilité locale artisan"
                      ],
                      "description": "The keywords the piece targets."
                    },
                    "strategicNote": {
                      "type": "string",
                      "description": "Free-form guidance for whoever writes the piece. Prose, not a field to parse.",
                      "example": "Lead with the search that ends without a call; the audit has the figure."
                    },
                    "recommendedStructure": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Suggested section outline, in reading order. It is the **flat legacy**\nview: `brief.plan.sections` carries the same outline with its intentions and\nits evidence, and is what to read when it is present.",
                        "example": "What a reader checks first"
                      },
                      "example": [
                        "What a reader checks first",
                        "What a complete listing changes",
                        "What the numbers say"
                      ],
                      "description": "Suggested section outline, in reading order. It is the **flat legacy**\nview: `brief.plan.sections` carries the same outline with its intentions and\nits evidence, and is what to read when it is present."
                    },
                    "recommendedPhrases": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Turns of phrase to reuse — brand voice made concrete, not keywords.",
                        "example": "dit autrement"
                      },
                      "example": [
                        "dit autrement",
                        "ce que ça change concrètement"
                      ],
                      "description": "Turns of phrase to reuse — brand voice made concrete, not keywords."
                    },
                    "recommendedCTA": {
                      "type": "string",
                      "description": "The call to action the piece should end on.",
                      "example": "Check your own listing in two minutes"
                    },
                    "intent": {
                      "type": "string",
                      "enum": [
                        "commerciale",
                        "informationnelle",
                        "inspirationnelle",
                        "locale",
                        "navigationnelle",
                        "transactionnelle",
                        "unclassified",
                        "éducationnelle"
                      ],
                      "description": "Search intent behind the topic. **The values are French**, including\n`éducationnelle` with its diacritic — they are stored values, not display\nlabels, and comparing against `'informational'` will never match.",
                      "example": "informationnelle"
                    },
                    "funnelStage": {
                      "type": "string",
                      "enum": [
                        "BOFU",
                        "MOFU",
                        "POST",
                        "TOFU"
                      ],
                      "description": "Where the piece sits in the funnel — TOFU, MOFU, BOFU.",
                      "example": "TOFU"
                    },
                    "generationProvenance": {
                      "type": "object",
                      "properties": {
                        "sourceType": {
                          "type": "string",
                          "enum": [
                            "ai-assisted",
                            "ai-generated",
                            "imported",
                            "manual"
                          ],
                          "description": "How the piece came to exist. `ai-assisted` means a human drove it;\n`ai-generated` means the chain produced it. The distinction is what an\naudit of your own content asks for first.",
                          "example": "ai-assisted"
                        },
                        "auditAligned": {
                          "type": "boolean",
                          "description": "Whether the piece answers something an audit actually found. `false`\nis not a defect — it means the piece was framed outside the audit.",
                          "example": true
                        }
                      }
                    },
                    "generationStatus": {
                      "type": "string",
                      "enum": [
                        "failed",
                        "generating",
                        "ready"
                      ],
                      "description": "Where generation stands for this piece. `ready` means the brief is\ncomplete enough to write from — not that anything has been written.",
                      "example": "ready"
                    },
                    "brief": {
                      "type": "object",
                      "properties": {
                        "research": {
                          "type": "object",
                          "properties": {
                            "query": {
                              "type": "string",
                              "description": "The question the research agent was given. It is what the sources\nbelow were gathered to answer.",
                              "example": "What do local business owners check before calling a tradesperson?"
                            },
                            "potentialUsefulSources": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "source": {
                                    "type": "string",
                                    "description": "Where the source lives.",
                                    "example": "https://presse-eco.example/2026/etude-recherche-locale"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "The source's title, as read from the page. Absent on a brief\ncaptured before the 2.0.0 contract (`content-research.yaml`).",
                                    "example": "Baromètre 2026 de la visibilité locale des artisans"
                                  },
                                  "publisher": {
                                    "type": "string",
                                    "description": "The organization that publishes the source.",
                                    "example": "INSEE"
                                  },
                                  "publishedAt": {
                                    "type": "string",
                                    "description": "The page's publication date, ISO 8601, as read from its metadata.\n`null` when the page carries none — never guessed.",
                                    "example": "2026-03-12T00:00:00.000Z"
                                  },
                                  "kind": {
                                    "type": "string",
                                    "enum": [
                                      "primary",
                                      "secondary"
                                    ],
                                    "description": "`primary` for official documentation, an institution, a dated\nstudy, or the platform's own publisher; `secondary` otherwise.",
                                    "example": "primary"
                                  }
                                }
                              },
                              "description": "The sources the research retained, with what was taken from each."
                            },
                            "potentialInternalLinks": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "contentId": {
                                    "type": "string",
                                    "description": "**Legacy, and never produced any more.** The prompt used to ask for\nan internal-links tool that never existed, so the model invented these\nentries — links to pages that were never published. They are stripped at\ncapture now, and the field survives only to read briefs persisted before\nthat. Treat a value here as unverified, never as a link to follow.",
                                    "example": "d94b2f60-1a77-4c85-9e13-6b0f2a7c8d41"
                                  },
                                  "title": {
                                    "type": "string",
                                    "description": "The invented title of the legacy entry. See `contentId`."
                                  },
                                  "description": {
                                    "type": "string",
                                    "description": "The invented description of the legacy entry. See `contentId`."
                                  },
                                  "url": {
                                    "type": "string",
                                    "description": "The invented URL of the legacy entry. **It may not resolve** — this\nis precisely the defect the field documents."
                                  }
                                }
                              }
                            }
                          },
                          "description": "The research gathered for THIS piece, frozen at generation. It is not\nthe project's source library, and it is not refreshed afterwards."
                        },
                        "plan": {
                          "type": "object",
                          "properties": {
                            "titre_h1": {
                              "type": "string",
                              "description": "The page's `<h1>`, as planned. It may differ from the piece's\n`canonicalTitle`, which is editorial rather than published.",
                              "example": "Incomplete Google listing: the three fields that cost you calls"
                            },
                            "meta_description": {
                              "type": "string",
                              "description": "The meta description to publish.",
                              "example": "Opening hours, photos, answered reviews: what a reader checks before calling, and how to fix each one."
                            },
                            "persona_cible": {
                              "type": "string",
                              "description": "The ONE persona the piece is written for. A single value by design: a\npiece written for everyone reads as written for no one.",
                              "example": "P1 restaurateur"
                            },
                            "introduction_attendue": {
                              "type": "string",
                              "description": "What the introduction must do, in one or two sentences.",
                              "example": "Open on the search that ends without a call, then name the three fields."
                            },
                            "sections": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "h2": {
                                    "type": "string",
                                    "description": "The section's heading, as it should be written.",
                                    "example": "What a complete listing actually changes"
                                  },
                                  "answer_first": {
                                    "type": "string",
                                    "description": "A self-contained answer of at most 320 characters, written so that a\nsearch engine or an assistant can quote the section without the rest of the\npage. Not a summary: it must stand alone.",
                                    "example": "A listing with opening hours, photos and answered reviews gets called; one missing any of the three usually does not."
                                  },
                                  "intention": {
                                    "type": "string",
                                    "description": "What the section is for, for whoever writes it. Guidance, not copy.",
                                    "example": "Give the reader the three fields to check, in order of impact."
                                  }
                                }
                              },
                              "description": "The sections to write, in reading order."
                            },
                            "proof_section": {
                              "type": "object",
                              "properties": {
                                "h2": {
                                  "type": "string",
                                  "description": "The proof section's heading. Every plan carries one: the arc is\nsee, fix, then prove.",
                                  "example": "What the numbers say"
                                },
                                "intention": {
                                  "type": "string",
                                  "description": "What the section must establish.",
                                  "example": "Back the claim with a measured figure and name where it comes from."
                                }
                              }
                            },
                            "conclusion": {
                              "type": "string",
                              "description": "What the conclusion must land.",
                              "example": "Close on the two-minute check, then the call to action."
                            },
                            "takeaways": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "description": "The points a reader should leave with.",
                                "example": "Three fields decide the call"
                              },
                              "example": [
                                "Three fields decide the call",
                                "Answered reviews weigh more than their rating"
                              ],
                              "description": "The points a reader should leave with."
                            }
                          },
                          "description": "The editorial plan, whole. Every field is optional: a brief persisted\nbefore the full plan existed carries only part of it, and the flat fields on\nthe content item (`recommendedStructure`, `recommendedPhrases`) are what\nsurvived from that era."
                        },
                        "qualityReport": {
                          "type": "object",
                          "properties": {
                            "verdict": {
                              "type": "string",
                              "enum": [
                                "PASS",
                                "REJECT",
                                "REVISE"
                              ],
                              "description": "The structure judge's verdict on the plan. `REVISE` means it was sent\nback for rework; whether that rework happened is `reviseCount`.",
                              "example": "PASS"
                            },
                            "score": {
                              "type": "number",
                              "description": "The judge's score out of 100. It grades the SKELETON — the plan — not\nanything written from it.",
                              "example": 82
                            },
                            "dimensionScores": {
                              "type": "object",
                              "description": "The score broken down by dimension, keyed by dimension name.",
                              "example": {
                                "answer_first": 88,
                                "evidence": 74,
                                "structure": 85
                              }
                            },
                            "blockingFailures": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "section": {
                                    "type": "string",
                                    "description": "The plan section the judge refused.",
                                    "example": "What the numbers say"
                                  },
                                  "why": {
                                    "type": "string",
                                    "description": "Why the judge refused it, in one sentence.",
                                    "example": "The claim carries a figure with no source."
                                  }
                                }
                              },
                              "description": "What the judge refused, section by section. Empty on a `PASS`."
                            },
                            "reviseHints": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "description": "What to change, in the judge's words. Present on `REVISE`, and kept\nafter the fact for the audit trail.",
                                "example": "Give the proof section a measured figure with its source"
                              },
                              "example": [
                                "Give the proof section a measured figure with its source"
                              ],
                              "description": "What to change, in the judge's words. Present on `REVISE`, and kept\nafter the fact for the audit trail."
                            },
                            "reviseCount": {
                              "type": "number",
                              "description": "How many revision rounds the plan went through, capped at two. `0`\nmeans it passed first time.",
                              "example": 1
                            },
                            "titleGateFailures": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "section": {
                                    "type": "string",
                                    "description": "Which part of the title the check failed on — `titre_h1`, or a\nsection heading.",
                                    "example": "titre_h1"
                                  },
                                  "item": {
                                    "type": "string",
                                    "description": "Which deterministic rule failed, by name.",
                                    "example": "banned_pattern"
                                  },
                                  "why": {
                                    "type": "string",
                                    "description": "What the rule found, in one sentence.",
                                    "example": "Opens on « Découvrez », which the house style bans."
                                  }
                                }
                              }
                            },
                            "enforced": {
                              "type": "boolean",
                              "description": "Whether the gate **blocked** or merely advised. Advisory is the\ndefault: a report with failures and `enforced: false` did not stop\nanything, and the piece may well have been written anyway.",
                              "example": false
                            },
                            "judgedAt": {
                              "type": "string",
                              "description": "When the plan was judged.",
                              "example": "2026-09-16T11:48:02.000Z"
                            }
                          },
                          "description": "The structure judge's report. Absent for a brief that was never\njudged — which is not the same as a brief that failed."
                        },
                        "generatedAt": {
                          "type": "string",
                          "description": "When the brief was generated. The piece may have been written much\nlater, or not at all.",
                          "example": "2026-09-16T11:42:19.000Z"
                        }
                      }
                    },
                    "formats": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of this format. Its publications hang off it.",
                            "example": "f1c07a49-3b82-4d16-9a5e-8c4d2b6f7013"
                          },
                          "contentId": {
                            "type": "string",
                            "description": "The content item this format belongs to.",
                            "example": "d94b2f60-1a77-4c85-9e13-6b0f2a7c8d41"
                          },
                          "format": {
                            "type": "string",
                            "enum": [
                              "alternative",
                              "analysis",
                              "behind_the_scenes",
                              "best_of",
                              "carousel",
                              "case_study",
                              "checklist",
                              "comparison",
                              "data_driven",
                              "ebook",
                              "error_list",
                              "expert_roundup",
                              "explainer",
                              "faq",
                              "glossary",
                              "guide",
                              "how_to",
                              "image",
                              "infographic",
                              "landing_page",
                              "listicle",
                              "newsletter",
                              "op_ed",
                              "pdf",
                              "playbook",
                              "podcast_script",
                              "research_report",
                              "review_expert",
                              "roadmap",
                              "short_text",
                              "short_video_script",
                              "storytelling",
                              "strategic_brief",
                              "success_story",
                              "thread",
                              "trend_breakdown",
                              "tutorial",
                              "video_script",
                              "white_paper"
                            ],
                            "description": "Which format this is. One content item carries several — an article and\nthe carousel drawn from it are two formats of the same piece.",
                            "example": "how_to"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "archived",
                              "draft",
                              "published",
                              "ready",
                              "review",
                              "scheduled"
                            ],
                            "description": "Where this format stands. `published` says a publication exists;\n`ready` says it could be published and has not been.",
                            "example": "published"
                          },
                          "platform": {
                            "type": "string",
                            "description": "The platform this format targets, when it targets one. `null` for a\nformat that is platform-agnostic, such as a blog article.",
                            "example": "linkedin"
                          },
                          "format_family": {
                            "type": "string",
                            "description": "The broad family of the format — what kind of artefact it is, where\n`format` is the precise editorial type.",
                            "example": "long_form"
                          },
                          "format_variant": {
                            "type": "string",
                            "description": "The variant within the family.",
                            "example": "article"
                          },
                          "title": {
                            "type": "string",
                            "description": "The title as this format publishes it. It may differ from the content\nitem's `canonicalTitle`: each format writes for its own surface.",
                            "example": "Incomplete Google listing: the three fields that cost you calls"
                          },
                          "subtitle": {
                            "type": "string",
                            "description": "The subtitle, when the format has one.",
                            "example": "What a reader checks before picking up the phone"
                          },
                          "summary": {
                            "type": "string",
                            "description": "A short summary of the format's content, for listings and previews.",
                            "example": "Opening hours, photos and answered reviews decide the call. Here is how to check each one."
                          },
                          "outline": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The format's section headings, in reading order.",
                              "example": "What a reader checks first"
                            },
                            "example": [
                              "What a reader checks first",
                              "What a complete listing changes"
                            ],
                            "description": "The format's section headings, in reading order."
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The keywords this format targets. They may narrow the content item's\nown list: a carousel does not chase the same queries as an article.",
                              "example": "fiche google incomplète"
                            },
                            "example": [
                              "fiche google incomplète"
                            ],
                            "description": "The keywords this format targets. They may narrow the content item's\nown list: a carousel does not chase the same queries as an article."
                          },
                          "publications": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "Stable identifier of this published version.",
                                  "example": "7e2b9c04-6d51-4a38-b0f9-1c8e5a3d7f62"
                                },
                                "contentFormatId": {
                                  "type": "string",
                                  "description": "The format this publication came from.",
                                  "example": "f1c07a49-3b82-4d16-9a5e-8c4d2b6f7013"
                                },
                                "versionNumber": {
                                  "type": "number",
                                  "description": "Which version this is, starting at 1. A format republished keeps its\nearlier versions: the list is a history, not a single current state.",
                                  "example": 2
                                },
                                "publishedAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "description": "When this version went out.",
                                  "example": "2026-09-24T08:15:27.000Z"
                                },
                                "platform": {
                                  "type": "string",
                                  "description": "Where it was published, as the platform is named in the product.",
                                  "example": "Website"
                                },
                                "url": {
                                  "type": "string",
                                  "description": "The public URL of this version. `null` for a platform that exposes\nnone, or when it was published outside the product.",
                                  "example": "https://www.acme.example/blog/fiche-google-incomplete"
                                },
                                "platformId": {
                                  "type": "string",
                                  "description": "The platform's own identifier for the post — a WordPress post id, a\nLinkedIn URN. It is what lets a later metrics pull find it again.",
                                  "example": "wp-1482"
                                },
                                "platformConnectionId": {
                                  "type": "string",
                                  "description": "Which stored connection published it. `null` for a publication\nrecorded by hand rather than pushed by the product.",
                                  "example": "2c5f8a31-9b04-4e76-8d12-3a7c6b9e0f54"
                                },
                                "title": {
                                  "type": "string",
                                  "description": "The title as published in THIS version. It is frozen: editing the\nformat afterwards does not rewrite it.",
                                  "example": "Incomplete Google listing: the three fields that cost you calls"
                                },
                                "subtitle": {
                                  "type": "string",
                                  "description": "The subtitle as published in this version.",
                                  "example": "What a reader checks before picking up the phone"
                                },
                                "summary": {
                                  "type": "string",
                                  "description": "The summary as published in this version.",
                                  "example": "Opening hours, photos and answered reviews decide the call."
                                },
                                "metadata": {
                                  "type": "object"
                                },
                                "createdAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "example": "2026-09-24T08:15:27.000Z"
                                }
                              }
                            }
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-09-16T11:52:44.000Z"
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-09-24T08:15:27.000Z"
                          }
                        }
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-16T11:40:03.000Z"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-24T08:15:27.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "A single content item",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/topics": {
      "get": {
        "description": "Editorial topics, paginated. **These are the source of the identifiers a content item is derived from** — an integrator that creates content starts here.\n\nArchived and rejected topics are hidden by default. `includeArchived` brings them back, and this matters: an archived topic is still derivable. Hidden is not the same as unusable.",
        "operationId": "listTopics",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          },
          {
            "name": "pageSize",
            "required": false,
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 20,
              "maximum": 50
            },
            "description": "Items per page. **Values above 50 are silently reduced to 50** — the response is not an error, it is simply shorter than asked. Read the returned length rather than assuming it."
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page to read, 1-based."
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "coverage": {
                      "type": "object",
                      "properties": {
                        "globalScore": {
                          "type": "number",
                          "description": "Overall coverage score, or `null` while it is **not computed**. It is\nnever a fabricated zero: a real zero stays a legitimate verdict, so treat\n`null` as unknown and say so rather than rendering an empty gauge.",
                          "example": 64
                        },
                        "personaCoverage": {
                          "type": "object",
                          "description": "Coverage per persona, keyed by persona label, 0-100. A persona absent\nfrom the map has no topic at all — not a zero score.",
                          "example": {
                            "P1 restaurateur": 72,
                            "P2 artisan": 41
                          }
                        },
                        "intentCoverage": {
                          "type": "object",
                          "description": "Coverage per search intent, keyed by intent. **The keys are French\nstored values** — `informationnelle`, `éducationnelle` with its diacritic —\nnot display labels.",
                          "example": {
                            "informationnelle": 68,
                            "transactionnelle": 35
                          }
                        },
                        "pillarCoverage": {
                          "type": "object",
                          "description": "Coverage per editorial pillar, keyed by pillar name.",
                          "example": {
                            "visibilité locale": 74,
                            "e-réputation": 52
                          }
                        },
                        "clusterCoverage": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "cluster": {
                                "type": "string",
                                "description": "The semantic cluster this score is about, by name.",
                                "example": "visibilité locale"
                              },
                              "score": {
                                "type": "number",
                                "description": "How well the topics cover this cluster, from 0 to 100.",
                                "example": 74
                              }
                            }
                          },
                          "description": "Coverage per semantic cluster, as a list rather than a map."
                        },
                        "funnelCoverage": {
                          "type": "object",
                          "description": "Coverage per funnel stage, keyed by stage.",
                          "example": {
                            "TOFU": 71,
                            "MOFU": 46,
                            "BOFU": 22
                          }
                        }
                      },
                      "description": "How much of the project's editorial ground the topics actually cover,\nby persona, intent, pillar, cluster and funnel stage. Volumetric coverage:\nit counts topics, it does not judge them."
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "totalTopics": {
                          "type": "number",
                          "description": "How many topics the project holds, archived ones excluded.",
                          "example": 47
                        },
                        "totalPillars": {
                          "type": "number",
                          "description": "How many distinct pillars they span.",
                          "example": 6
                        },
                        "totalClusters": {
                          "type": "number",
                          "description": "How many distinct semantic clusters.",
                          "example": 11
                        },
                        "totalIntents": {
                          "type": "number",
                          "description": "How many distinct search intents.",
                          "example": 4
                        },
                        "totalPersonas": {
                          "type": "number",
                          "description": "How many distinct personas are addressed.",
                          "example": 3
                        },
                        "topicsByIntent": {
                          "type": "object",
                          "description": "Topic counts per intent, keyed by intent. These are counts; the\npercentages live in `coverage.intentCoverage`.",
                          "example": {
                            "informationnelle": 29,
                            "transactionnelle": 11,
                            "navigationnelle": 5,
                            "commerciale": 2
                          }
                        }
                      },
                      "description": "Volumes behind `coverage` — what the percentages are computed on."
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier of the topic. It is what a content item's `topicId`\npoints to.",
                            "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                          },
                          "projectId": {
                            "type": "string",
                            "description": "The project this topic belongs to.",
                            "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                          },
                          "title": {
                            "type": "string",
                            "description": "The topic as a working title. Not the published title of a piece: a\ncontent item derived from it carries its own.",
                            "example": "Why an incomplete Google listing costs calls"
                          },
                          "description": {
                            "type": "string",
                            "description": "What the topic covers, in a couple of sentences.",
                            "example": "How the three fields readers check before calling — hours, photos, answered reviews — decide whether a listing converts."
                          },
                          "strategic_note": {
                            "type": "string",
                            "description": "Why this topic is worth writing now, in the project's own strategy.",
                            "example": "Ranks for the queries the audit found unanswered, and the proof is already on hand."
                          },
                          "marketing_goal": {
                            "type": "string",
                            "description": "What the piece is meant to achieve commercially.",
                            "example": "Get a first audit request from a local business owner"
                          },
                          "target_problem": {
                            "type": "string",
                            "description": "The reader's problem it addresses, in their words rather than ours.",
                            "example": "People find me but never call"
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose.",
                              "example": "fiche google incomplète"
                            },
                            "example": [
                              "fiche google incomplète",
                              "visibilité locale artisan"
                            ],
                            "description": "The keywords the topic targets. Empty when none was retained — not the\nsame as targeting none on purpose."
                          },
                          "intent": {
                            "type": "string",
                            "enum": [
                              "commerciale",
                              "informationnelle",
                              "inspirationnelle",
                              "locale",
                              "navigationnelle",
                              "transactionnelle",
                              "unclassified",
                              "éducationnelle"
                            ],
                            "description": "The search intent the topic answers. `null` when it was not\nclassified. **The values are French**, including `éducationnelle` with its\ndiacritic — they are stored values, not display labels, and comparing\nagainst `'informational'` will never match.",
                            "example": "informationnelle"
                          },
                          "persona": {
                            "type": "string",
                            "description": "The persona the topic is written for, by label. One persona, where\n`personaRelevance` scores them all.",
                            "example": "P1 restaurateur"
                          },
                          "category": {
                            "type": "string",
                            "enum": [
                              "actionable_advice",
                              "constructive_rant",
                              "pedagogical_analysis",
                              "personal_story",
                              "strong_opinion"
                            ],
                            "description": "The editorial register the piece should take — advice, analysis,\nopinion, story or rant. It shapes the writing, not the format.",
                            "example": "actionable_advice"
                          },
                          "cluster": {
                            "type": "string",
                            "description": "The semantic cluster this topic belongs to. Grouping by meaning, not by keyword.",
                            "example": "visibilité locale"
                          },
                          "funnelStage": {
                            "type": "string",
                            "enum": [
                              "BOFU",
                              "MOFU",
                              "POST",
                              "TOFU"
                            ],
                            "description": "Which funnel stage the topic serves. `null` when it is not\npositioned — not the same as serving the top.",
                            "example": "TOFU"
                          },
                          "personaRelevance": {
                            "type": "object",
                            "description": "How relevant the topic is to each persona, keyed by persona label.\nA persona absent from the map was not scored — that is not a zero.",
                            "example": {
                              "P1 restaurateur": 0.82,
                              "P2 artisan": 0.47
                            }
                          },
                          "recommendedFormats": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "alternative",
                                "analysis",
                                "behind_the_scenes",
                                "best_of",
                                "carousel",
                                "case_study",
                                "checklist",
                                "comparison",
                                "data_driven",
                                "ebook",
                                "error_list",
                                "expert_roundup",
                                "explainer",
                                "faq",
                                "glossary",
                                "guide",
                                "how_to",
                                "image",
                                "infographic",
                                "landing_page",
                                "listicle",
                                "newsletter",
                                "op_ed",
                                "pdf",
                                "playbook",
                                "podcast_script",
                                "research_report",
                                "review_expert",
                                "roadmap",
                                "short_text",
                                "short_video_script",
                                "storytelling",
                                "strategic_brief",
                                "success_story",
                                "thread",
                                "trend_breakdown",
                                "tutorial",
                                "video_script",
                                "white_paper"
                              ],
                              "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another.",
                              "example": "how_to"
                            },
                            "example": [
                              "how_to",
                              "checklist"
                            ],
                            "description": "The formats that suit this topic, in the house vocabulary. A\nrecommendation, not a constraint: nothing stops a piece from taking another."
                          },
                          "recommendedCTA": {
                            "type": "string",
                            "description": "The call to action the topic should end on.",
                            "example": "Check your own listing in two minutes"
                          },
                          "seoTitle": {
                            "type": "string",
                            "description": "The SEO `<title>` variant, when one was written. `null` until the\nmulti-surface generator has run — it is a different string from `title`,\nwritten for a search result rather than for a calendar.",
                            "example": "Incomplete Google listing: the 3 fields that cost you calls"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "approved",
                              "archived",
                              "drafted",
                              "proposed",
                              "published",
                              "rejected"
                            ],
                            "description": "Where the topic stands in its life cycle. `proposed` comes from a\nresearch session and has not been accepted yet; `approved` is the default\nfor everything else, including topics that predate the life cycle.",
                            "example": "approved"
                          },
                          "archivedAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the topic was archived. `null` while it is active — archiving is reversible.",
                            "example": "2026-09-12T16:20:05.000Z"
                          },
                          "archiveReason": {
                            "type": "string",
                            "description": "Why it was archived, in prose. Explains; does not classify — do not branch on it.",
                            "example": "Covered by a piece published in July; keeping both would split the same query."
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-08-30T10:22:41.000Z"
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-09-19T04:31:52.000Z"
                          }
                        }
                      },
                      "description": "The page's items, in the order the query returned them."
                    },
                    "total": {
                      "type": "number",
                      "description": "How many items match the query in total, across every page — not the\nsize of `items`.",
                      "example": 47
                    },
                    "page": {
                      "type": "number",
                      "description": "Which page this is, starting at 1.",
                      "example": 1
                    },
                    "pageSize": {
                      "type": "number",
                      "description": "How many items a full page holds. The last page holds fewer.",
                      "example": 20
                    },
                    "limit": {
                      "type": "number",
                      "description": "The limit that was applied, when the request asked for one. It echoes\nthe request; `pageSize` is what the response actually used.",
                      "example": 20
                    },
                    "hasNextPage": {
                      "type": "boolean",
                      "description": "Whether another page follows. Prefer it over comparing `page` to\n`totalPages` — it stays right when the count is capped.",
                      "example": true
                    },
                    "hasPreviousPage": {
                      "type": "boolean",
                      "description": "Whether a page precedes this one.",
                      "example": false
                    },
                    "totalPages": {
                      "type": "number",
                      "description": "How many pages the result spans, derived from `total` and `pageSize`.\n`0` when `pageSize` is not positive.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Editorial topics for this project",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/editorial-calendar": {
      "get": {
        "description": "The project's social editorial calendar: reels, carousels and posts, planned or in production, each with its status.\n\n**This is the social calendar, not the content list.** The two are separate surfaces: a content item is written matter, a calendar entry is a piece scheduled for a social channel. They are produced by different pipelines and neither contains the other.\n\nThe status of a piece governs what may be done with it: only `validating` accepts a revision, and publishing to the network is never done here — it stays a human gesture, and no capability declares it.",
        "operationId": "listCalendarItems",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable identifier of the calendar item. It is what `decisionLog`\nentries and the package folder are keyed on.",
                        "example": "a2f7c3d1-8b45-4e69-9c02-5d1e7f8a3b60"
                      },
                      "projectId": {
                        "type": "string",
                        "description": "The project this calendar belongs to.",
                        "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                      },
                      "title": {
                        "type": "string",
                        "description": "The piece's working title, as it appears on the calendar.",
                        "example": "Three signs your Google listing is costing you calls"
                      },
                      "weekLabel": {
                        "type": "string",
                        "description": "The week the piece is planned for, as a label. Grouping, not a date — see `plannedDate`.",
                        "example": "S39"
                      },
                      "dayLabel": {
                        "type": "string",
                        "description": "The day within the week, as a label. Also grouping, not a date.",
                        "example": "mardi"
                      },
                      "plannedDate": {
                        "type": "string",
                        "description": "The calendar date the piece is planned for. `null` while only the week is decided.",
                        "example": "2026-09-29"
                      },
                      "format": {
                        "type": "string",
                        "enum": [
                          "carousel",
                          "post",
                          "reel"
                        ],
                        "description": "Which kind of piece this is. It decides which part of\n`productionBrief` is filled: `scenes` for a reel, `slides` for a carousel,\n`post` for a single image.",
                        "example": "reel"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "failed",
                          "in_production",
                          "measured",
                          "planned",
                          "published",
                          "ready_to_post",
                          "rejected",
                          "scripted",
                          "validating"
                        ],
                        "description": "Where the piece stands in production, from `planned` to `measured`.\nThe order is not alphabetical and the path is not linear: `validating` can\nreturn to `scripted` after a rejection, and `failed` carries its reason in\n`failureReason`. Branch on this value, never on its position in the list.",
                        "example": "ready_to_post"
                      },
                      "personas": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "description": "The personas this piece targets, by label. Empty when it targets everyone.",
                          "example": "P1 restaurateur"
                        },
                        "example": [
                          "P1 restaurateur",
                          "P2 artisan"
                        ],
                        "description": "The personas this piece targets, by label. Empty when it targets everyone."
                      },
                      "funnelStage": {
                        "type": "string",
                        "enum": [
                          "BOFU",
                          "MOFU",
                          "POST",
                          "TOFU"
                        ],
                        "description": "Which funnel stage the piece serves. `null` when the piece is not\npositioned — not the same as serving the top of the funnel.",
                        "example": "TOFU"
                      },
                      "topicId": {
                        "type": "string",
                        "description": "The topic this piece derives from, when it derives from one. `null` for\na piece written straight onto the calendar. The topic itself is not\nincluded: read it through the topics endpoint.",
                        "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                      },
                      "productionBrief": {
                        "type": "object",
                        "properties": {
                          "hook": {
                            "type": "object",
                            "properties": {
                              "pattern": {
                                "type": "string",
                                "description": "Which hook pattern was used, from the house library. It says how the\nhook is built, not what it says.",
                                "example": "chiffre-contre-intuitif"
                              },
                              "onScreen": {
                                "type": "string",
                                "description": "The hook as it appears on screen. This is the text the hook-quality\nmachine judges — the only authority on it.",
                                "example": "68% of local searches end without a call"
                              },
                              "spoken": {
                                "type": "string",
                                "description": "The hook as it is spoken, when it differs from the on-screen text.\nAbsent for a format with no voice.",
                                "example": "Sixty-eight percent of local searches end without a single call."
                              }
                            },
                            "description": "The first three seconds, which decide whether the rest is seen."
                          },
                          "scenes": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "index": {
                                  "type": "number",
                                  "description": "Position of the scene in the reel, starting at 1. The scenes are\nordered by this, not by their place in the array.",
                                  "example": 2
                                },
                                "duration": {
                                  "type": "number",
                                  "description": "Target length of the scene, in seconds. A target for whoever edits the\npiece, not a measured duration.",
                                  "example": 4
                                },
                                "onScreenText": {
                                  "type": "string",
                                  "description": "The overlay text shown during the scene.",
                                  "example": "Three quarters of the calls never happen"
                                },
                                "voiceover": {
                                  "type": "string",
                                  "description": "What is said over the scene.",
                                  "example": "Most of those searches end on a listing that answers nothing."
                                },
                                "visualDescription": {
                                  "type": "string",
                                  "description": "What should be on screen, described for whoever shoots or assembles it.",
                                  "example": "Screen recording of a listing with no opening hours, zoomed on the empty field"
                                }
                              }
                            },
                            "description": "The reel's scenes, in order. Present for `format: \"reel\"` only — a\ncarousel carries `slides` instead, and a post carries `post`."
                          },
                          "slides": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "index": {
                                  "type": "number",
                                  "description": "Position of the slide, starting at 1.",
                                  "example": 3
                                },
                                "role": {
                                  "type": "string",
                                  "description": "What the slide is for editorially — hook, problem, proof, mechanism,\ncall to action. It is what makes the sequence readable, not decoration.",
                                  "example": "preuve"
                                },
                                "title": {
                                  "type": "string",
                                  "description": "The slide's headline.",
                                  "example": "What a complete listing changes"
                                },
                                "body": {
                                  "type": "string",
                                  "description": "The slide's body text.",
                                  "example": "Opening hours, photos and answered reviews: the three fields that decide whether someone calls."
                                },
                                "visualDescription": {
                                  "type": "string",
                                  "description": "What should be on the slide, described for whoever designs it.",
                                  "example": "Side-by-side of the same listing before and after completion"
                                }
                              }
                            },
                            "description": "The carousel's slides, in order. Present for `format: \"carousel\"` only."
                          },
                          "post": {
                            "type": "object",
                            "properties": {
                              "onScreenText": {
                                "type": "string",
                                "description": "The text shown on the single image.",
                                "example": "Your listing answers one question out of three"
                              },
                              "visualDescription": {
                                "type": "string",
                                "description": "What should be on the image, described for whoever designs it.",
                                "example": "Close-up of a phone showing an incomplete listing"
                              }
                            },
                            "description": "The single image and its text. Present for `format: \"post\"` only."
                          },
                          "caption": {
                            "type": "string",
                            "description": "The caption as it will be published, ready to paste.",
                            "example": "Three fields decide whether someone calls you. Most listings fill one."
                          },
                          "hashtags": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The hashtags to publish with it, without the leading `#`.",
                              "example": "visibilitelocale"
                            },
                            "example": [
                              "visibilitelocale",
                              "artisan",
                              "googlebusiness"
                            ],
                            "description": "The hashtags to publish with it, without the leading `#`."
                          },
                          "microStep": {
                            "type": "string",
                            "description": "The one action the piece asks for. Deliberately small — it is what the\npiece is measured against.",
                            "example": "Tester"
                          },
                          "cta": {
                            "type": "string",
                            "description": "The call to action, in the words that will be published.",
                            "example": "Check your own listing in two minutes — link in bio."
                          },
                          "heygenSceneBriefs": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "scene": {
                                  "type": "number",
                                  "description": "Which scene of `scenes` this brief is for, by its `index`.",
                                  "example": 2
                                },
                                "prompt": {
                                  "type": "string",
                                  "description": "The prompt to paste into the avatar tool. Nothing is generated\nautomatically from it — a human runs it.",
                                  "example": "Medium shot, neutral background, calm delivery, no gestures"
                                },
                                "script": {
                                  "type": "string",
                                  "description": "What the avatar says, word for word.",
                                  "example": "Most of those searches end on a listing that answers nothing."
                                }
                              }
                            },
                            "description": "Per-scene briefs for the avatar tool. Reels only, and only when the\npiece is produced with a digital twin rather than voice alone."
                          },
                          "montageBrief": {
                            "type": "string",
                            "description": "Editing instructions for whoever assembles the reel. Assembly is\nmanual; nothing here is executed automatically.",
                            "example": "Cut on the beat between scenes 2 and 3, keep the screen recording full-frame."
                          },
                          "revisions": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "at": {
                                  "type": "string",
                                  "description": "When the judge asked for this revision.",
                                  "example": "2026-09-22T11:04:37.000Z"
                                },
                                "hint": {
                                  "type": "string",
                                  "description": "What the judge asked to change, in one sentence.",
                                  "example": "The hook states a figure without naming its source."
                                }
                              }
                            },
                            "description": "Every revision the judge asked for, oldest first. An empty or absent\nlist means the brief passed on its first attempt."
                          }
                        },
                        "description": "Everything needed to produce the piece — hook, scenes or slides,\ncaption, hashtags, call to action. `null` until the piece reaches\n`scripted`. Which sub-object is filled follows `format`."
                      },
                      "packageAssets": {
                        "type": "object",
                        "properties": {
                          "folder": {
                            "type": "string",
                            "description": "Where the package's files live, as a path within the asset store.",
                            "example": "social/acme/S39-mardi"
                          },
                          "assembledAt": {
                            "type": "string",
                            "description": "When the package was assembled. Later than the brief, earlier than the\npublication.",
                            "example": "2026-09-27T08:22:10.000Z"
                          },
                          "files": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "description": "File name inside the package folder.",
                                  "example": "caption.txt"
                                },
                                "kind": {
                                  "type": "string",
                                  "enum": [
                                    "caption",
                                    "hashtags",
                                    "heygen-briefs",
                                    "manifest",
                                    "montage-brief",
                                    "visual"
                                  ],
                                  "description": "What the file is. Decides whether `url` or `content` carries it.",
                                  "example": "caption"
                                },
                                "url": {
                                  "type": "string",
                                  "description": "Where a generated visual lives. **The link is durable and\nunauthenticated**: anyone holding it can fetch the asset, and it does not\nexpire. Logging it or forwarding it publishes the asset. Text files carry\ntheir bytes in `content` instead, and have no `url`.",
                                  "example": "https://assets.example/social/acme/S39-mardi/visual-1.png"
                                },
                                "content": {
                                  "type": "string",
                                  "description": "The file's text, inline. Present for text files; a visual carries `url` instead.",
                                  "example": "Three fields decide whether someone calls you. Most listings fill one."
                                }
                              }
                            },
                            "description": "The files of the package. Texts carry their bytes; visuals carry a link."
                          }
                        },
                        "description": "The ready-to-post package: the assembled files and where they live.\n`null` until the piece reaches `ready_to_post`."
                      },
                      "decisionLog": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "at": {
                              "type": "string",
                              "description": "When the decision was taken.",
                              "example": "2026-09-26T14:07:52.000Z"
                            },
                            "source": {
                              "type": "string",
                              "enum": [
                                "agent",
                                "callback",
                                "dashboard",
                                "n8n"
                              ],
                              "description": "Where the decision came from — a human in the dashboard, an automation,\na signed callback, or an agent through the API. It is what tells a reader\nwhether a person looked at the piece.",
                              "example": "dashboard"
                            },
                            "decision": {
                              "type": "string",
                              "description": "What was decided, in the vocabulary of the production loop.",
                              "example": "validate"
                            },
                            "feedback": {
                              "type": "string",
                              "description": "What was said along with it. Absent for a decision taken without a\ncomment.",
                              "example": "Hook is good; trim the last scene by a second."
                            },
                            "postUrl": {
                              "type": "string",
                              "description": "The published post, when the decision is what recorded it. This is how\na piece gets its performance loop.",
                              "example": "https://social.example/p/acme-S39-mardi"
                            }
                          }
                        },
                        "description": "Every decision taken on this piece, oldest first, with where it came\nfrom. This is the audit trail of the production loop: a rejection and the\nrevision that answered it both appear here."
                      },
                      "failureReason": {
                        "type": "string",
                        "description": "Why production stopped, in prose. Present only alongside a failed\n`status`; it explains, it does not classify — do not branch on it.",
                        "example": "The judge rejected the hook twice; production stopped rather than publish a third attempt."
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-09-15T09:41:18.000Z"
                      },
                      "updatedAt": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-09-26T14:07:52.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Editorial calendar entries",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/editorial-calendar/{itemId}": {
      "get": {
        "description": "One calendar entry: its framing, its production status, and the reason it failed if it did.\n\n**This is the polling probe for social production.** A piece moves through `planned` → `in_production` → `validating` → `ready_to_post`, or lands on `rejected`/`failed` with a `failureReason`. Read it until the status is terminal; there is no callback.",
        "operationId": "getCalendarItem",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          },
          {
            "name": "itemId",
            "required": true,
            "in": "path",
            "description": "The calendar entry to read. Obtained from `GET /editorial-calendar`, or returned by the capability that queues a piece for production.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "The project this calendar belongs to.",
                      "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                    },
                    "title": {
                      "type": "string",
                      "description": "The piece's working title, as it appears on the calendar.",
                      "example": "Three signs your Google listing is costing you calls"
                    },
                    "weekLabel": {
                      "type": "string",
                      "description": "The week the piece is planned for, as a label. Grouping, not a date — see `plannedDate`.",
                      "example": "S39"
                    },
                    "dayLabel": {
                      "type": "string",
                      "description": "The day within the week, as a label. Also grouping, not a date.",
                      "example": "mardi"
                    },
                    "plannedDate": {
                      "type": "string",
                      "description": "The calendar date the piece is planned for. `null` while only the week is decided.",
                      "example": "2026-09-29"
                    },
                    "format": {
                      "type": "string",
                      "enum": [
                        "carousel",
                        "post",
                        "reel"
                      ],
                      "description": "Which kind of piece this is. It decides which part of\n`productionBrief` is filled: `scenes` for a reel, `slides` for a carousel,\n`post` for a single image.",
                      "example": "reel"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "failed",
                        "in_production",
                        "measured",
                        "planned",
                        "published",
                        "ready_to_post",
                        "rejected",
                        "scripted",
                        "validating"
                      ],
                      "description": "Where the piece stands in production, from `planned` to `measured`.\nThe order is not alphabetical and the path is not linear: `validating` can\nreturn to `scripted` after a rejection, and `failed` carries its reason in\n`failureReason`. Branch on this value, never on its position in the list.",
                      "example": "ready_to_post"
                    },
                    "personas": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "The personas this piece targets, by label. Empty when it targets everyone.",
                        "example": "P1 restaurateur"
                      },
                      "example": [
                        "P1 restaurateur",
                        "P2 artisan"
                      ],
                      "description": "The personas this piece targets, by label. Empty when it targets everyone."
                    },
                    "funnelStage": {
                      "type": "string",
                      "enum": [
                        "BOFU",
                        "MOFU",
                        "POST",
                        "TOFU"
                      ],
                      "description": "Which funnel stage the piece serves. `null` when the piece is not\npositioned — not the same as serving the top of the funnel.",
                      "example": "TOFU"
                    },
                    "topicId": {
                      "type": "string",
                      "description": "The topic this piece derives from, when it derives from one. `null` for\na piece written straight onto the calendar. The topic itself is not\nincluded: read it through the topics endpoint.",
                      "example": "6b1d8e35-4c70-4a92-8f16-2d9c7a0b5e48"
                    },
                    "productionBrief": {
                      "type": "object",
                      "properties": {
                        "hook": {
                          "type": "object",
                          "properties": {
                            "pattern": {
                              "type": "string",
                              "description": "Which hook pattern was used, from the house library. It says how the\nhook is built, not what it says.",
                              "example": "chiffre-contre-intuitif"
                            },
                            "onScreen": {
                              "type": "string",
                              "description": "The hook as it appears on screen. This is the text the hook-quality\nmachine judges — the only authority on it.",
                              "example": "68% of local searches end without a call"
                            },
                            "spoken": {
                              "type": "string",
                              "description": "The hook as it is spoken, when it differs from the on-screen text.\nAbsent for a format with no voice.",
                              "example": "Sixty-eight percent of local searches end without a single call."
                            }
                          },
                          "description": "The first three seconds, which decide whether the rest is seen."
                        },
                        "scenes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "number",
                                "description": "Position of the scene in the reel, starting at 1. The scenes are\nordered by this, not by their place in the array.",
                                "example": 2
                              },
                              "duration": {
                                "type": "number",
                                "description": "Target length of the scene, in seconds. A target for whoever edits the\npiece, not a measured duration.",
                                "example": 4
                              },
                              "onScreenText": {
                                "type": "string",
                                "description": "The overlay text shown during the scene.",
                                "example": "Three quarters of the calls never happen"
                              },
                              "voiceover": {
                                "type": "string",
                                "description": "What is said over the scene.",
                                "example": "Most of those searches end on a listing that answers nothing."
                              },
                              "visualDescription": {
                                "type": "string",
                                "description": "What should be on screen, described for whoever shoots or assembles it.",
                                "example": "Screen recording of a listing with no opening hours, zoomed on the empty field"
                              }
                            }
                          },
                          "description": "The reel's scenes, in order. Present for `format: \"reel\"` only — a\ncarousel carries `slides` instead, and a post carries `post`."
                        },
                        "slides": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "number",
                                "description": "Position of the slide, starting at 1.",
                                "example": 3
                              },
                              "role": {
                                "type": "string",
                                "description": "What the slide is for editorially — hook, problem, proof, mechanism,\ncall to action. It is what makes the sequence readable, not decoration.",
                                "example": "preuve"
                              },
                              "title": {
                                "type": "string",
                                "description": "The slide's headline.",
                                "example": "What a complete listing changes"
                              },
                              "body": {
                                "type": "string",
                                "description": "The slide's body text.",
                                "example": "Opening hours, photos and answered reviews: the three fields that decide whether someone calls."
                              },
                              "visualDescription": {
                                "type": "string",
                                "description": "What should be on the slide, described for whoever designs it.",
                                "example": "Side-by-side of the same listing before and after completion"
                              }
                            }
                          },
                          "description": "The carousel's slides, in order. Present for `format: \"carousel\"` only."
                        },
                        "post": {
                          "type": "object",
                          "properties": {
                            "onScreenText": {
                              "type": "string",
                              "description": "The text shown on the single image.",
                              "example": "Your listing answers one question out of three"
                            },
                            "visualDescription": {
                              "type": "string",
                              "description": "What should be on the image, described for whoever designs it.",
                              "example": "Close-up of a phone showing an incomplete listing"
                            }
                          },
                          "description": "The single image and its text. Present for `format: \"post\"` only."
                        },
                        "caption": {
                          "type": "string",
                          "description": "The caption as it will be published, ready to paste.",
                          "example": "Three fields decide whether someone calls you. Most listings fill one."
                        },
                        "hashtags": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The hashtags to publish with it, without the leading `#`.",
                            "example": "visibilitelocale"
                          },
                          "example": [
                            "visibilitelocale",
                            "artisan",
                            "googlebusiness"
                          ],
                          "description": "The hashtags to publish with it, without the leading `#`."
                        },
                        "microStep": {
                          "type": "string",
                          "description": "The one action the piece asks for. Deliberately small — it is what the\npiece is measured against.",
                          "example": "Tester"
                        },
                        "cta": {
                          "type": "string",
                          "description": "The call to action, in the words that will be published.",
                          "example": "Check your own listing in two minutes — link in bio."
                        },
                        "heygenSceneBriefs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "scene": {
                                "type": "number",
                                "description": "Which scene of `scenes` this brief is for, by its `index`.",
                                "example": 2
                              },
                              "prompt": {
                                "type": "string",
                                "description": "The prompt to paste into the avatar tool. Nothing is generated\nautomatically from it — a human runs it.",
                                "example": "Medium shot, neutral background, calm delivery, no gestures"
                              },
                              "script": {
                                "type": "string",
                                "description": "What the avatar says, word for word.",
                                "example": "Most of those searches end on a listing that answers nothing."
                              }
                            }
                          },
                          "description": "Per-scene briefs for the avatar tool. Reels only, and only when the\npiece is produced with a digital twin rather than voice alone."
                        },
                        "montageBrief": {
                          "type": "string",
                          "description": "Editing instructions for whoever assembles the reel. Assembly is\nmanual; nothing here is executed automatically.",
                          "example": "Cut on the beat between scenes 2 and 3, keep the screen recording full-frame."
                        },
                        "revisions": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "at": {
                                "type": "string",
                                "description": "When the judge asked for this revision.",
                                "example": "2026-09-22T11:04:37.000Z"
                              },
                              "hint": {
                                "type": "string",
                                "description": "What the judge asked to change, in one sentence.",
                                "example": "The hook states a figure without naming its source."
                              }
                            }
                          },
                          "description": "Every revision the judge asked for, oldest first. An empty or absent\nlist means the brief passed on its first attempt."
                        }
                      },
                      "description": "Everything needed to produce the piece — hook, scenes or slides,\ncaption, hashtags, call to action. `null` until the piece reaches\n`scripted`. Which sub-object is filled follows `format`."
                    },
                    "packageAssets": {
                      "type": "object",
                      "properties": {
                        "folder": {
                          "type": "string",
                          "description": "Where the package's files live, as a path within the asset store.",
                          "example": "social/acme/S39-mardi"
                        },
                        "assembledAt": {
                          "type": "string",
                          "description": "When the package was assembled. Later than the brief, earlier than the\npublication.",
                          "example": "2026-09-27T08:22:10.000Z"
                        },
                        "files": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string",
                                "description": "File name inside the package folder.",
                                "example": "caption.txt"
                              },
                              "kind": {
                                "type": "string",
                                "enum": [
                                  "caption",
                                  "hashtags",
                                  "heygen-briefs",
                                  "manifest",
                                  "montage-brief",
                                  "visual"
                                ],
                                "description": "What the file is. Decides whether `url` or `content` carries it.",
                                "example": "caption"
                              },
                              "url": {
                                "type": "string",
                                "description": "Where a generated visual lives. **The link is durable and\nunauthenticated**: anyone holding it can fetch the asset, and it does not\nexpire. Logging it or forwarding it publishes the asset. Text files carry\ntheir bytes in `content` instead, and have no `url`.",
                                "example": "https://assets.example/social/acme/S39-mardi/visual-1.png"
                              },
                              "content": {
                                "type": "string",
                                "description": "The file's text, inline. Present for text files; a visual carries `url` instead.",
                                "example": "Three fields decide whether someone calls you. Most listings fill one."
                              }
                            }
                          },
                          "description": "The files of the package. Texts carry their bytes; visuals carry a link."
                        }
                      },
                      "description": "The ready-to-post package: the assembled files and where they live.\n`null` until the piece reaches `ready_to_post`."
                    },
                    "decisionLog": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "description": "When the decision was taken.",
                            "example": "2026-09-26T14:07:52.000Z"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "agent",
                              "callback",
                              "dashboard",
                              "n8n"
                            ],
                            "description": "Where the decision came from — a human in the dashboard, an automation,\na signed callback, or an agent through the API. It is what tells a reader\nwhether a person looked at the piece.",
                            "example": "dashboard"
                          },
                          "decision": {
                            "type": "string",
                            "description": "What was decided, in the vocabulary of the production loop.",
                            "example": "validate"
                          },
                          "feedback": {
                            "type": "string",
                            "description": "What was said along with it. Absent for a decision taken without a\ncomment.",
                            "example": "Hook is good; trim the last scene by a second."
                          },
                          "postUrl": {
                            "type": "string",
                            "description": "The published post, when the decision is what recorded it. This is how\na piece gets its performance loop.",
                            "example": "https://social.example/p/acme-S39-mardi"
                          }
                        }
                      },
                      "description": "Every decision taken on this piece, oldest first, with where it came\nfrom. This is the audit trail of the production loop: a rejection and the\nrevision that answered it both appear here."
                    },
                    "failureReason": {
                      "type": "string",
                      "description": "Why production stopped, in prose. Present only alongside a failed\n`status`; it explains, it does not classify — do not branch on it.",
                      "example": "The judge rejected the hook twice; production stopped rather than publish a third attempt."
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-15T09:41:18.000Z"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-26T14:07:52.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "A single editorial calendar entry",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/editorial-strategy": {
      "get": {
        "description": "The project's editorial strategy: context, personas, content clusters, and the state of its generation.\n\nThe generation state is part of the answer. A project whose strategy has never been generated returns an empty one — that is « not yet produced », not « nothing to say », and the two would look identical without the state.",
        "operationId": "getEditorialStrategy",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "The project this strategy belongs to.",
                      "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                    },
                    "mainObjective": {
                      "type": "string",
                      "description": "The objective the project stated for itself. What a person typed —\nabsent until someone fills it in, and never inferred.",
                      "example": "remplir la salle en semaine"
                    },
                    "secondaryObjectives": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Further objectives, stated by the project.",
                        "example": "être trouvé sur les recherches de quartier"
                      },
                      "example": [
                        "être trouvé sur les recherches de quartier"
                      ],
                      "description": "Further objectives, stated by the project."
                    },
                    "targetPersona": {
                      "type": "string",
                      "description": "The persona the project named as its main audience, in its own words.",
                      "example": "des habitués du quartier, plutôt 35-60 ans"
                    },
                    "sector": {
                      "type": "string",
                      "description": "The project's sector. Stated by the project, or derived by the audit when it was not.",
                      "example": "restauration indépendante"
                    },
                    "offer": {
                      "type": "string",
                      "description": "What the project sells, in its own words.",
                      "example": "une cuisine de saison, midi et soir, sans carte figée"
                    },
                    "toneOfVoice": {
                      "type": "string",
                      "description": "The tone the project asked for. See `brand-voice` for the structured version.",
                      "example": "chaleureux, jamais guindé"
                    },
                    "preferredContentFormats": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Formats the project prefers — a preference expressed, not a constraint applied.",
                        "example": "short_video_script"
                      },
                      "example": [
                        "short_video_script",
                        "carousel"
                      ],
                      "description": "Formats the project prefers — a preference expressed, not a constraint applied."
                    },
                    "context": {
                      "type": "object",
                      "properties": {
                        "services": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What the project sells, as distinct offers.",
                            "example": "pose de menuiseries sur mesure"
                          },
                          "example": [
                            "pose de menuiseries sur mesure",
                            "rénovation de façade"
                          ],
                          "description": "What the project sells, as distinct offers."
                        },
                        "objectif": {
                          "type": "string",
                          "description": "The commercial objective the strategy derived.",
                          "example": "générer des demandes de devis qualifiées"
                        },
                        "secteur": {
                          "type": "string",
                          "description": "The sector as the strategy reads it — it may be narrower than what the\nproject stated about itself.",
                          "example": "artisanat du bâtiment"
                        },
                        "type_offre": {
                          "type": "string",
                          "description": "What kind of offer it is: a service, a product, a subscription.",
                          "example": "prestation sur devis"
                        },
                        "promesse": {
                          "type": "string",
                          "description": "The promise the project makes, in one sentence.",
                          "example": "une pose propre, dans les délais annoncés"
                        },
                        "problematique": {
                          "type": "string",
                          "description": "The problem it solves, phrased as the reader would.",
                          "example": "les artisans ne rappellent pas, et les devis n'arrivent jamais"
                        },
                        "benefice": {
                          "type": "string",
                          "description": "The benefit the reader gets, as opposed to the feature that delivers it.",
                          "example": "savoir quand les travaux finissent, et que ce soit vrai"
                        },
                        "preuve_sociale": {
                          "type": "string",
                          "description": "The social proof available — what can be shown, not what is claimed.",
                          "example": "127 avis Google à 4,8 et deux chantiers publics référencés"
                        },
                        "intention_achat": {
                          "type": "string",
                          "description": "How ready to buy the audience is, in the strategy's words.",
                          "example": "forte sur devis, faible sur l'entretien préventif"
                        },
                        "valeurs_de_marque": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The brand values the content should carry.",
                            "example": "franchise sur les délais"
                          },
                          "example": [
                            "franchise sur les délais",
                            "travail visible"
                          ],
                          "description": "The brand values the content should carry."
                        },
                        "cible": {
                          "type": "object",
                          "properties": {
                            "secteur": {
                              "type": "string",
                              "description": "The sector the audience belongs to — the market's, not the\nproject's own.",
                              "example": "particuliers propriétaires"
                            },
                            "localisation": {
                              "type": "string",
                              "description": "Where the audience is. For a local business it is the catchment\narea.",
                              "example": "métropole de Lyon"
                            },
                            "niveau_maturite_digitale": {
                              "type": "string",
                              "description": "How digitally mature the audience is. It decides how much the content\nhas to explain before it can convince.",
                              "example": "faible : recherche sur mobile, appelle plutôt qu'écrire"
                            }
                          },
                          "description": "Who the content targets, at the level of a market rather than a person."
                        },
                        "persona_principal": {
                          "type": "object",
                          "properties": {
                            "prenom": {
                              "type": "string",
                              "description": "The sketched persona's first name.",
                              "example": "Sylvie"
                            },
                            "age": {
                              "type": "string",
                              "description": "The sketched persona's age, as a phrase.",
                              "example": "48 ans"
                            },
                            "situation": {
                              "type": "string",
                              "description": "The situation the sketched persona is in when they need the offer.",
                              "example": "propriétaire d'une maison de 1970 à rénover"
                            },
                            "motivation": {
                              "type": "string",
                              "description": "What pushes them to act now rather than later.",
                              "example": "en finir avec les fenêtres qui laissent passer le froid"
                            },
                            "freins": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "description": "What holds this persona back.",
                                "example": "peur du chantier qui traîne"
                              },
                              "example": [
                                "peur du chantier qui traîne",
                                "devis incomparables entre eux"
                              ],
                              "description": "What holds this persona back."
                            }
                          },
                          "description": "A single sketched persona, kept for the prompts that read it. The\nstructured audience profiles live in `personas` on the strategy, which is\nricher and is what to prefer."
                        },
                        "besoins_specifiques": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "Needs the content must answer.",
                            "example": "comprendre ce qui fait varier un devis"
                          },
                          "example": [
                            "comprendre ce qui fait varier un devis",
                            "savoir quelles aides existent"
                          ],
                          "description": "Needs the content must answer."
                        },
                        "freins_a_l_achat": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What stops a purchase. Distinct from `persona_principal.freins`: these\nare the market's objections, not one person's.",
                            "example": "prix perçu comme opaque"
                          },
                          "example": [
                            "prix perçu comme opaque",
                            "délais jamais garantis"
                          ],
                          "description": "What stops a purchase. Distinct from `persona_principal.freins`: these\nare the market's objections, not one person's."
                        },
                        "canaux_favoris": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "Where the audience already is.",
                            "example": "recherche Google"
                          },
                          "example": [
                            "recherche Google",
                            "bouche-à-oreille",
                            "Facebook local"
                          ],
                          "description": "Where the audience already is."
                        },
                        "questions_recurrentes": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "Questions the audience keeps asking. They are the best raw material for\ncontent that gets found.",
                            "example": "combien coûte le remplacement d'une fenêtre ?"
                          },
                          "example": [
                            "combien coûte le remplacement d'une fenêtre ?",
                            "faut-il une autorisation ?"
                          ],
                          "description": "Questions the audience keeps asking. They are the best raw material for\ncontent that gets found."
                        },
                        "scenarios_utilisation": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The situations in which the offer is used.",
                            "example": "rénovation avant revente"
                          },
                          "example": [
                            "rénovation avant revente",
                            "mise aux normes après achat"
                          ],
                          "description": "The situations in which the offer is used."
                        },
                        "mots_cles_opportunites": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "Keyword opportunities the strategy spotted. Opportunities, not measured\nvolumes — `searchIntents[].keywordVolumes` carries those.",
                            "example": "remplacement fenêtre prix"
                          },
                          "example": [
                            "remplacement fenêtre prix",
                            "aide rénovation énergétique 2026"
                          ],
                          "description": "Keyword opportunities the strategy spotted. Opportunities, not measured\nvolumes — `searchIntents[].keywordVolumes` carries those."
                        },
                        "verticals": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The verticals the project operates in.",
                            "example": "menuiserie"
                          },
                          "example": [
                            "menuiserie",
                            "isolation"
                          ],
                          "description": "The verticals the project operates in."
                        },
                        "categories": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The content categories the strategy retained for this project.",
                            "example": "guides pratiques"
                          },
                          "example": [
                            "guides pratiques",
                            "coulisses de chantier"
                          ],
                          "description": "The content categories the strategy retained for this project."
                        }
                      },
                      "description": "The context the strategy derived, as opposed to what the project\nstated. When a field exists in both, the stated value wins: an empty string\nis a deliberate erasure, not an absence of input."
                    },
                    "personas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "age": {
                            "type": "string",
                            "description": "The persona's age, as a phrase rather than a number — a range or an\napproximation is fine.",
                            "example": "52 ans"
                          },
                          "role": {
                            "type": "string",
                            "description": "The persona's role in their own organisation — not their role in the\npurchase, which is `decision_role`.",
                            "example": "gérant"
                          },
                          "peurs": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What this persona is afraid of. Deeper than `freins`, which are\npractical obstacles.",
                              "example": "se retrouver avec un chantier à l'arrêt"
                            },
                            "example": [
                              "se retrouver avec un chantier à l'arrêt"
                            ],
                            "description": "What this persona is afraid of. Deeper than `freins`, which are\npractical obstacles."
                          },
                          "freins": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The practical obstacles in their way.",
                              "example": "ne sait pas comparer deux devis"
                            },
                            "example": [
                              "ne sait pas comparer deux devis"
                            ],
                            "description": "The practical obstacles in their way."
                          },
                          "prenom": {
                            "type": "string",
                            "description": "The first name the persona goes by. It is how clusters refer to it\nin `personas_cibles`.",
                            "example": "Marc"
                          },
                          "secteur": {
                            "type": "string",
                            "description": "The sector the persona works in — theirs, which may differ from the\nproject's.",
                            "example": "restauration indépendante"
                          },
                          "kpi_cible": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "kpi": {
                                  "type": "string",
                                  "description": "The metric this persona is judged on.",
                                  "example": "couverts en semaine"
                                },
                                "cible": {
                                  "type": "string",
                                  "description": "The target they aim for on that metric.",
                                  "example": "+15 % en trois mois"
                                }
                              }
                            },
                            "description": "The KPI this persona is judged on, and the target they aim for. It is\nwhat a piece written for them should promise to move."
                          },
                          "objectifs": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What the persona is trying to achieve, in their own terms.",
                              "example": "remplir la salle en semaine"
                            },
                            "example": [
                              "remplir la salle en semaine"
                            ],
                            "description": "What the persona is trying to achieve, in their own terms."
                          },
                          "delai_cible": {
                            "type": "string",
                            "description": "The horizon over which they expect a result.",
                            "example": "sous trois mois"
                          },
                          "description": {
                            "type": "string",
                            "description": "A short portrait: who they are, what they run, what constrains them.\nThe one field to read before the rest.",
                            "example": "Gérant d'un restaurant de 40 couverts, seul aux commandes du numérique, sans temps ni goût pour ça."
                          },
                          "budget_bande": {
                            "type": "string",
                            "description": "The budget band they sit in, as a range rather than a figure.",
                            "example": "200 à 500 € par mois"
                          },
                          "title_hooks": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Title openings that work on this persona. Reusable as written.",
                              "example": "ce que vos clients cherchent avant de réserver"
                            },
                            "example": [
                              "ce que vos clients cherchent avant de réserver"
                            ],
                            "description": "Title openings that work on this persona. Reusable as written."
                          },
                          "cta_preferes": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The calls to action this persona responds to.",
                              "example": "demander un diagnostic gratuit"
                            },
                            "example": [
                              "demander un diagnostic gratuit"
                            ],
                            "description": "The calls to action this persona responds to."
                          },
                          "funnel_stage": {
                            "type": "string",
                            "description": "Where this persona usually sits in the funnel when they meet the\nbrand.",
                            "example": "MOFU"
                          },
                          "localisation": {
                            "type": "string",
                            "description": "Where the persona operates. It matters most for local businesses.",
                            "example": "Lyon et première couronne"
                          },
                          "stack_outils": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The tools they already use. It says what an integration can rely on.",
                              "example": "Google Business Profile"
                            },
                            "example": [
                              "Google Business Profile",
                              "Instagram"
                            ],
                            "description": "The tools they already use. It says what an integration can rely on."
                          },
                          "decision_role": {
                            "type": "string",
                            "description": "Their role in the purchase — decider, influencer, gatekeeper.\nDistinct from `role`.",
                            "example": "décideur unique"
                          },
                          "angle_editorial": {
                            "type": "string",
                            "description": "The angle that works on this persona, as one sentence.",
                            "example": "montrer le coût de l'inaction, sans dramatiser"
                          },
                          "awareness_level": {
                            "type": "string",
                            "description": "How aware they are, in Schwartz's sense: it decides whether the\ncontent must name the problem or can go straight to the offer.",
                            "example": "conscient du problème, pas des solutions"
                          },
                          "ton_editorial": {
                            "type": "string",
                            "description": "The tone to write in for this persona. It can differ from the\nbrand's default voice.",
                            "example": "direct, concret, sans jargon"
                          },
                          "canaux_preferes": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Where they consume content. `canaux_commerciaux` is where they buy.",
                              "example": "recherche Google"
                            },
                            "example": [
                              "recherche Google",
                              "Instagram"
                            ],
                            "description": "Where they consume content. `canaux_commerciaux` is where they buy."
                          },
                          "topics_exemples": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Topics this persona would click on, as titles. Illustrations of the\nangle, not a content plan.",
                              "example": "pourquoi votre fiche Google fait fuir les réservations"
                            },
                            "example": [
                              "pourquoi votre fiche Google fait fuir les réservations"
                            ],
                            "description": "Topics this persona would click on, as titles. Illustrations of the\nangle, not a content plan."
                          },
                          "attentes_contenu": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What this persona expects from a piece of content before they will\nact on it.",
                              "example": "des exemples chiffrés"
                            },
                            "example": [
                              "des exemples chiffrés",
                              "des étapes applicables le jour même"
                            ],
                            "description": "What this persona expects from a piece of content before they will\nact on it."
                          },
                          "citation_typique": {
                            "type": "string",
                            "description": "Something this persona would actually say. It is the voice to write\nfor, not a quote to publish.",
                            "example": "On me trouve, mais personne ne réserve."
                          },
                          "next_best_action": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The next step to offer, sized so it can be done immediately.",
                              "example": "vérifier sa fiche en deux minutes"
                            },
                            "example": [
                              "vérifier sa fiche en deux minutes"
                            ],
                            "description": "The next step to offer, sized so it can be done immediately."
                          },
                          "sensibilite_prix": {
                            "type": "string",
                            "description": "How price-sensitive they are, in prose. It shapes how an offer\nshould be framed, not whether to mention it.",
                            "example": "élevée : compare systématiquement trois offres"
                          },
                          "criteres_decision": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What they weigh when choosing. Ordered by importance, most decisive\nfirst.",
                              "example": "preuve locale"
                            },
                            "example": [
                              "preuve locale",
                              "délai annoncé tenu"
                            ],
                            "description": "What they weigh when choosing. Ordered by importance, most decisive\nfirst."
                          },
                          "preuves_preferees": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The kind of proof that convinces THIS persona — not the proof the\nproject finds most flattering.",
                              "example": "avis de voisins de quartier"
                            },
                            "example": [
                              "avis de voisins de quartier",
                              "avant/après daté"
                            ],
                            "description": "The kind of proof that convinces THIS persona — not the proof the\nproject finds most flattering."
                          },
                          "valeurs_attendues": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The values they expect a supplier to show. Brand values that do not\nappear here will not land.",
                              "example": "franchise"
                            },
                            "example": [
                              "franchise",
                              "proximité"
                            ],
                            "description": "The values they expect a supplier to show. Brand values that do not\nappear here will not land."
                          },
                          "canaux_commerciaux": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Where they actually buy. Often not where they read.",
                              "example": "téléphone"
                            },
                            "example": [
                              "téléphone",
                              "formulaire de contact"
                            ],
                            "description": "Where they actually buy. Often not where they read."
                          },
                          "contexte_recherche": {
                            "type": "string",
                            "description": "When and how they search. It constrains format and length more than\nany preference does.",
                            "example": "le soir, sur mobile, après le service"
                          },
                          "cycle_achat_estime": {
                            "type": "string",
                            "description": "How long they usually take from first need to purchase.",
                            "example": "deux à six semaines"
                          },
                          "moment_declencheur": {
                            "type": "string",
                            "description": "The moment that turns a latent need into a search.",
                            "example": "une semaine creuse de trop"
                          },
                          "besoins_specifiques": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Needs particular to this persona, beyond the sector's general ones.",
                              "example": "comprendre ce que voit un client avant de réserver"
                            },
                            "example": [
                              "comprendre ce que voit un client avant de réserver"
                            ],
                            "description": "Needs particular to this persona, beyond the sector's general ones."
                          },
                          "habitudes_digitales": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "How they use digital tools day to day. It decides which channels and\nformats reach them.",
                              "example": "consulte ses avis une fois par semaine"
                            },
                            "example": [
                              "consulte ses avis une fois par semaine"
                            ],
                            "description": "How they use digital tools day to day. It decides which channels and\nformats reach them."
                          },
                          "evenement_declencheur": {
                            "type": "string",
                            "description": "A concrete event that triggers the search, where\n`moment_declencheur` is the situation.",
                            "example": "un avis négatif resté sans réponse"
                          },
                          "motivations_profondes": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "What drives them underneath the stated objective.",
                              "example": "être reconnu comme une bonne adresse"
                            },
                            "example": [
                              "être reconnu comme une bonne adresse"
                            ],
                            "description": "What drives them underneath the stated objective."
                          },
                          "format_contenu_prefere": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The formats they take in most readily. A preference, not a\nconstraint.",
                              "example": "liste de contrôle"
                            },
                            "example": [
                              "liste de contrôle",
                              "vidéo courte"
                            ],
                            "description": "The formats they take in most readily. A preference, not a\nconstraint."
                          },
                          "questions_prioritaires": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The questions to answer first. Ordered: the first one is what the\npiece should open on.",
                              "example": "que voient mes clients quand ils me cherchent ?"
                            },
                            "example": [
                              "que voient mes clients quand ils me cherchent ?"
                            ],
                            "description": "The questions to answer first. Ordered: the first one is what the\npiece should open on."
                          },
                          "objections_prioritaires": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The objections to answer first, before the persona will engage.",
                              "example": "je n'ai pas le temps de m'en occuper"
                            },
                            "example": [
                              "je n'ai pas le temps de m'en occuper"
                            ],
                            "description": "The objections to answer first, before the persona will engage."
                          },
                          "niveau_maturite_digitale": {
                            "type": "string",
                            "description": "How comfortable they are with digital tools. It decides how much a\npiece must explain before it can convince.",
                            "example": "faible : gère seul, sans outil dédié"
                          },
                          "axes_editoriaux_recommandes": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The editorial angles the strategy recommends for this persona.",
                              "example": "visibilité locale"
                            },
                            "example": [
                              "visibilité locale",
                              "réputation"
                            ],
                            "description": "The editorial angles the strategy recommends for this persona."
                          }
                        }
                      },
                      "description": "The audience profiles the strategy built. Richer than `targetPersona`, which is one line of prose."
                    },
                    "editorialPositioning": {
                      "type": "object",
                      "properties": {
                        "uniqueAngle": {
                          "type": "string",
                          "description": "The one thing the project says that its sector does not. If it could\nbe written by a competitor, it is not this field's content.",
                          "example": "On montre les chantiers en cours, pas seulement les finis."
                        },
                        "differentiators": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "What sets the project apart, one per entry. Claims it can back, not\naspirations.",
                            "example": "devis chiffré en 48 h"
                          },
                          "example": [
                            "devis chiffré en 48 h",
                            "photos datées de chaque étape"
                          ],
                          "description": "What sets the project apart, one per entry. Claims it can back, not\naspirations."
                        },
                        "messagingPillars": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "description": "The two or three messages every piece should carry. Repetition is the\npoint: this is what consistency means here.",
                            "example": "la transparence sur les délais"
                          },
                          "example": [
                            "la transparence sur les délais",
                            "le travail visible"
                          ],
                          "description": "The two or three messages every piece should carry. Repetition is the\npoint: this is what consistency means here."
                        },
                        "brandVoice": {
                          "type": "string",
                          "description": "The voice in prose. The structured version — axes, register, address\nform — is served by the brand-voice endpoint.",
                          "example": "Direct, concret, jamais condescendant. On explique sans faire la leçon."
                        }
                      },
                      "description": "Where the project stands editorially against its sector."
                    },
                    "valueProposition": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "The promises the project makes, one per entry.",
                        "example": "une carte qui change chaque semaine"
                      },
                      "example": [
                        "une carte qui change chaque semaine",
                        "tout fait maison"
                      ],
                      "description": "The promises the project makes, one per entry."
                    },
                    "keyMessages": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "The messages to repeat across pieces — what consistency means here.",
                        "example": "la saison décide de la carte"
                      },
                      "example": [
                        "la saison décide de la carte",
                        "le patron est en cuisine"
                      ],
                      "description": "The messages to repeat across pieces — what consistency means here."
                    },
                    "searchIntents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "intentType": {
                            "type": "string",
                            "enum": [
                              "commerciale",
                              "informationnelle",
                              "inspirationnelle",
                              "locale",
                              "navigationnelle",
                              "transactionnelle",
                              "unclassified",
                              "éducationnelle"
                            ],
                            "description": "The intent this entry describes. **French stored values** — the same\nvocabulary as `intent` on a content item.",
                            "example": "informationnelle"
                          },
                          "description": {
                            "type": "string",
                            "description": "What the reader is trying to do when they search this way.",
                            "example": "Comprendre ce qui fait fuir un client avant qu'il appelle."
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The keywords carrying this intent.",
                              "example": "fiche google restaurant incomplète"
                            },
                            "example": [
                              "fiche google restaurant incomplète",
                              "pourquoi personne ne réserve"
                            ],
                            "description": "The keywords carrying this intent."
                          },
                          "contentVolume": {
                            "type": "number",
                            "description": "How many pieces this intent warrants. A recommendation for planning,\nnot a quota anything enforces.",
                            "example": 4
                          },
                          "keywordVolumes": {
                            "type": "object",
                            "description": "Measured search volume per keyword, keyed by the normalised keyword\n(case and spaces ignored). Absent when no volume was looked up — which is\nnot a volume of zero; each entry says which case it is.",
                            "example": {
                              "fiche google restaurant incomplète": {
                                "searchVolume": 320,
                                "searchVolumeStatus": "ok"
                              }
                            }
                          }
                        }
                      },
                      "description": "Search intents the strategy retained, with their French values — see `intent` on a content item."
                    },
                    "contentPillars": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "The pillar's name. It is what a content item's `pillarName`\nmatches against.",
                            "example": "visibilité locale"
                          },
                          "description": {
                            "type": "string",
                            "description": "What territory the pillar covers, in a sentence.",
                            "example": "Tout ce qui décide si un client proche vous trouve et vous appelle."
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "critical",
                              "high",
                              "info",
                              "low",
                              "medium"
                            ],
                            "description": "How much of the effort this pillar deserves, relative to the others.",
                            "example": "high"
                          }
                        }
                      },
                      "description": "The recurring themes the project should own, with the priority the strategy assigned."
                    },
                    "semanticClusters": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "nom_cluster": {
                            "type": "string",
                            "description": "The cluster's name. It is the key readers see, and what\n`coverage.clusterCoverage` reports against.",
                            "example": "visibilité locale"
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "The keywords that belong to this cluster. Two clusters can share one:\nthe grouping is by meaning, not by keyword.",
                              "example": "fiche google restaurant"
                            },
                            "example": [
                              "fiche google restaurant",
                              "avis google restaurant"
                            ],
                            "description": "The keywords that belong to this cluster. Two clusters can share one:\nthe grouping is by meaning, not by keyword."
                          },
                          "description": {
                            "type": "string",
                            "description": "What the cluster covers.",
                            "example": "Tout ce qu'un client voit avant de réserver, et comment le corriger."
                          },
                          "volume_optimal": {
                            "type": "number",
                            "description": "How many pieces the cluster warrants. A recommendation for planning,\nnot a quota anything enforces.",
                            "example": 6
                          },
                          "type_intention": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "commerciale",
                                "informationnelle",
                                "inspirationnelle",
                                "locale",
                                "navigationnelle",
                                "transactionnelle",
                                "unclassified",
                                "éducationnelle"
                              ],
                              "description": "The search intents this cluster answers. **French stored values**, the\nsame vocabulary as `intent` on a content item.",
                              "example": "informationnelle"
                            },
                            "example": [
                              "informationnelle",
                              "commerciale"
                            ],
                            "description": "The search intents this cluster answers. **French stored values**, the\nsame vocabulary as `intent` on a content item."
                          },
                          "personas_cibles": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "description": "Which personas the cluster is for, by first name — the same names as\n`personas[].prenom`.",
                              "example": "Marc"
                            },
                            "example": [
                              "Marc",
                              "Sylvie"
                            ],
                            "description": "Which personas the cluster is for, by first name — the same names as\n`personas[].prenom`."
                          },
                          "priorisation": {
                            "type": "object",
                            "properties": {
                              "phase1": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "What to write first. It is what earns the right to phase 2.",
                                  "example": "Ce que vos clients voient avant de réserver"
                                },
                                "example": [
                                  "Ce que vos clients voient avant de réserver"
                                ],
                                "description": "What to write first. It is what earns the right to phase 2."
                              },
                              "phase2": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "description": "What to write once phase 1 is in place.",
                                  "example": "Répondre aux avis sans y passer la semaine"
                                },
                                "example": [
                                  "Répondre aux avis sans y passer la semaine"
                                ],
                                "description": "What to write once phase 1 is in place."
                              }
                            },
                            "description": "What to write first, and what comes after. Two phases, ordered: phase 1\nis what earns the right to phase 2."
                          },
                          "axes_editoriaux": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "nom": {
                                  "type": "string",
                                  "description": "The editorial angle's name.",
                                  "example": "preuve locale"
                                },
                                "pertinence": {
                                  "type": "string",
                                  "description": "Why this angle fits the cluster, in prose.",
                                  "example": "Les lecteurs comparent d'abord sur la proximité, pas sur le prix."
                                },
                                "role_recommande": {
                                  "type": "string",
                                  "enum": [
                                    "inadapté",
                                    "pilier",
                                    "satellite"
                                  ],
                                  "description": "What the angle should carry: a `pilier` deserves a cornerstone piece,\na `satellite` a supporting one, and `inadapté` says to leave it alone.",
                                  "example": "pilier"
                                },
                                "justification": {
                                  "type": "string",
                                  "description": "Why the angle got that role. It is the sentence to read before\noverriding the recommendation.",
                                  "example": "Deux concurrents la tiennent déjà : sans preuve locale, aucun contenu ne passe devant."
                                }
                              }
                            }
                          },
                          "angle_strategique": {
                            "type": "string",
                            "description": "The angle the whole cluster should take. Each `axes_editoriaux` entry\nrefines it; this one sets the direction.",
                            "example": "Montrer ce que le client voit, avant de parler de ce qu'on fait."
                          },
                          "role_strategique_cluster": {
                            "type": "string",
                            "enum": [
                              "inadapté",
                              "pilier",
                              "satellite"
                            ],
                            "description": "The cluster's role in the whole strategy. `inadapté` is a finding, not\nan error: the cluster exists in the market and the project should not chase\nit.",
                            "example": "pilier"
                          }
                        }
                      },
                      "description": "Topic groupings by meaning, not by keyword — two clusters can share a keyword."
                    },
                    "suggestedTopics": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "description": "Topics the strategy proposes, as titles. They are proposals: nothing\nexists as a content item until one is created from them.",
                        "example": "Ce que vos clients voient avant de réserver"
                      },
                      "example": [
                        "Ce que vos clients voient avant de réserver",
                        "Répondre aux avis sans y passer la semaine"
                      ],
                      "description": "Topics the strategy proposes, as titles. They are proposals: nothing\nexists as a content item until one is created from them."
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-02T14:31:07.000Z"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-24T09:05:11.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Editorial strategy for this project",
        "tags": [
          "Editorial"
        ]
      }
    },
    "/v1/projects/{projectId}/brand-voice": {
      "get": {
        "description": "The project's brand voice: active style, form of address, technical level, language, and per-channel overrides.\n\n`configured` and `enabled` say different things: a voice can be defined and switched off. Reading one for the other would apply a style the project has deliberately disabled.",
        "operationId": "getBrandVoice",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "description": "The project this call is about. **A routing hint, not an authorisation.** The project comes from the credential; this segment exists so multi-project code stays uniform. If it names a project outside the token, the call is refused with `routing_hint_mismatch`.",
            "schema": {
              "format": "uuid",
              "example": "550e8400-e29b-41d4-a716-446655440000",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields listed here are the ones the `api` surface publishes — the intersection of the capability payload and the surface allowlist. Anything the allowlist withholds is absent from the response, not null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "description": "The project these settings belong to. Always present, including when\n`configured` is `false`.",
                      "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
                    },
                    "configured": {
                      "type": "boolean",
                      "description": "`false` means the project has never configured a brand voice — the\nother fields then carry defaults, not choices. Read it before treating\n`enabled: true` as a decision someone made.",
                      "example": true
                    },
                    "enabled": {
                      "type": "boolean",
                      "description": "Whether the brand voice applies to generation. Meaningful only once\n`configured` is `true`; before that it is the default, not a setting.",
                      "example": true
                    },
                    "activeStyleRef": {
                      "type": "string",
                      "description": "Which style is active — `preset:<key>` for a system preset,\n`style:<uuid>` for one the project defined. A string rather than an\nidentifier because the two worlds share this field: presets are not rows.",
                      "example": "preset:pedagogique-clair"
                    },
                    "visualPolicy": {
                      "type": "object",
                      "properties": {
                        "preference": {
                          "type": "string",
                          "enum": [
                            "balanced",
                            "illustration-first",
                            "photo-first"
                          ],
                          "description": "Which kind of visual the project prefers when nothing overrides it.",
                          "example": "balanced"
                        }
                      },
                      "description": "The project's default visual preference for generated images —\n`balanced` until the project sets it. A per-content brief may still\noverride it via `visualPreference`."
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-02T14:31:07.000Z"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-09-18T09:12:44.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The credential was not recognised, was revoked, or expired. The `WWW-Authenticate` header points at the resource metadata needed to re-authorise. Reasons: `credential_invalid`, `credential_revoked`, `credential_expired`, `membership_revoked` — see https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "403": {
            "description": "The call was understood and refused. Distinguish `capability_not_allowed` (outside the consent grant — re-consent) from `insufficient_permission` (the bearer lacks the permission — ask an administrator). See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "404": {
            "description": "No such capability or route. Reason `capability_unknown` carries a `suggestions` array — the cause is almost always a typo. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          },
          "429": {
            "description": "Two distinct mechanisms answer 429, and `reason` says which. `rate_limited` counts requests per minute, per credential and per project, and resets within the minute; `quota_exceeded` counts work performed over the billing period. See https://developers.digitalyser.io/reference/errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailsDto"
                }
              }
            }
          }
        },
        "summary": "Brand voice settings",
        "tags": [
          "Editorial"
        ]
      }
    }
  },
  "info": {
    "title": "Digitalyser.io API",
    "description": "The capability registry, over HTTP. Same catalogue, same permissions and same refusals as the MCP transport — only the shape of a refusal differs, because MCP wraps every error in a 200 JSON-RPC envelope and this surface returns real status codes.",
    "version": "1",
    "contact": {},
    "license": {
      "name": "Proprietary",
      "url": "https://developers.digitalyser.io/docs/compliance"
    }
  },
  "tags": [
    {
      "name": "Capabilities",
      "description": "The capability registry itself: list what this credential may call, invoke a capability by identifier, and apply a proposal. Every other route presents one capability, so permissions, plan gating, output masking and refusals are exactly those of the MCP transport — only the shape of a refusal differs, because MCP wraps every error in a 200 JSON-RPC envelope and this surface returns real status codes. Anything that changes state is reached through this group."
    },
    {
      "name": "Audits",
      "description": "Read the project's audit results — overview, online presence, local visibility, reputation, analytics, AI discoverability, content and competitors. Read-only: each route dispatches one read capability."
    },
    {
      "name": "Editorial",
      "description": "Read the project's editorial material — strategy, brand voice, topics, content items and the publishing calendar. Read-only: changes go through the Capabilities group."
    }
  ],
  "servers": [
    {
      "url": "https://app.digitalyser.io/api",
      "description": "Production — the only public environment. There is no sandbox: call it with a credential scoped to your own project. Read capabilities change nothing, so start there; a `propose` capability may already write when invoked, which its description states."
    }
  ],
  "components": {
    "securitySchemes": {
      "agent-token": {
        "scheme": "bearer",
        "bearerFormat": "dga_…",
        "type": "http",
        "description": "An agent project token. Obtained by OAuth consent for an AI client, or derived per project from an organisation service key for a machine. A scoped API key authenticates the transport but opens no capability: a capability takes its project from the credential."
      }
    },
    "schemas": {
      "CapabilityValueResultDto": {
        "type": "object",
        "properties": {
          "capabilityId": {
            "type": "string",
            "description": "The capability that produced this result.",
            "example": "audit.overview.get"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project the call ran against — the credential's, never one named in the call.",
            "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
          },
          "resultType": {
            "type": "string",
            "enum": [
              "value"
            ],
            "description": "The interaction is over: `data` is the result.",
            "example": "value"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The capability's result. **Its shape depends on the capability** — it is deliberately not fixed here. For a capability that also has a dedicated read endpoint, `data` is that endpoint's body.",
            "example": {
              "schemaVersion": "2"
            }
          }
        },
        "required": [
          "capabilityId",
          "projectId",
          "resultType",
          "data"
        ]
      },
      "CapabilityInputRequiredResultDto": {
        "type": "object",
        "properties": {
          "capabilityId": {
            "type": "string",
            "description": "The capability that produced this result.",
            "example": "audit.overview.get"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project the call ran against — the credential's, never one named in the call.",
            "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
          },
          "resultType": {
            "type": "string",
            "enum": [
              "input_required"
            ],
            "description": "The capability needs more input: a questionnaire to fill and send back.",
            "example": "input_required"
          },
          "schema": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSON Schema of the questionnaire. Fill it and send the answers back with `requestState`.",
            "example": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "enum": [
                    "keep",
                    "exclude"
                  ]
                }
              }
            }
          },
          "requestState": {
            "type": "string",
            "description": "Opaque handle that resumes the interaction. **Single use**: replaying it is refused with `invalid_handle`. Pass it as `requestState` in the next call, nothing else.",
            "example": "rs_9c2f7a413e584d0b8b6a1f5d2c9e4a37"
          }
        },
        "required": [
          "capabilityId",
          "projectId",
          "resultType",
          "schema",
          "requestState"
        ]
      },
      "CapabilityDiffDto": {
        "type": "object",
        "properties": {
          "target": {
            "type": "string",
            "description": "What applying will touch — an existing identifier, or `<kind>:new` for a creation.",
            "example": "brand-voice-style:new"
          },
          "fields": {
            "description": "**The only fields applying may change.** Anything else in `after` is refused as `diff_forbidden`.",
            "example": [
              "name",
              "tone"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "before": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true,
            "description": "The current values of those fields. `null` for a creation, where there is nothing before.",
            "example": null
          },
          "after": {
            "type": "object",
            "additionalProperties": true,
            "description": "The values applying would write. Computed by the server, never taken from the caller.",
            "example": {
              "name": "Direct and concrete",
              "tone": "plain"
            }
          },
          "provenance": {
            "type": "object",
            "additionalProperties": true,
            "description": "Where each field came from, for capabilities that ingest external content. Absent for the others."
          }
        },
        "required": [
          "target",
          "fields",
          "before",
          "after"
        ]
      },
      "CapabilityProposalResultDto": {
        "type": "object",
        "properties": {
          "capabilityId": {
            "type": "string",
            "description": "The capability that produced this result.",
            "example": "audit.overview.get"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project the call ran against — the credential's, never one named in the call.",
            "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
          },
          "resultType": {
            "type": "string",
            "enum": [
              "proposal"
            ],
            "description": "A computed diff. **Nothing has been written**; `applyProposal` decides.",
            "example": "proposal"
          },
          "proposalId": {
            "type": "string",
            "format": "uuid",
            "description": "What to pass to `applyProposal`. A proposal belongs to the principal that created it and is applied once.",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "diff": {
            "description": "The change, as the server computed it and masked it for this plan.",
            "allOf": [
              {
                "$ref": "#/components/schemas/CapabilityDiffDto"
              }
            ]
          },
          "applyCapabilityId": {
            "type": "string",
            "description": "Identifier of the capability that applies this proposal. Applying goes through `applyProposal`.",
            "example": "brandvoice.style.apply"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the proposal lapses. They are short-lived on purpose: past this, applying is refused as `expired`.",
            "example": "2026-10-03T14:30:00.000Z"
          },
          "originVerified": {
            "type": "boolean",
            "description": "Whether a human demonstrably originated the request. **`false` over MCP**, where that is not verifiable — treat it as a reason to confirm with a person, not as proof of intent.",
            "example": false
          }
        },
        "required": [
          "capabilityId",
          "projectId",
          "resultType",
          "proposalId",
          "diff",
          "applyCapabilityId",
          "expiresAt",
          "originVerified"
        ]
      },
      "CapabilityEntryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable, readable identifier — what `invokeCapability` takes as `capabilityId`. It is a dotted name, not a UUID.",
            "example": "audit.overview.get"
          },
          "description": {
            "type": "string",
            "description": "What the capability does and when to call it, written for a person or a model.",
            "example": "Reads the cross-module audit overview of the project."
          },
          "effect": {
            "type": "string",
            "enum": [
              "read",
              "propose",
              "write"
            ],
            "description": "`read` changes nothing. `propose` computes a diff that `applyProposal` decides. `write` is not invoked directly — it is reached through the proposal it belongs to. Read `writesOnInvoke` before assuming `propose` is safe to call.",
            "example": "read"
          },
          "requiresProject": {
            "type": "boolean",
            "description": "Whether the capability needs a project. `true` for nearly all of them, and the project comes from the credential, never from the call.",
            "example": true
          },
          "writesOnInvoke": {
            "type": "boolean",
            "description": "**Whether invoking it already does the work and consumes quota.** Always present, never guessed: an absent flag would let a client conclude \"probably safe\". When `true` there is no dry run — ask before calling, not after.",
            "example": false
          },
          "paramsSchema": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSON Schema of the `params` this capability accepts. An empty `properties` means it takes no parameters — and nothing else: it does not mean \"undocumented\".",
            "example": {
              "type": "object",
              "properties": {},
              "additionalProperties": false
            }
          }
        },
        "required": [
          "id",
          "description",
          "effect",
          "requiresProject",
          "writesOnInvoke",
          "paramsSchema"
        ]
      },
      "CapabilityUnavailableEntryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of a capability this project has but this call site cannot invoke.",
            "example": "brandvoice.style.propose"
          },
          "description": {
            "type": "string",
            "description": "What it does.",
            "example": "Proposes a new brand-voice style."
          }
        },
        "required": [
          "id",
          "description"
        ]
      },
      "CapabilityUnavailableDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "session_required"
            ],
            "description": "Why they are not listed here. One cause today: outside a session there is no thread to confirm a diff in.",
            "example": "session_required"
          },
          "message": {
            "type": "string",
            "description": "A sentence written to be repeated as it stands. **It does not make these capabilities invocable**: `invokeCapability` still refuses them.",
            "example": "These capabilities exist on this project but need a session to confirm a change."
          },
          "capabilities": {
            "description": "The capabilities concerned.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CapabilityUnavailableEntryDto"
            }
          }
        },
        "required": [
          "reason",
          "message",
          "capabilities"
        ]
      },
      "CapabilityListingDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The project the credential carries. `null` for a credential that carries none.",
            "example": "8f14e45f-ea0e-4b1e-9b7a-2c3d4e5f6071"
          },
          "capabilities": {
            "description": "The catalogue **as this credential sees it**. It is computed per call from the plan, the permissions of the bearer, the consent allowlist and the project — two credentials on the same project can see different lists.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CapabilityEntryDto"
            }
          },
          "unavailableHere": {
            "description": "What the project can do but this call site cannot invoke. **Absent when there is nothing to say.** Without it, a catalogue with no write capability reads as a product limit, which it is not.",
            "allOf": [
              {
                "$ref": "#/components/schemas/CapabilityUnavailableDto"
              }
            ]
          }
        },
        "required": [
          "projectId",
          "capabilities"
        ]
      },
      "ProblemDetailsDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "URI identifying the problem type. Dereferenceable: it points at the section of the error reference that documents this exact reason. Stable — safe to branch on.",
            "example": "https://developers.digitalyser.io/reference/errors#insufficient_permission"
          },
          "title": {
            "type": "string",
            "description": "Error class. Coarser than `reason`: several reasons share one title. Do not branch on it.",
            "example": "agent_authorization_denied"
          },
          "status": {
            "type": "number",
            "description": "The HTTP status code, repeated in the body.",
            "example": 403
          },
          "reason": {
            "type": "string",
            "description": "The stable identifier of the refusal — the same string that follows the `#` in `type`. **This is the field to branch on.** Every value is documented in the error reference.",
            "example": "insufficient_permission"
          },
          "suggestions": {
            "description": "Closest known capability identifiers. Present only on `capability_unknown`, where the cause is almost always a typo.",
            "example": [
              "audit.overview.get",
              "audit.content.get"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "reason"
        ]
      },
      "InvokeCapabilityDto": {
        "type": "object",
        "properties": {
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "Parameters for the capability, matching the JSON Schema that `GET /v1/capabilities` publishes for it under `paramsSchema`. The shape therefore differs per capability — read it there rather than guessing.\n\n**Omitting a required parameter is not always an error.** Some capabilities answer with a questionnaire instead, so a person can fill it in; a client that treats that as a failure will loop. A project or organisation key is refused at any depth: the project comes from the credential, never from the body.",
            "example": {
              "action": "exclude",
              "competitorEntryIds": [
                "550e8400-e29b-41d4-a716-446655440000"
              ]
            }
          },
          "requestState": {
            "type": "string",
            "description": "The `requestState` handle returned by a previous turn of the same interaction — how you answer a questionnaire. **Single use**: replaying it is refused with `invalid_handle`, and a fresh invocation without it starts over.",
            "example": "rs_7f3c1a2e"
          }
        }
      }
    }
  },
  "security": [
    {
      "agent-token": []
    }
  ]
}
