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

# Qscore — API-first credit bureau

> Buy full credit reports with score for people and companies (Chile first), download the PDF, verify it publicly, manage ARCO disputes, and monitor subjects with alerts.

export const EnvUrls = ({lang = "en"}) => {
  const T = {
    en: {
      test: "Test",
      live: "Live",
      hint: "Same API on both environments — build against test first, then go live by swapping the base URL and the key.",
      guide: "Environments and testing",
      href: "/en/environment-testing"
    },
    es: {
      test: "Test",
      live: "Live",
      hint: "La misma API en ambos ambientes — construye primero contra test y pasa a live cambiando la URL base y la key.",
      guide: "Entorno y pruebas",
      href: "/es/environment-testing"
    },
    zh: {
      test: "Test",
      live: "Live",
      hint: "两个环境的 API 完全一致——先在 test 环境构建，再通过切换基础 URL 和密钥上线。",
      guide: "环境与测试",
      href: "/zh/environment-testing"
    }
  };
  const t = T[lang] || T.en;
  const row = (label, url, keyPattern, badgeCls) => <div className="flex flex-wrap items-center gap-2 px-3 py-2">
      <span className={"rounded px-1.5 py-0.5 text-xs font-semibold uppercase tracking-wide " + badgeCls}>
        {label}
      </span>
      <code className="text-xs">{url}</code>
      <span className="text-xs text-gray-400 dark:text-zinc-500">·</span>
      <code className="text-xs">{keyPattern}</code>
    </div>;
  return <div className="my-4 rounded-xl border border-gray-200 dark:border-zinc-700 divide-y divide-gray-200 dark:divide-zinc-700 text-sm not-prose">
      {row(t.test, "https://cryptobank.qbank.cl/platform", "pk_test_...", "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300")}
      {row(t.live, "https://api.qbank.cl/platform", "pk_...", "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300")}
      <p className="px-3 py-2 text-xs text-gray-500 dark:text-zinc-400 m-0">
        {t.hint} <a href={t.href}>{t.guide} →</a>
      </p>
    </div>;
};

<EnvUrls lang="en" />

Qscore is the API-first credit bureau of the platform. One call returns a **complete credit report** of a person or company — identity, tradelines, delinquencies, bankruptcies, commercial activity, alternative data — plus a **credit score (1–999) with its band and explainable reason codes**, rendered as a branded PDF and exposed as JSON.

* **Chile first, country-agnostic design**: today subjects are Chilean (`country: "CL"`, RUT as `doc_id`); new countries plug in without contract changes.
* **Live freshness**: every report queries the data sources at purchase time and declares, per source, whether the data is `live`, `cached` or `unavailable`. No silent stale data.
* **Compliance built in**: the declared `purpose` is mandatory (Chilean data protection law), every score carries its reason codes, and every report includes a public verification code.

<Note>
  Qscore is a paid product gated by the `risk` service flag of your account and billed per report (`risk_report_person` / `risk_report_company` standalone fees). If the generation fails after the charge, the fee is **refunded automatically** and the report ends `failed` with its `error_code`. The exception is **your own report**: a verified account holder generates their self report for free — see "Your own report (self)" below.
</Note>

<Note>Qscore also includes the separate [fraud and identity report](/en/guides/qscore-fraud) and the public [Qscore Index](/en/guides/qscore-index).</Note>

## How it works

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    autonumber
    participant C as Your system
    participant P as CBPay platform
    participant Q as Qbank core
    participant S as Data sources
    C->>P: POST /v1/qscore/reports (doc_id, purpose, idempotency_key)
    P->>Q: POST /v1/bureau/fetch (live fetch)
    Q->>S: Query official sources
    S-->>Q: Bureau records (deduplicated)
    Q-->>P: Records
    P->>P: Compute score v1 + build + render PDF
    P-->>C: 201 report ready (score, band, full report JSON)
    P-->>C: Webhook risk_report_ready
    C->>P: GET /v1/qscore/reports/{report_id}/pdf
```

Generation is **synchronous**: the `POST` fetches the bureau records, computes the score, renders the PDF and returns the ready report in a single response. A source that is down does **not** fail a paid report — it is generated with the persisted data and the source is declared `cached` (or `unavailable` if it contributed nothing) in the `sources` section.

## Your own report (self)

If you hold a **verified account** (approved KYC/KYB), you can generate and download **your own Qscore report** directly. This is your right of access to your personal data (ARCO / Chilean Law 21.719), not a purchase:

* **Free**: no fee is charged, ever.
* **No score penalty**: self reports are excluded from the inquiry count of your score — checking your own report never hurts it.
* **Anti-oracle by design**: the subject identity comes from the `tax_id` verified in your KYC/KYB. The request does **not** accept a `doc_id` — asking for someone else's report through these endpoints is impossible.
* **Frequency limit**: one **new** report every 30 days. If you already have a `ready` report within the window, the `POST` returns it with `idempotency_hit: true` (HTTP 200) instead of generating another.

### Generate (or reuse) your report

`POST /v1/qscore/my-report` — the body is optional: `{"lang": "en"|"es"|"zh"}` (default `en`). Generation is **synchronous**: the response carries the finished report. No `idempotency_key` is needed — idempotency is deterministic per account, subject and day (a double submit on the same day returns the already-created report).

```bash Generate your own report theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.qbank.cl/platform/v1/qscore/my-report" \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"lang": "es"}'
```

```json 201 Created (new report generated) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "report_id": "9f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "ready",
  "purpose": "self_access",
  "lang": "es",
  "band": "B",
  "model_version": "qscore-v2",
  "verify_code": "Q9f1c2d3e4a5b4c6d8e7f0a1b2c3d4e5f1a2b3c4d5e6f",
  "created_at": "2026-08-08T15:04:12Z",
  "score": 742,
  "reason_codes": ["RC01", "RC07"],
  "completed_at": "2026-08-08T15:04:19Z",
  "report": { "...": "full report JSON" }
}
```

A second call within 30 days answers `200 OK` with the same report and `"idempotency_hit": true`. If the generation fails, the response is `201` with `status: "failed"` and its `error_code` / `error_message` (nothing was charged — the self report is free).

### Read your latest report

`GET /v1/qscore/my-report` returns your most recent self report (any status) without generating a new one — `404 not_found` if you never generated one.

### Download the PDF

`GET /v1/qscore/my-report/pdf` downloads the PDF of your latest self report (`Content-Disposition: attachment; filename="qscore_self_<id>.pdf"`). If the report is not `ready` yet, it answers `404 pdf_not_ready`.

The PDF carries the same public verification code as any Qscore report — anyone holding it can check its authenticity at `GET /verify/qscore/{code}` (see "Public verification" below).

### Self-report errors

| HTTP | Code | When | Solution |
| - | - | - | - |
| 403 | `kyc_required` | Your account's KYC/KYB is not approved | Complete identity verification first |
| 409 | `no_tax_id` | Your account has no verified tax id on file | Complete your verified profile or contact support |
| 400 | `invalid_tax_id` | The tax id on file is not valid for the account country | Contact support to fix your verified data |
| 409 | `identity_mismatch` | The account `tax_id` does not match the verified identity document (it was overridden) | Contact support — your verified data must be consistent |
| 404 | `not_found` | You never generated a self report (`GET`) | Generate one with `POST /v1/qscore/my-report` |
| 404 | `pdf_not_ready` | The report is not `ready` yet or has no PDF | Retry the download once the report is `ready` |

<Note>
  The commercial endpoint `POST /v1/qscore/reports` **rejects** `purpose: "self_access"` with `400 invalid_purpose` — self access only goes through `/v1/qscore/my-report`. The `risk_report_ready` webhook of a self report carries an extra `"purpose": "self_access"` field in its payload.
</Note>

## 1. Buy a report

`POST /v1/qscore/reports` creates and generates the full report. `idempotency_key` is **mandatory** (the report charges a fee: a retry with the same key returns the original report with `idempotency_hit: true` and never double-charges).

| Field | Type | Required | Description |
| - | - | - | - |
| `doc_id` | string | yes | Document ID of the subject. In Chile, the RUT (`11.111.111-1`); it is normalized to canonical form. |
| `country` | string | yes | ISO 3166-1 alpha-2 country of the document. Today `CL`. |
| `subject_type` | string | no | `person` or `company`. If omitted it is inferred from the document. |
| `purpose` | string | yes | Declared purpose (data protection law): `credit_evaluation`, `tenant_screening`, `hiring`, `supplier_onboarding`, `other`. |
| `lang` | string | no | Report language: `en` (default), `es`, `zh`. |
| `idempotency_key` | string | yes | Your unique key for this purchase. |

<Tabs>
  <Tab title="Person">
    ```bash Create person report theme={"theme":{"light":"github-light","dark":"github-dark"}}
    curl -X POST "https://api.qbank.cl/platform/v1/qscore/reports" \
      -H "Authorization: Bearer pk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "doc_id": "11.111.111-1",
        "country": "CL",
        "purpose": "credit_evaluation",
        "lang": "en",
        "idempotency_key": "qscore-2026-08-08-0001"
      }'
    ```
  </Tab>

  <Tab title="Company">
    ```bash Create company report theme={"theme":{"light":"github-light","dark":"github-dark"}}
    curl -X POST "https://api.qbank.cl/platform/v1/qscore/reports" \
      -H "Authorization: Bearer pk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "doc_id": "76.123.456-0",
        "country": "CL",
        "subject_type": "company",
        "purpose": "supplier_onboarding",
        "lang": "en",
        "idempotency_key": "qscore-2026-08-08-0002"
      }'
    ```
  </Tab>
</Tabs>

```json 201 Created (report ready) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "ready",
  "purpose": "credit_evaluation",
  "lang": "en",
  "band": "B",
  "model_version": "qscore-v1",
  "verify_code": "Qf47ac10b58cc4372a5670e02b2c3d4791a2b3c4d5e6f",
  "created_at": "2026-08-08T15:04:22Z",
  "score": 742,
  "completed_at": "2026-08-08T15:04:25Z",
  "report": {
    "meta": {
      "report_id": "QSR-f47ac10b58cc",
      "lang": "en",
      "purpose": "credit_evaluation",
      "generated_at": "2026-08-08T15:04:25Z",
      "verification_code": "Qf47ac10b58cc4372a5670e02b2c3d4791a2b3c4d5e6f",
      "verification_url": "https://business.cbpayapp.com/verify/qscore/Qf47ac10b58cc4372a5670e02b2c3d4791a2b3c4d5e6f"
    },
    "identity": {
      "subject_type": "person",
      "doc_id": "11111111-1",
      "name": "Juan Pérez González",
      "country": "CL"
    },
    "score": {
      "score": 742,
      "band": "B",
      "model_version": "qscore-v1",
      "reason_codes": [
        {"code": "ACTIVE_TRADELINES", "direction": "positive", "weight": "medium"},
        {"code": "CREDIT_HISTORY_DEPTH", "direction": "positive", "weight": "low"}
      ],
      "computed_at": "2026-08-08T15:04:25Z"
    },
    "summary": ["No open delinquencies on record", "Active tax status"],
    "internal_score": {"available": false},
    "sources": [
      {"source": "res_chile", "label": "Registro de Empresas y Sociedades (RES)", "records": 2, "fetched_at": "2026-08-08T15:04:23Z", "freshness": "live"}
    ]
  }
}
```

If something fails after the fee was charged, the fee is refunded and the response is the error with `error_code: "generation_failed"` persisted on the report. Re-running with the **same** `idempotency_key` returns the original report (or its failure) — it never charges twice.

## 2. The score (model v1)

The score runs `qscore-v1`: base **600**, range **1–999**, adjusted by adverse facts (open delinquencies, protests, bankruptcies, recent queries) and positive signals (active tradelines, credit history depth, company activity, alternative data).

| Band | Range | Reading |
| - | - | - |
| `A` | 800–999 | Excellent |
| `B` | 650–799 | Good |
| `C` | 500–649 | Fair |
| `D` | 350–499 | Weak |
| `E` | 1–349 | High risk |
| `SC` | — | No data found for the subject (score is `null`) |

Every report carries its `reason_codes` — the explainability layer of the score:

| Code | Direction | Meaning |
| - | - | - |
| `NO_DATA` | negative | No records found for the subject (band `SC`) |
| `BANKRUPTCY_OPEN` | negative | Open insolvency/bankruptcy proceeding |
| `OPEN_DELINQUENCY` | negative | Open delinquency in collections |
| `PROTESTO_OPEN` | negative | Unpaid protested document (bounced check/promissory note) |
| `RECENT_DELINQUENCY` | negative | Delinquency reported recently |
| `MANY_RECENT_QUERIES` | negative | Many reports purchased on the subject in the last 90 days |
| `ACTIVE_TRADELINES` | positive | Active, up-to-date credit lines |
| `CREDIT_HISTORY_DEPTH` | positive | Long credit history |
| `COMPANY_ACTIVE` | positive | Active company with tax activity |
| `COMPANY_NEW` | negative | Recently incorporated company |
| `ALTERNATIVE_POSITIVE` | positive | Positive alternative data (utilities, open finance) |
| `INTERNAL_ACTIVITY` | positive | Positive internal platform signals |

### Industry peer benchmark (company reports only)

**Company** reports may include the `peer_benchmark` block: the score's position **within its segment** — same country and same industry (ISIC classification, from the tax registry).

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
"peer_benchmark": {
  "available": true,
  "segment_code": "6499",
  "segment_label": "Other financial service activities",
  "peers": 12,
  "percentile": 75,
  "median_score": 640
}
```

| Field | Type | Description |
| - | - | - |
| `available` | boolean | `true` when there is a large enough comparable population. |
| `segment_code` / `segment_label` | string | The subject's industry code and label (ISIC). |
| `peers` | number | Comparable companies considered. |
| `percentile` | number | Percentage of peers with a **lower** score (75 = better than 75% of the segment). |
| `median_score` | number | Segment median score. |

Benchmark rules:

* **Company reports only** — person reports never include it (the block is omitted).
* **The industry comes from the tax registry** and is stamped on the subject (the latest known value wins).
* **The comparable population is the latest score of each company** in the same country and industry, excluding the evaluated subject.
* **Published only with at least 5 comparable companies** — below that, the block is omitted from the report (statistical context is never invented).
* `percentile` reads as "better than N% of the segment"; `median_score` is the segment median.

The report PDF includes the peer comparison section only when the block is available.

## 3. Query and history

### List reports

`GET /v1/qscore/reports` lists the reports purchased by your account. `from` and `to` (dates `YYYY-MM-DD`, organization timezone, both inclusive) are **mandatory**; filters `subject_id` and `status` (`pending`, `ready`, `failed`) are optional; pagination with `page` / `page_size` (default 50, max 200).

```bash List reports theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.qbank.cl/platform/v1/qscore/reports?from=2026-08-01&to=2026-08-31&status=ready&page=1&page_size=50" \
  -H "Authorization: Bearer pk_live_..."
```

```json 200 OK theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "items": [
    {
      "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "account_id": "ae8c91f2-…",
      "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "purpose": "credit_evaluation",
      "status": "ready",
      "lang": "en",
      "score": 742,
      "band": "B",
      "score_model_version": "qscore-v1",
      "reason_codes": [{"code": "ACTIVE_TRADELINES", "direction": "positive", "weight": "medium"}],
      "verify_code": "Qf47ac10b58cc4372a5670e02b2c3d4791a2b3c4d5e6f",
      "created_at": "2026-08-08T15:04:22Z",
      "completed_at": "2026-08-08T15:04:25Z"
    }
  ],
  "meta": {"page": 1, "page_size": 50, "total": 1}
}
```

### Report detail

`GET /v1/qscore/reports/{report_id}` returns the report; when `ready` it includes the full `report` object (same shape as the creation response).

### Download the PDF

`GET /v1/qscore/reports/{report_id}/pdf` downloads the branded PDF (`application/pdf`, filename `qscore_<report_id>.pdf`). Until the report is `ready` it answers `404 pdf_not_ready`. The PDF is a private document: download it **authenticated** — it is never attached to emails nor exposed on public URLs.

### Subject file and current score (without buying a new report)

`GET /v1/qscore/subjects/{doc_id}?country=CL` returns the subject file (identity + latest score) for a document you already reported on:

```json 200 OK (subject file) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "country": "CL",
  "doc_id": "11111111-1",
  "subject_type": "person",
  "display_name": "Juan Pérez González",
  "last_score": 742,
  "last_band": "B",
  "last_score_at": "2026-08-08T15:04:25Z"
}
```

`GET /v1/qscore/subjects/{doc_id}/score?country=CL` returns just the current score (`404 no_score` if the subject has none yet):

```json 200 OK (current score) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "doc_id": "11111111-1",
  "country": "CL",
  "band": "B",
  "model_version": "qscore-v1",
  "computed_at": "2026-08-08T15:04:25Z",
  "score": 742
}
```

## 4. Report statuses

| Status | Meaning | What to do |
| - | - | - |
| `pending` | Report created, generation in progress (transient inside the synchronous call) | Nothing — the `POST` response carries the final state |
| `ready` | Report generated: score, full JSON and PDF available (final) | Read the JSON, download the PDF, share the verification link |
| `failed` | Generation failed; the fee was **refunded** (final) | Read `error_code` / `error_message`, fix the cause, buy a new report with a **new** `idempotency_key` |

## 5. Errors

| HTTP | Code | When | Solution |
| - | - | - | - |
| 400 | `invalid_payload` | `doc_id`/`country` missing or malformed JSON | Send both fields with a valid JSON body |
| 400 | `purpose_required` | `purpose` missing | Declare the purpose (data protection law) |
| 400 | `invalid_purpose` | `purpose` outside the closed list, or `self_access` sent to the commercial endpoint | Use `credit_evaluation`, `tenant_screening`, `hiring`, `supplier_onboarding` or `other` — self access goes through `POST /v1/qscore/my-report` |
| 400 | `invalid_doc_id` | The document is not valid for the country (e.g. bad RUT check digit) | Fix the `doc_id` format for the country |
| 400 | `invalid_subject_type` | `subject_type` is not `person`/`company` and could not be inferred | Send `subject_type` explicitly |
| 400 | `idempotency_key_required` | Missing `idempotency_key` | Send a unique key per purchase |
| 404 | `not_found` | Report/subject does not exist (or belongs to another account) | Check the ID |
| 404 | `no_score` | The subject has no computed score yet | Buy a report first |
| 404 | `pdf_not_ready` | The report is not `ready` yet | Poll the detail until `status=ready` |
| 502 | `generation_failed` | The report could not be generated after charging | The fee was refunded; retry later or contact support |

See the full catalogue in [Errors](/en/errors).

## 6. Webhooks

Subscribe to the Qscore events in your [webhook settings](/en/webhooks). All three are account-audience events, signed like every other webhook.

<AccordionGroup>
  <Accordion title="risk_report_ready — a report finished generating">
    ```json risk_report_ready theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {
      "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "doc_id": "11111111-1",
      "country": "CL",
      "subject_type": "person",
      "score": 742,
      "band": "B",
      "verify_code": "Qf47ac10b58cc4372a5670e02b2c3d4791a2b3c4d5e6f"
    }
    ```

    A self report (see "Your own report (self)") emits the same event with an extra `"purpose": "self_access"` field; commercial reports omit it.
  </Accordion>

  <Accordion title="risk_score_changed — the subject's score moved">
    Fired when a new report computes a score different from the subject's previous one.

    ```json risk_score_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {
      "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "old_score": 715,
      "new_score": 742,
      "old_band": "B",
      "new_band": "B"
    }
    ```
  </Accordion>

  <Accordion title="risk_monitoring_alert — a monitored subject changed">
    Fired for every active monitoring subscription when the subject's score drops below your `monitor_since_score` floor, new bureau records appear, or records are removed. The first evaluation after subscribing only seeds the baseline and never alerts.

    ```json risk_monitoring_alert theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {
      "monitoring_id": "2f7b1c94-8d3a-4c5e-9f01-6a7b8c9d0e11",
      "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "doc_id": "11111111-1",
      "country": "CL",
      "subject_type": "person",
      "triggers": ["score_drop_below", "new_records"],
      "previous_score": 688,
      "score": 612,
      "band": "C",
      "record_count": 4,
      "new_records": [
        {
          "source": "res_chile",
          "record_type": "debt_collection",
          "reported_at": "2026-08-08",
          "amount": "350000",
          "currency": "CLP",
          "status": "open"
        }
      ],
      "detected_at": "2026-08-08T16:30:00Z"
    }
    ```
  </Accordion>
</AccordionGroup>

## 7. Public verification

Every report PDF prints a **verification code** and URL. Anyone holding the code can check the report's authenticity — without PII — at `GET /verify/qscore/{code}` (no auth):

```json 200 OK (valid report) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "valid": true,
  "type": "verification_report",
  "kind": "qscore",
  "status": "ready",
  "decision": "B",
  "date": "2026-08-08",
  "issued_by": "CBPay"
}
```

An invalid or tampered code answers `404` with `{"valid": false, ...}`. The endpoint is rate-limited per IP and reveals nothing beyond validity, band and date.

## 8. ARCO disputes

Data subjects can exercise their ARCO rights (access, rectification, cancellation, opposition). Your account opens a dispute against a specific record of a subject:

```bash Open a dispute theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.qbank.cl/platform/v1/qscore/subjects/11111111-1/disputes?country=CL" \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "record_source": "res_chile",
    "record_ref": "RES-2026-04512",
    "reason": "The reported delinquency was paid on 2026-07-30",
    "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
  }'
```

```json 201 Created theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "dispute_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "record_source": "res_chile",
  "record_ref": "RES-2026-04512",
  "reason": "The reported delinquency was paid on 2026-07-30",
  "status": "open",
  "created_by": "ae8c91f2-…",
  "created_at": "2026-08-08T16:11:00Z"
}
```

Dispute lifecycle: `open` → `under_review` → `resolved_corrected` | `resolved_rejected` (final). List them with `GET /v1/qscore/subjects/{doc_id}/disputes?country=CL&status=open` (paginated) and read one with `GET /v1/qscore/disputes/{dispute_id}`. Resolution is handled by your org admin from the admin panel.

## 9. Monitoring

Once you own a `ready` report on a subject, subscribe to **continuous monitoring** and receive a `risk_monitoring_alert` webhook every time something relevant changes: the score drops below your threshold, new bureau records appear, or records are removed. Monitoring is **free** — the only requirement is the purchased report (the same policy as the score endpoint: nobody watches a third party without paying to know them first).

```bash Subscribe (or update thresholds) theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT "https://api.qbank.cl/platform/v1/qscore/subjects/11111111-1/monitoring" \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "country": "CL",
    "monitor_since_score": 640,
    "only_material": true
  }'
```

```json 200 OK theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "monitoring_id": "2f7b1c94-8d3a-4c5e-9f01-6a7b8c9d0e11",
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "doc_id": "11111111-1",
  "country": "CL",
  "subject_type": "person",
  "active": true,
  "only_material": true,
  "monitor_since_score": 640,
  "last_score": 688,
  "last_record_count": 3,
  "created_at": "2026-08-08T16:20:00Z",
  "last_checked_at": "2026-08-08T16:25:00Z"
}
```

* `monitor_since_score` (optional, 1–999): alert when the score falls below this threshold (`score_drop_below` trigger).
* `only_material` (default `false`): when `true`, only material changes fire the alert.
* The worker re-evaluates every monitored subject every **\~5 minutes**. The first pass only seeds the baseline — it never alerts on data you already saw in the report you paid for.

Read one subscription with `GET /v1/qscore/subjects/{doc_id}/monitoring`, list every monitored subject of the account with `GET /v1/qscore/monitoring?active=true&page=1&page_size=50` (paginated: `items`, `page`, `page_size`, `total`), and deactivate with `DELETE`:

```bash Deactivate monitoring theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE "https://api.qbank.cl/platform/v1/qscore/subjects/11111111-1/monitoring?country=CL" \
  -H "Authorization: Bearer pk_live_..."
```

```json 200 OK theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "doc_id": "11111111-1",
  "active": false
}
```

`DELETE` deactivates (`active: false`) — the subscription history is never deleted, and a new `PUT` reactivates it with fresh thresholds.

<Warning>
  Without a purchased `ready` report for the subject, `PUT` answers `403 report_required` — the **same** response a non-existent subject gets, by design, so the endpoint never reveals whether a document exists in the bureau. See [errors](/en/errors).
</Warning>

The alert payload (`risk_monitoring_alert`) carries the triggers (`score_drop_below`, `new_records`, `records_removed`), the current and previous score, the band and the new records — full example in [webhooks](/en/webhooks).

## 10. Batch scoring (portfolios)

To score a whole portfolio instead of one subject at a time, submit a **batch** with `POST /v1/qscore/batches`: up to 5,000 subjects (JSON or CSV), one shared `country` and `purpose`, and an estimated fee computed up front. The API answers `202 Accepted` immediately and a background worker generates the individual reports one by one — each item is a standard Qscore report with its own PDF, fee and automatic refund if its generation fails.

* **Terminal fan-out**: when the batch finishes you receive exactly **one** `risk_batch_completed` webhook and **one** summary email (never one per subject).
* **Follow-up**: list and inspect batches, page through their items and download the consolidated CSV at `GET /v1/qscore/batches/{id}/results.csv`.

The full flow (mermaid diagram, per-item rejection, statuses, errors and FAQ) lives in the [batch scoring guide](/en/guides/qscore-batch).

## FAQ

<AccordionGroup>
  <Accordion title="Is the score recomputed on every report?">
    Yes. Every purchase fetches the sources live and recomputes the score with the current `qscore-v1` model. If a source is down, the report is generated with persisted data and the source is declared `cached`/`unavailable` in the `sources` section — never silently.
  </Accordion>

  <Accordion title="What happens if the report fails after charging me?">
    The fee is refunded automatically in the same flow and the report ends `failed` with its `error_code`. Your `idempotency_key` replays to that failed report; to try again, use a new key.
  </Accordion>

  <Accordion title="Why is purpose mandatory?">
    Chilean data protection law requires a declared, legitimate purpose to query a person's or company's credit data. It is stored with the report and printed in it (auditability for the data subject).
  </Accordion>

  <Accordion title="Can I check someone's score without paying a report?">
    Yes — if you already purchased a report on that subject, `GET /v1/qscore/subjects/{doc_id}/score` returns the latest computed score at no extra cost. The first report on a subject is always a paid full report.
  </Accordion>

  <Accordion title="Is the PDF sent by email?">
    No. The "report ready" email carries no attachment on purpose (third-party data minimization). The PDF only downloads authenticated from the API.
  </Accordion>

  <Accordion title="Which countries are supported?">
    Chile today (`country: "CL"`, RUT as `doc_id`). The contract is country-agnostic: new countries will work with the same endpoints once their sources are plugged in.
  </Accordion>

  <Accordion title="How often is a monitored subject checked?">
    Every \~5 minutes. The `risk_monitoring_alert` webhook only fires when something changed against the baseline (or only on material changes with `only_material: true`) — you never get paged for a no-op.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.