> ## Documentation Index
> Fetch the complete documentation index at: https://gotrebol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Coordenadas de citas (Citations Artifact)

> Cómo activar el artifact de coordenadas, qué estructura tiene y cómo integrarlo para mostrar de dónde se extrajo cada dato.

El **citations artifact** es un JSON que mapea cada field del response de una sección a sus coordenadas exactas (`bounding_boxes`) dentro del PDF original. Lo usas para construir UIs auditables: al mostrar un dato extraído, resaltas la región del documento de donde Trébol lo extrajo. Trébol genera el artifact cuando termina la verificación y te entrega una URL firmada para descargarlo.

## Endpoints con soporte de citations

| Endpoint                                  | Sección/Tipo     | Dónde aparece `citations`                   | Paths de ejemplo                                                                                                                     |
| :---------------------------------------- | :--------------- | :------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v2/verifications/{id}/people`       | `people`         | `data.citations.url` (único artifact)       | `key_people[0].roles[0].role_name`, `signatory_groups.executives[0].names`                                                           |
| `GET /v2/companies/{tag}/people`          | `people`         | `data.citations.url` (único artifact)       | ídem                                                                                                                                 |
| `GET /v2/verifications/{id}/shareholders` | `shareholders`   | `data.citations.url` (único artifact)       | `shareholders[0].name`, `shareholders[0].id_number`, `shareholders[0].fixed_value`                                                   |
| `GET /v2/companies/{tag}/shareholders`    | `shareholders`   | `data.citations.url` (único artifact)       | ídem                                                                                                                                 |
| `GET /v2/verifications/{id}/sources`      | `sources` (acta) | `data.sources[i].citations.url` (por item)  | `notary_info.notaryName`, `corporate_purpose.object`, `assembly_minute.resolutions`, `shareholders[0].name`, `board_members[0].name` |
| `GET /v2/companies/{tag}/sources`         | `sources` (acta) | `data.sources[i].citations.url` (por item)  | ídem                                                                                                                                 |
| `GET /verifications/{id}`                 | items de acta    | `items[i].citations.url` (por item de acta) | ídem                                                                                                                                 |
| `GET /verification-items/{id}`            | item de acta     | `citations.url` (en el objeto raíz)         | ídem                                                                                                                                 |

## Cómo activarlo

Agrega el query parameter `?with_citations=true` al endpoint de la sección. El response añade un campo `data.citations` con la URL firmada al JSON. Tienes dos rutas equivalentes: por `verification_id` o por `etiqueta`.

<Tabs>
  <Tab title="people">
    <CodeGroup>
      ```bash Por verification_id theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/verifications/{verification_id}/people?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```bash Por etiqueta theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/companies/{etiqueta}/people?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```javascript JavaScript theme={"dark"}
      const res = await fetch(
        `https://api.gotrebol.com/v2/verifications/${verificationId}/people?with_citations=true`,
        { headers: { "x-api-key": apiKey } }
      );
      const { data } = await res.json();
      const artifact = await (await fetch(data.citations.url)).json();
      ```

      ```python Python theme={"dark"}
      import requests

      res = requests.get(
          f"https://api.gotrebol.com/v2/verifications/{verification_id}/people",
          params={"with_citations": "true"},
          headers={"x-api-key": api_key},
      )
      citations_url = res.json()["data"]["citations"]["url"]
      artifact = requests.get(citations_url).json()
      ```
    </CodeGroup>
  </Tab>

  <Tab title="shareholders">
    <CodeGroup>
      ```bash Por verification_id theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/verifications/{verification_id}/shareholders?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```bash Por etiqueta theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/companies/{etiqueta}/shareholders?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```javascript JavaScript theme={"dark"}
      const res = await fetch(
        `https://api.gotrebol.com/v2/verifications/${verificationId}/shareholders?with_citations=true`,
        { headers: { "x-api-key": apiKey } }
      );
      const { data } = await res.json();
      const artifact = await (await fetch(data.citations.url)).json();
      ```

      ```python Python theme={"dark"}
      import requests

      res = requests.get(
          f"https://api.gotrebol.com/v2/verifications/{verification_id}/shareholders",
          params={"with_citations": "true"},
          headers={"x-api-key": api_key},
      )
      citations_url = res.json()["data"]["citations"]["url"]
      artifact = requests.get(citations_url).json()
      ```
    </CodeGroup>
  </Tab>

  <Tab title="sources (acta)">
    La sección `sources` devuelve un **array** de items. Con `?with_citations=true`, cada item de tipo acta incluye su propio objeto `citations` con la URL del artifact de ese documento.

    <CodeGroup>
      ```bash Por verification_id theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/verifications/{verification_id}/sources?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```bash Por etiqueta theme={"dark"}
      curl -X GET "https://api.gotrebol.com/v2/companies/{etiqueta}/sources?with_citations=true" \
           -H "x-api-key: {api_key}"
      ```

      ```javascript JavaScript theme={"dark"}
      const res = await fetch(
        `https://api.gotrebol.com/v2/verifications/${verificationId}/sources?with_citations=true`,
        { headers: { "x-api-key": apiKey } }
      );
      const { data } = await res.json();
      // Cada item del array tiene su propio data.sources[i].citations
      const actaSources = data.sources.filter(s => s.citations);
      const artifact = await (await fetch(actaSources[0].citations.url)).json();
      ```

      ```python Python theme={"dark"}
      import requests

      res = requests.get(
          f"https://api.gotrebol.com/v2/verifications/{verification_id}/sources",
          params={"with_citations": "true"},
          headers={"x-api-key": api_key},
      )
      sources = res.json()["data"]["sources"]
      acta_sources = [s for s in sources if s.get("citations")]
      artifact = requests.get(acta_sources[0]["citations"]["url"]).json()
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Tip>
  La URL firmada caduca **1 hora** después de generarla. Descarga el JSON y persiste su contenido en tu lado. Si la URL caduca, vuelve a llamar al endpoint con el flag para obtener una nueva.
</Tip>

<Warning>
  Trébol genera el artifact con el evento `verification.v2.finished`. Los requests anteriores a ese evento no traen la url de `citations`.
</Warning>

## Estructura del artifact

El artifact es un JSON con un schema versionado. Hoy la versión activa es `citations/v1`.

### Top level

| Campo             | Tipo                | Descripción                                                                                                                                              |
| :---------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema_version`  | `string`            | Versión del schema. Actualmente `"citations/v1"`.                                                                                                        |
| `verification_id` | `string`            | ID de la verificación a la que pertenece el artifact.                                                                                                    |
| `section`         | `string`            | Sección citada: `"people"`, `"shareholders"` o `"acta"`. Los artifacts de `sources` tienen `section: "acta"` y además incluyen `item_id`.                |
| `fields`          | `Field[]`           | Lista plana de fields citados con sus coordenadas.                                                                                                       |
| `unresolved`      | `UnresolvedField[]` | Fields que Trébol extrajo pero no logró mapear a coordenadas. Vacío en el caso normal. Cada objeto trae `path` y `value`, pero **sin** `bounding_boxes`. |
| `item_id`         | `number`            | *(Solo artifacts de `sources`)* ID del item al que pertenece el artifact. Corresponde al `id` del source en el response de la sección.                   |

### Objeto `Field`

| Campo            | Tipo                          | Descripción                                                                                                                                                                                                                                                |
| :--------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`           | `string`                      | Ruta del field en el response de la sección, en notación de punto + índices (ej. `key_people[3].roles[0].powers[0].power_name`, `shareholders[0].id_number`). El índice refleja la posición en el array del endpoint — misma posición que el UI renderiza. |
| `value`          | `string \| number \| boolean` | Valor extraído. Coincide con el valor en el response de `/people`.                                                                                                                                                                                         |
| `source`         | `Source`                      | Metadata del documento fuente: `item_id`, `item_type`, `document_date`, `document_number`.                                                                                                                                                                 |
| `bounding_boxes` | `BoundingBox[]`               | Una o más coordenadas. Un mismo field puede tener varios bboxes si la información aparece en distintas partes del documento.                                                                                                                               |

### Objeto `BoundingBox`

| Campo      | Tipo                 | Descripción                                                                                                                                         |
| :--------- | :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page`     | `number`             | Página del PDF (1-indexed).                                                                                                                         |
| `vertices` | `[number, number][]` | 4 puntos `[x, y]` con coordenadas **normalizadas en \[0, 1]** respecto al tamaño de la página renderizada. Origen `top-left` (y crece hacia abajo). |
| `text`     | `string`             | Snippet de texto OCR del bbox. Útil para mostrar contexto sin renderizar el PDF.                                                                    |

<Warning>
  Los 4 vértices **no siempre forman un rectángulo aligned con los ejes**. Documentos con texto rotado (sellos, firmas) producen polígonos inclinados. Renderiza los 4 vértices como un polígono, no como un rect axis-aligned.
</Warning>

### Ejemplos

<Tabs>
  <Tab title="people">
    ```json theme={"dark"}
    {
      "schema_version": "citations/v1",
      "verification_id": "79aa3663-ca3f-4aaa-a354-ae4630b96f3b",
      "section": "people",
      "fields": [
        {
          "path": "key_people[3].roles[0].duration",
          "value": -1,
          "source": {
            "item_id": 37257,
            "item_type": "ac_mx",
            "document_date": "2015-08-25",
            "document_number": "10223"
          },
          "bounding_boxes": [
            {
              "page": 6,
              "vertices": [
                [0.1193, 0.1484],
                [0.8623, 0.1554],
                [0.8594, 0.2666],
                [0.1164, 0.2596]
              ],
              "text": "--- VIGÉSIMA SEGUNDA .- La administración de la sociedad estará a cargo de un administrador único..."
            }
          ]
        }
      ],
      "unresolved": []
    }
    ```
  </Tab>

  <Tab title="shareholders">
    ```json theme={"dark"}
    {
      "schema_version": "citations/v1",
      "verification_id": "79aa3663-ca3f-4aaa-a354-ae4630b96f3b",
      "section": "shareholders",
      "fields": [
        {
          "path": "shareholders[0].name",
          "value": "CARLOS SLIM HELÚ",
          "source": {
            "item_id": 37257,
            "item_type": "ac_mx",
            "document_date": "2015-08-25",
            "document_number": "10223"
          },
          "bounding_boxes": [
            {
              "page": 3,
              "vertices": [
                [0.1193, 0.3210],
                [0.6540, 0.3210],
                [0.6540, 0.3480],
                [0.1193, 0.3480]
              ],
              "text": "CARLOS SLIM HELÚ, titular de 40 acciones..."
            }
          ]
        },
        {
          "path": "shareholders[0].fixed_value",
          "value": "40000",
          "source": {
            "item_id": 37257,
            "item_type": "ac_mx",
            "document_date": "2015-08-25",
            "document_number": "10223"
          },
          "bounding_boxes": [
            {
              "page": 3,
              "vertices": [
                [0.5100, 0.3210],
                [0.7200, 0.3210],
                [0.7200, 0.3480],
                [0.5100, 0.3480]
              ],
              "text": "$40,000.00"
            }
          ]
        }
      ],
      "unresolved": []
    }
    ```
  </Tab>

  <Tab title="sources (acta)">
    ```json theme={"dark"}
    {
      "schema_version": "citations/v1",
      "verification_id": "79aa3663-ca3f-4aaa-a354-ae4630b96f3b",
      "section": "acta",
      "item_id": 37257,
      "fields": [
        {
          "path": "notary_info.notaryName",
          "value": "ROBERTO MONTES HERRERA",
          "source": {
            "item_id": 37257,
            "item_type": "ac_mx",
            "document_date": "2015-08-25",
            "document_number": "10223"
          },
          "bounding_boxes": [
            {
              "page": 1,
              "vertices": [
                [0.2010, 0.1230],
                [0.7540, 0.1230],
                [0.7540, 0.1450],
                [0.2010, 0.1450]
              ],
              "text": "Ante mí, ROBERTO MONTES HERRERA, Notario Público número 45..."
            }
          ]
        },
        {
          "path": "board_members[0].name",
          "value": "ANA TORRES VIDAL",
          "source": {
            "item_id": 37257,
            "item_type": "ac_mx",
            "document_date": "2015-08-25",
            "document_number": "10223"
          },
          "bounding_boxes": [
            {
              "page": 8,
              "vertices": [
                [0.1500, 0.5100],
                [0.6200, 0.5100],
                [0.6200, 0.5320],
                [0.1500, 0.5320]
              ],
              "text": "ANA TORRES VIDAL, designada como Directora General..."
            }
          ]
        }
      ],
      "unresolved": []
    }
    ```
  </Tab>
</Tabs>

El campo `value` refleja el valor extraído originalmente del documento. Para `people`, algunos fields usan valores centinela: `duration` se expresa en años y `-1` significa cargo indefinido.

### Cómo manejar `unresolved`

Cuando Trébol extrae un dato pero no logra ubicarlo en el PDF (texto re-flujado, tablas complejas, OCR de baja confianza), el field cae en `unresolved` en vez de `fields`. Estos objetos traen `path` y `value`, pero no `bounding_boxes`.

En tu UI, muestra el valor normalmente y omite el botón de "ver cita" para esos fields. No los escondas: el dato sigue siendo válido, solo no tiene coordenadas que resaltar.

## Cómo implementarlo en tu UI

### Obtener el PDF original

El artifact te da las coordenadas, pero no el PDF. Cada `field` trae `source.item_id`: ese es el documento del que salió el dato. Para resaltar la cita necesitas el PDF de ese mismo `item_id`.

La URL del PDF vive en el response de la sección, no en el artifact. Para `people`, cada objeto `source` dentro del response incluye `item_id` y `document_url`. Para `shareholders`, la URL del PDF del acta está en `data.source.document_url` (un único documento fuente para todos los accionistas). Para `sources`, cada item del array expone `document_url` directamente — ese es el PDF del acta. El artifact de ese item trae `item_id` en el nivel raíz: úsalo para identificar el item correcto cuando hay múltiples actas; no cruces por `field.source.item_id` dentro del artifact (todos los fields de un artifact de acta pertenecen al mismo documento).

```javascript theme={"dark"}
// Para people: recorre el response y arma item_id -> document_url
function buildPdfUrlMap(sectionData) {
  const map = new Map();
  JSON.stringify(sectionData, (key, val) => {
    if (val && typeof val === "object" && val.item_id && val.document_url) {
      map.set(val.item_id, val.document_url);
    }
    return val;
  });
  return map;
}
const peoplePdfMap = buildPdfUrlMap(peopleData);
const peoplePdfUrl = peoplePdfMap.get(field.source.item_id);

// Para shareholders: el PDF del acta está directamente en data.source
const shareholdersPdfUrl = shareholdersData.source?.document_url ?? null;

// Para sources: cruza por artifact.item_id (nivel raíz del artifact) para
// obtener el item correcto; su document_url es el PDF a renderizar.
const sourceItem = sourcesData.sources.find(s => s.id === artifact.item_id);
const sourcesPdfUrl = sourceItem?.document_url ?? null;
```

<Note>
  `document_url` también es una URL firmada que caduca en 1 hora. Resuélvela en el momento de renderizar, no la persistas. Si está vacía o `null`, ese `item_id` no tiene PDF disponible para citar.
</Note>

### Receta de renderizado

La receta que recomendamos: usa el componente `PDFViewer` de [`pdfjs-dist`](https://github.com/mozilla/pdf.js) (no `pdf.js` core directo) y monta un `<canvas>` overlay como hijo del `div` que el viewer crea para cada página. Dibuja el polígono con `moveTo`/`lineTo` sobre los 4 vértices reales. Esto te da text layer, scroll, virtualización y zoom sin escribir código.

### Arquitectura mínima

<Steps>
  <Step title="Cargar pdfjs-dist y setear el worker">
    Importa `pdfjs-dist` y asigna `GlobalWorkerOptions.workerSrc` para que el worker quede disponible.
  </Step>

  <Step title="Instanciar PDFViewer">
    Crea un `EventBus` y un `PDFViewer` apuntando a un contenedor con `position: absolute` (requerido por el viewer).
  </Step>

  <Step title="Cargar el PDF original">
    Usa `pdfjs.getDocument(PDF_URL).promise` y pásalo a `viewer.setDocument()`.
  </Step>

  <Step title="Al seleccionar un field, dibuja el bbox">
    Toma el `bbox`, multiplica cada vértice por `pageView.viewport.width/height`, dibuja el polígono en un `<canvas>` agregado como hijo de `pageView.div` y scrollea con `viewer.scrollPageIntoView()`.
  </Step>

  <Step title="Redibuja en zoom">
    Suscríbete a `eventBus.on("scalechanging", ...)`. El canvas está en píxeles fijos: sin esto, el highlight se desfasa al cambiar el zoom.
  </Step>
</Steps>

### Ejemplo funcional

Copia este snippet a un archivo local, reemplaza `PDF_URL` y `ARTIFACT_URL` por valores reales, y ábrelo en el navegador.

<CodeGroup>
  ```html index.html theme={"dark"}
  <!doctype html>
  <html>
    <head>
      <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.7.76/web/pdf_viewer.css"
      />
      <style>
        #viewerContainer {
          position: absolute;
          top: 60px;
          left: 0;
          width: 70%;
          height: 80%;
          overflow: auto;
        }
      </style>
    </head>
    <body>
      <button id="cite">Mostrar cita</button>
      <div id="viewerContainer"><div id="viewer" class="pdfViewer"></div></div>
      <script type="module" src="./viewer.js"></script>
    </body>
  </html>
  ```

  ```javascript viewer.js theme={"dark"}
  import * as pdfjs from "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.7.76/build/pdf.mjs";
  import * as pdfjsViewer from "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.7.76/web/pdf_viewer.mjs";

  pdfjs.GlobalWorkerOptions.workerSrc =
    "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.7.76/build/pdf.worker.mjs";

  // PDF_URL: el document_url del item_id que vas a citar (ver "Obtener el PDF original")
  const PDF_URL = "<document_url del response de /people>";
  const ARTIFACT_URL = "<URL firmada que devolvió /people?with_citations=true>";

  const container = document.getElementById("viewerContainer");
  const eventBus = new pdfjsViewer.EventBus();
  const viewer = new pdfjsViewer.PDFViewer({ container, eventBus });
  eventBus.on("pagesinit", () => (viewer.currentScaleValue = "1.0"));

  const [pdf, artifact] = await Promise.all([
    pdfjs.getDocument(PDF_URL).promise,
    fetch(ARTIFACT_URL).then((r) => r.json()),
  ]);
  viewer.setDocument(pdf);

  let currentBbox = null;
  let overlayEl = null;

  function drawBoundingBox(bbox) {
    if (overlayEl) overlayEl.remove();
    const pageView = viewer.getPageView(bbox.page - 1);
    if (!pageView) return;

    const canvas = document.createElement("canvas");
    canvas.width = Math.ceil(pageView.viewport.width);
    canvas.height = Math.ceil(pageView.viewport.height);
    Object.assign(canvas.style, {
      position: "absolute",
      top: "0",
      left: "0",
      pointerEvents: "none",
    });

    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "rgba(255, 200, 0, 0.35)";
    ctx.strokeStyle = "rgba(220, 130, 0, 0.9)";
    ctx.lineWidth = 2;
    ctx.beginPath();
    bbox.vertices.forEach(([nx, ny], i) => {
      const px = nx * canvas.width;
      const py = ny * canvas.height;
      if (i === 0) ctx.moveTo(px, py);
      else ctx.lineTo(px, py);
    });
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    pageView.div.appendChild(canvas);
    overlayEl = canvas;
    currentBbox = bbox;
    viewer.scrollPageIntoView({ pageNumber: bbox.page });
  }

  eventBus.on("scalechanging", () => {
    if (currentBbox) setTimeout(() => drawBoundingBox(currentBbox), 0);
  });

  document.getElementById("cite").onclick = () => {
    const field = artifact.fields.find(
      (f) => f.path === "key_people[3].roles[0].duration"
    );
    if (field) drawBoundingBox(field.bounding_boxes[0]);
  };
  ```
</CodeGroup>

### Detalles importantes de integración

* **Worker de pdf.js**: con bundlers modernos usa `new URL("pdfjs-dist/build/pdf.worker.mjs", import.meta.url).toString()`. Sin bundler, sirve `pdf.worker.mjs` desde tu propio host o desde un CDN.
* **CSS del viewer**: importa `pdfjs-dist/web/pdf_viewer.css` o las páginas se ven sin estilo.
* **Coordenadas normalizadas**: multiplica `nx * viewport.width` y `ny * viewport.height`. **No** uses `viewport.convertToViewportPoint`: asume coordenadas en espacio PDF (bottom-left) y aquí las coordenadas son top-left normalizadas.
* **`bbox.page` es 1-indexed**, pero `viewer.getPageView()` espera 0-indexed. Resta 1.
* **Zoom**: el overlay vive en píxeles fijos. Sin escuchar `scalechanging` y redibujar, el highlight se desfasa.
* **Polígonos rotados**: dibuja siempre como polígono (`moveTo` + `lineTo` + `closePath`), no como `rect` axis-aligned.
* **Páginas virtualizadas**: `PDFViewer` no renderiza todas las páginas hasta que entran al viewport. Si `getPageView()` devuelve `undefined`, llama primero `scrollPageIntoView` y espera al evento `pagerendered`.
* **CORS**: el bucket del PDF y el del artifact deben permitir el origen de tu app. Si tu backend hace fetch del artifact y lo sirve a tu front, este punto desaparece.

## Siguientes pasos

<CardGroup cols={3}>
  <Card title="Leer una verificación" href="/docs/guia-devs/monitorear/leer">
    Cómo consumir el endpoint `/people` y otras secciones de la verificación.
  </Card>

  <Card title="Webhooks" href="/docs/guia-devs/webhooks">
    Suscríbete a `verification.v2.finished` para saber cuándo el artifact está listo.
  </Card>

  <Card title="Tipos de item" href="/docs/guia-devs/referencia/tipos-item">
    Qué items alimentan la sección `people` y, por tanto, generan citas.
  </Card>
</CardGroup>
